From c8453035da6246a232d4f8ba4e3c2b74c22e9b28 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 20 May 2023 19:57:48 +0200 Subject: [PATCH] LttP: extract Dungeon and Boss from core (#1787) --- BaseClasses.py | 65 ------- worlds/alttp/Bosses.py | 76 +++++--- worlds/alttp/Dungeons.py | 126 +++++++++--- worlds/alttp/ItemPool.py | 184 +++++++++--------- worlds/alttp/Rom.py | 62 +++--- worlds/alttp/SubClasses.py | 8 +- worlds/alttp/__init__.py | 102 +++++----- worlds/alttp/test/dungeons/TestDungeon.py | 7 +- worlds/alttp/test/inverted/TestInverted.py | 2 +- .../test/inverted/TestInvertedBombRules.py | 2 +- .../TestInvertedMinor.py | 2 +- .../test/inverted_owg/TestInvertedOWG.py | 2 +- worlds/oot/Hints.py | 11 +- 13 files changed, 343 insertions(+), 306 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 68407ee08388..2802f6d8648e 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -96,7 +96,6 @@ def __init__(self, players: int): self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids} self.glitch_triforce = False self.algorithm = 'balanced' - self.dungeons: Dict[Tuple[str, int], Dungeon] = {} self.groups = {} self.regions = [] self.shops = [] @@ -386,12 +385,6 @@ def get_location(self, location: str, player: int) -> Location: self._recache() return self._location_cache[location, player] - def get_dungeon(self, dungeonname: str, player: int) -> Dungeon: - try: - return self.dungeons[dungeonname, player] - except KeyError as e: - raise KeyError('No such dungeon %s for player %d' % (dungeonname, player)) from e - def get_all_state(self, use_cache: bool) -> CollectionState: cached = getattr(self, "_all_state", None) if use_cache and cached: @@ -801,7 +794,6 @@ class Region: entrances: List[Entrance] exits: List[Entrance] locations: List[Location] - dungeon: Optional[Dungeon] = None def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None): self.name = name @@ -904,63 +896,6 @@ def __str__(self): return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})' -class Dungeon(object): - def __init__(self, name: str, regions: List[Region], big_key: Item, small_keys: List[Item], - dungeon_items: List[Item], player: int): - self.name = name - self.regions = regions - self.big_key = big_key - self.small_keys = small_keys - self.dungeon_items = dungeon_items - self.bosses = dict() - self.player = player - self.multiworld = None - - @property - def boss(self) -> Optional[Boss]: - return self.bosses.get(None, None) - - @boss.setter - def boss(self, value: Optional[Boss]): - self.bosses[None] = value - - @property - def keys(self) -> List[Item]: - return self.small_keys + ([self.big_key] if self.big_key else []) - - @property - def all_items(self) -> List[Item]: - return self.dungeon_items + self.keys - - def is_dungeon_item(self, item: Item) -> bool: - return item.player == self.player and item.name in (dungeon_item.name for dungeon_item in self.all_items) - - def __eq__(self, other: Dungeon) -> bool: - if not other: - return False - return self.name == other.name and self.player == other.player - - def __repr__(self): - return self.__str__() - - def __str__(self): - return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})' - - -class Boss(): - def __init__(self, name: str, enemizer_name: str, defeat_rule: Callable, player: int): - self.name = name - self.enemizer_name = enemizer_name - self.defeat_rule = defeat_rule - self.player = player - - def can_defeat(self, state) -> bool: - return self.defeat_rule(state, self.player) - - def __repr__(self): - return f"Boss({self.name})" - - class LocationProgressType(IntEnum): DEFAULT = 1 PRIORITY = 2 diff --git a/worlds/alttp/Bosses.py b/worlds/alttp/Bosses.py index 8c6dcabc87e6..90ffe9dcf4b1 100644 --- a/worlds/alttp/Bosses.py +++ b/worlds/alttp/Bosses.py @@ -1,10 +1,29 @@ +from __future__ import annotations + import logging -from typing import Optional, Union, List, Tuple, Callable, Dict +from typing import Optional, Union, List, Tuple, Callable, Dict, TYPE_CHECKING -from BaseClasses import Boss from Fill import FillError from .Options import LTTPBosses as Bosses -from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, has_melee_weapon, has_fire_source +from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, \ + has_melee_weapon, has_fire_source + +if TYPE_CHECKING: + from . import ALTTPWorld + + +class Boss: + def __init__(self, name: str, enemizer_name: str, defeat_rule: Callable, player: int): + self.name = name + self.enemizer_name = enemizer_name + self.defeat_rule = defeat_rule + self.player = player + + def can_defeat(self, state) -> bool: + return self.defeat_rule(state, self.player) + + def __repr__(self): + return f"Boss({self.name})" def BossFactory(boss: str, player: int) -> Optional[Boss]: @@ -166,10 +185,10 @@ def GanonDefeatRule(state, player: int) -> bool: ] -def place_plando_bosses(bosses: List[str], world, player: int) -> Tuple[List[str], List[Tuple[str, str]]]: +def place_plando_bosses(world: "ALTTPWorld", bosses: List[str]) -> Tuple[List[str], List[Tuple[str, str]]]: # Most to least restrictive order boss_locations = boss_location_table.copy() - world.random.shuffle(boss_locations) + world.multiworld.random.shuffle(boss_locations) boss_locations.sort(key=lambda location: -int(restrictive_boss_locations[location])) already_placed_bosses: List[str] = [] @@ -184,12 +203,12 @@ def place_plando_bosses(bosses: List[str], world, player: int) -> Tuple[List[str level = loc[-1] loc = " ".join(loc[:-1]) loc = loc.title().replace("Of", "of") - place_boss(world, player, boss, loc, level) + place_boss(world, boss, loc, level) already_placed_bosses.append(boss) boss_locations.remove((loc, level)) else: # boss chosen with no specified locations boss = boss.title() - boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations) + boss_locations, already_placed_bosses = place_where_possible(world, boss, boss_locations) return already_placed_bosses, boss_locations @@ -224,20 +243,23 @@ def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) -> for boss in boss_table if not boss.startswith("Agahnim")) -def place_boss(world, player: int, boss: str, location: str, level: Optional[str]) -> None: - if location == 'Ganons Tower' and world.mode[player] == 'inverted': +def place_boss(world: "ALTTPWorld", boss: str, location: str, level: Optional[str]) -> None: + player = world.player + if location == 'Ganons Tower' and world.multiworld.mode[player] == 'inverted': location = 'Inverted Ganons Tower' logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else '')) - world.get_dungeon(location, player).bosses[level] = BossFactory(boss, player) + world.dungeons[location].bosses[level] = BossFactory(boss, player) -def format_boss_location(location: str, level: str) -> str: - return location + (' (' + level + ')' if level else '') +def format_boss_location(location_name: str, level: str) -> str: + return location_name + (' (' + level + ')' if level else '') -def place_bosses(world, player: int) -> None: +def place_bosses(world: "ALTTPWorld") -> None: + multiworld = world.multiworld + player = world.player # will either be an int or a lower case string with ';' between options - boss_shuffle: Union[str, int] = world.boss_shuffle[player].value + boss_shuffle: Union[str, int] = multiworld.boss_shuffle[player].value already_placed_bosses: List[str] = [] remaining_locations: List[Tuple[str, str]] = [] # handle plando @@ -246,14 +268,14 @@ def place_bosses(world, player: int) -> None: options = boss_shuffle.split(";") boss_shuffle = Bosses.options[options.pop()] # place our plando bosses - already_placed_bosses, remaining_locations = place_plando_bosses(options, world, player) + already_placed_bosses, remaining_locations = place_plando_bosses(world, options) if boss_shuffle == Bosses.option_none: # vanilla boss locations return # Most to least restrictive order if not remaining_locations and not already_placed_bosses: remaining_locations = boss_location_table.copy() - world.random.shuffle(remaining_locations) + multiworld.random.shuffle(remaining_locations) remaining_locations.sort(key=lambda location: -int(restrictive_boss_locations[location])) all_bosses = sorted(boss_table.keys()) # sorted to be deterministic on older pythons @@ -263,7 +285,7 @@ def place_bosses(world, player: int) -> None: if boss_shuffle == Bosses.option_basic: # vanilla bosses shuffled bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm'] else: # all bosses present, the three duplicates chosen at random - bosses = placeable_bosses + world.random.sample(placeable_bosses, 3) + bosses = placeable_bosses + multiworld.random.sample(placeable_bosses, 3) # there is probably a better way to do this while already_placed_bosses: @@ -275,7 +297,7 @@ def place_bosses(world, player: int) -> None: logging.debug('Bosses chosen %s', bosses) - world.random.shuffle(bosses) + multiworld.random.shuffle(bosses) for loc, level in remaining_locations: for _ in range(len(bosses)): boss = bosses.pop() @@ -288,39 +310,39 @@ def place_bosses(world, player: int) -> None: else: raise FillError(f'Could not place boss for location {format_boss_location(loc, level)}') - place_boss(world, player, boss, loc, level) + place_boss(world, boss, loc, level) elif boss_shuffle == Bosses.option_chaos: # all bosses chosen at random for loc, level in remaining_locations: try: - boss = world.random.choice( + boss = multiworld.random.choice( [b for b in placeable_bosses if can_place_boss(b, loc, level)]) except IndexError: raise FillError(f'Could not place boss for location {format_boss_location(loc, level)}') else: - place_boss(world, player, boss, loc, level) + place_boss(world, boss, loc, level) elif boss_shuffle == Bosses.option_singularity: - primary_boss = world.random.choice(placeable_bosses) - remaining_boss_locations, _ = place_where_possible(world, player, primary_boss, remaining_locations) + primary_boss = multiworld.random.choice(placeable_bosses) + remaining_boss_locations, _ = place_where_possible(world, primary_boss, remaining_locations) if remaining_boss_locations: # pick a boss to go into the remaining locations - remaining_boss = world.random.choice([boss for boss in placeable_bosses if all( + remaining_boss = multiworld.random.choice([boss for boss in placeable_bosses if all( can_place_boss(boss, loc, level) for loc, level in remaining_boss_locations)]) - remaining_boss_locations, _ = place_where_possible(world, player, remaining_boss, remaining_boss_locations) + remaining_boss_locations, _ = place_where_possible(world, remaining_boss, remaining_boss_locations) if remaining_boss_locations: raise Exception("Unfilled boss locations!") else: raise FillError(f"Could not find boss shuffle mode {boss_shuffle}") -def place_where_possible(world, player: int, boss: str, boss_locations) -> Tuple[List[Tuple[str, str]], List[str]]: +def place_where_possible(world: "ALTTPWorld", boss: str, boss_locations) -> Tuple[List[Tuple[str, str]], List[str]]: remainder: List[Tuple[str, str]] = [] placed_bosses: List[str] = [] for loc, level in boss_locations: # place that boss where it can go if can_place_boss(boss, loc, level): - place_boss(world, player, boss, loc, level) + place_boss(world, boss, loc, level) placed_bosses.append(boss) else: remainder.append((loc, level)) diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index 575bd37d9482..b789fd6db638 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -1,28 +1,83 @@ +from __future__ import annotations + import typing +from typing import List, Optional -from BaseClasses import CollectionState, Dungeon +from BaseClasses import CollectionState, Region, MultiWorld from Fill import fill_restrictive -from .Bosses import BossFactory +from .Bosses import BossFactory, Boss from .Items import ItemFactory from .Regions import lookup_boss_drops from .Options import smallkey_shuffle if typing.TYPE_CHECKING: - from .SubClasses import ALttPLocation + from .SubClasses import ALttPLocation, ALttPItem + from . import ALTTPWorld + + +class Dungeon: + def __init__(self, name: str, regions: List[Region], big_key: ALttPItem, small_keys: List[ALttPItem], + dungeon_items: List[ALttPItem], player: int): + self.name = name + self.regions = regions + self.big_key = big_key + self.small_keys = small_keys + self.dungeon_items = dungeon_items + self.bosses = dict() + self.player = player + self.multiworld = None + + @property + def boss(self) -> Optional[Boss]: + return self.bosses.get(None, None) + + @boss.setter + def boss(self, value: Optional[Boss]): + self.bosses[None] = value + + @property + def keys(self) -> List[ALttPItem]: + return self.small_keys + ([self.big_key] if self.big_key else []) + + @property + def all_items(self) -> List[ALttPItem]: + return self.dungeon_items + self.keys + def is_dungeon_item(self, item: ALttPItem) -> bool: + return item.player == self.player and item.name in (dungeon_item.name for dungeon_item in self.all_items) + + def __eq__(self, other: Dungeon) -> bool: + if not other: + return False + return self.name == other.name and self.player == other.player + + def __repr__(self): + return self.__str__() + + def __str__(self): + return self.multiworld.get_name_string_for_object(self) if self.multiworld \ + else f'{self.name} (Player {self.player})' + + +def create_dungeons(world: "ALTTPWorld"): + multiworld = world.multiworld + player = world.player -def create_dungeons(world, player): def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): dungeon = Dungeon(name, dungeon_regions, big_key, - [] if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal else small_keys, + [] if multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal else small_keys, dungeon_items, player) for item in dungeon.all_items: item.dungeon = dungeon dungeon.boss = BossFactory(default_boss, player) if default_boss else None - for region in dungeon.regions: - world.get_region(region, player).dungeon = dungeon - dungeon.multiworld = world + regions = [] + for region_name in dungeon.regions: + region = multiworld.get_region(region_name, player) + region.dungeon = dungeon + regions.append(region) + dungeon.multiworld = multiworld + dungeon.regions = regions return dungeon ES = make_dungeon('Hyrule Castle', None, ['Hyrule Castle', 'Sewers', 'Sewer Drop', 'Sewers (Dark)', 'Sanctuary'], @@ -83,7 +138,7 @@ def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dunge ItemFactory(['Small Key (Turtle Rock)'] * 4, player), ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player)) - if world.mode[player] != 'inverted': + if multiworld.mode[player] != 'inverted': AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), []) GT = make_dungeon('Ganons Tower', 'Agahnim2', @@ -111,26 +166,34 @@ def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dunge GT.bosses['top'] = BossFactory('Moldorm', player) for dungeon in [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT]: - world.dungeons[dungeon.name, dungeon.player] = dungeon + world.dungeons[dungeon.name] = dungeon -def get_dungeon_item_pool(world) -> typing.List: - return [item for dungeon in world.dungeons.values() for item in dungeon.all_items] +def get_dungeon_item_pool(multiworld: MultiWorld) -> typing.List[ALttPItem]: + return [item + for world in multiworld.get_game_worlds("A Link to the Past") + for item in get_dungeon_item_pool_player(world)] -def get_dungeon_item_pool_player(world, player) -> typing.List: - return [item for dungeon in world.dungeons.values() if dungeon.player == player for item in dungeon.all_items] +def get_dungeon_item_pool_player(world) -> typing.List[ALttPItem]: + return [item + for dungeon in world.dungeons.values() + for item in dungeon.all_items] -def get_unfilled_dungeon_locations(multiworld) -> typing.List: - return [location for location in multiworld.get_locations() if not location.item and location.parent_region.dungeon] +def get_unfilled_dungeon_locations(multiworld: MultiWorld) -> typing.List[ALttPLocation]: + return [location + for world in multiworld.get_game_worlds("A Link to the Past") + for dungeon in world.dungeons.values() + for region in dungeon.regions + for location in region.locations if not location.item] -def fill_dungeons_restrictive(world): +def fill_dungeons_restrictive(multiworld: MultiWorld): """Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside.""" localized: set = set() dungeon_specific: set = set() - for subworld in world.get_game_worlds("A Link to the Past"): + for subworld in multiworld.get_game_worlds("A Link to the Past"): player = subworld.player localized |= {(player, item_name) for item_name in subworld.dungeon_local_item_names} @@ -138,12 +201,12 @@ def fill_dungeons_restrictive(world): subworld.dungeon_specific_item_names} if localized: - in_dungeon_items = [item for item in get_dungeon_item_pool(world) if (item.player, item.name) in localized] + in_dungeon_items = [item for item in get_dungeon_item_pool(multiworld) if (item.player, item.name) in localized] if in_dungeon_items: - restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if + restricted_players = {player for player, restricted in multiworld.restrict_dungeon_item_on_boss.items() if restricted} locations: typing.List["ALttPLocation"] = [ - location for location in get_unfilled_dungeon_locations(world) + location for location in get_unfilled_dungeon_locations(multiworld) # filter boss if not (location.player in restricted_players and location.name in lookup_boss_drops)] if dungeon_specific: @@ -153,7 +216,7 @@ def fill_dungeons_restrictive(world): location.item_rule = lambda item, dungeon=dungeon, orig_rule=orig_rule: \ (not (item.player, item.name) in dungeon_specific or item.dungeon is dungeon) and orig_rule(item) - world.random.shuffle(locations) + multiworld.random.shuffle(locations) # Dungeon-locked items have to be placed first, to not run out of spaces for dungeon-locked items # subsort in the order Big Key, Small Key, Other before placing dungeon items @@ -162,14 +225,15 @@ def fill_dungeons_restrictive(world): 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 which contains only the items from get_pre_fill_items which aren't in_dungeon + # Construct a partial all_state which contains only the items from get_pre_fill_items, + # which aren't in_dungeon in_dungeon_player_ids = {item.player for item in in_dungeon_items} - all_state_base = CollectionState(world) - for item in world.itempool: - world.worlds[item.player].collect(all_state_base, item) + 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 += world.worlds[player].get_pre_fill_items() + pre_fill_items += multiworld.worlds[player].get_pre_fill_items() for item in in_dungeon_items: try: pre_fill_items.remove(item) @@ -177,16 +241,15 @@ def fill_dungeons_restrictive(world): # pre_fill_items should be a subset of in_dungeon_items, but just in case pass for item in pre_fill_items: - world.worlds[item.player].collect(all_state_base, item) + multiworld.worlds[item.player].collect(all_state_base, item) all_state_base.sweep_for_events() - # Remove completion condition so that minimal-accessibility worlds place keys properly for player in {item.player for item in in_dungeon_items}: if all_state_base.has("Triforce", player): - all_state_base.remove(world.worlds[player].create_item("Triforce")) + all_state_base.remove(multiworld.worlds[player].create_item("Triforce")) - fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True) + fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True) dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], @@ -200,3 +263,4 @@ def fill_dungeons_restrictive(world): 'Ice Palace - Prize': [0x155BF], 'Misery Mire - Prize': [0x155B9], 'Turtle Rock - Prize': [0x155C7, 0x155A7, 0x155AA, 0x155AB]} + diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 5761e5f099fe..67b79058b8f4 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -226,40 +226,40 @@ def generate_itempool(world): player = world.player - world = world.multiworld + multiworld = world.multiworld - if world.difficulty[player] not in difficulties: - raise NotImplementedError(f"Diffulty {world.difficulty[player]}") - if world.goal[player] not in {'ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'icerodhunt', + if multiworld.difficulty[player] not in difficulties: + raise NotImplementedError(f"Diffulty {multiworld.difficulty[player]}") + if multiworld.goal[player] not in {'ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'icerodhunt', 'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'}: - raise NotImplementedError(f"Goal {world.goal[player]} for player {player}") - if world.mode[player] not in {'open', 'standard', 'inverted'}: - raise NotImplementedError(f"Mode {world.mode[player]} for player {player}") - if world.timer[player] not in {False, 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'}: - raise NotImplementedError(f"Timer {world.mode[player]} for player {player}") - - if world.timer[player] in ['ohko', 'timed-ohko']: - world.can_take_damage[player] = False - if world.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt', 'icerodhunt']: - world.push_item(world.get_location('Ganon', player), ItemFactory('Nothing', player), False) + raise NotImplementedError(f"Goal {multiworld.goal[player]} for player {player}") + if multiworld.mode[player] not in {'open', 'standard', 'inverted'}: + raise NotImplementedError(f"Mode {multiworld.mode[player]} for player {player}") + if multiworld.timer[player] not in {False, 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'}: + raise NotImplementedError(f"Timer {multiworld.mode[player]} for player {player}") + + if multiworld.timer[player] in ['ohko', 'timed-ohko']: + multiworld.can_take_damage[player] = False + if multiworld.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt', 'icerodhunt']: + multiworld.push_item(multiworld.get_location('Ganon', player), ItemFactory('Nothing', player), False) else: - world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False) - - if world.goal[player] == 'icerodhunt': - world.progression_balancing[player].value = 0 - loc = world.get_location('Turtle Rock - Boss', player) - world.push_item(loc, ItemFactory('Triforce Piece', player), False) - world.treasure_hunt_count[player] = 1 - if world.boss_shuffle[player] != 'none': - if isinstance(world.boss_shuffle[player].value, str) and 'turtle rock-' not in world.boss_shuffle[player].value: - world.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{world.boss_shuffle[player].current_key}') - elif isinstance(world.boss_shuffle[player].value, int): - world.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{world.boss_shuffle[player].current_key}') + multiworld.push_item(multiworld.get_location('Ganon', player), ItemFactory('Triforce', player), False) + + if multiworld.goal[player] == 'icerodhunt': + multiworld.progression_balancing[player].value = 0 + loc = multiworld.get_location('Turtle Rock - Boss', player) + multiworld.push_item(loc, ItemFactory('Triforce Piece', player), False) + multiworld.treasure_hunt_count[player] = 1 + if multiworld.boss_shuffle[player] != 'none': + if isinstance(multiworld.boss_shuffle[player].value, str) and 'turtle rock-' not in multiworld.boss_shuffle[player].value: + multiworld.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{multiworld.boss_shuffle[player].current_key}') + elif isinstance(multiworld.boss_shuffle[player].value, int): + multiworld.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{multiworld.boss_shuffle[player].current_key}') else: logging.warning(f'Cannot guarantee that Trinexx is the boss of Turtle Rock for player {player}') loc.event = True loc.locked = True - itemdiff = difficulties[world.difficulty[player]] + itemdiff = difficulties[multiworld.difficulty[player]] itempool = [] itempool.extend(itemdiff.alwaysitems) itempool.remove('Ice Rod') @@ -270,7 +270,7 @@ def generate_itempool(world): itempool.extend(itemdiff.bottles) itempool.extend(itemdiff.basicbow) itempool.extend(itemdiff.basicarmor) - if not world.swordless[player]: + if not multiworld.swordless[player]: itempool.extend(itemdiff.basicsword) itempool.extend(itemdiff.basicmagic) itempool.extend(itemdiff.basicglove) @@ -279,28 +279,28 @@ def generate_itempool(world): itempool.extend(['Rupees (300)'] * 34) itempool.extend(['Bombs (10)'] * 5) itempool.extend(['Arrows (10)'] * 7) - if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal: + if multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal: itempool.extend(itemdiff.universal_keys) itempool.append('Small Key (Universal)') for item in itempool: - world.push_precollected(ItemFactory(item, player)) + multiworld.push_precollected(ItemFactory(item, player)) - if world.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']: - region = world.get_region('Light World', player) + if multiworld.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']: + region = multiworld.get_region('Light World', player) loc = ALttPLocation(player, "Murahdahla", parent=region) loc.access_rule = lambda state: has_triforce_pieces(state, player) region.locations.append(loc) - world.clear_location_cache() + multiworld.clear_location_cache() - world.push_item(loc, ItemFactory('Triforce', player), False) + multiworld.push_item(loc, ItemFactory('Triforce', player), False) loc.event = True loc.locked = True - world.get_location('Ganon', player).event = True - world.get_location('Ganon', player).locked = True + multiworld.get_location('Ganon', player).event = True + multiworld.get_location('Ganon', player).locked = True event_pairs = [ ('Agahnim 1', 'Beat Agahnim 1'), ('Agahnim 2', 'Beat Agahnim 2'), @@ -312,26 +312,26 @@ def generate_itempool(world): ('Flute Activation Spot', 'Activated Flute') ] for location_name, event_name in event_pairs: - location = world.get_location(location_name, player) + location = multiworld.get_location(location_name, player) event = ItemFactory(event_name, player) - world.push_item(location, event, False) + multiworld.push_item(location, event, False) location.event = location.locked = True # set up item pool additional_triforce_pieces = 0 - if world.custom: + if multiworld.custom: (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, - treasure_hunt_icon) = make_custom_item_pool(world, player) - world.rupoor_cost = min(world.customitemarray[67], 9999) + treasure_hunt_icon) = make_custom_item_pool(multiworld, player) + multiworld.rupoor_cost = min(multiworld.customitemarray[67], 9999) else: pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, \ - treasure_hunt_icon, additional_triforce_pieces = get_pool_core(world, player) + treasure_hunt_icon, additional_triforce_pieces = get_pool_core(multiworld, player) for item in precollected_items: - world.push_precollected(ItemFactory(item, player)) + multiworld.push_precollected(ItemFactory(item, player)) - if world.mode[player] == 'standard' and not has_melee_weapon(world.state, player): + if multiworld.mode[player] == 'standard' and not has_melee_weapon(multiworld.state, player): if "Link's Uncle" not in placed_items: found_sword = False found_bow = False @@ -347,60 +347,60 @@ def generate_itempool(world): if item in ['Hammer', 'Bombs (10)', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']: if item not in possible_weapons: possible_weapons.append(item) - starting_weapon = world.random.choice(possible_weapons) + starting_weapon = multiworld.random.choice(possible_weapons) placed_items["Link's Uncle"] = starting_weapon pool.remove(starting_weapon) - if placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Cane of Somaria', 'Cane of Byrna'] and world.enemy_health[player] not in ['default', 'easy']: - world.escape_assist[player].append('bombs') + if placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Cane of Somaria', 'Cane of Byrna'] and multiworld.enemy_health[player] not in ['default', 'easy']: + multiworld.escape_assist[player].append('bombs') for (location, item) in placed_items.items(): - world.get_location(location, player).place_locked_item(ItemFactory(item, player)) + multiworld.get_location(location, player).place_locked_item(ItemFactory(item, player)) items = ItemFactory(pool, player) # convert one Progressive Bow into Progressive Bow (Alt), in ID only, for ganon silvers hint text - if world.worlds[player].has_progressive_bows: + if multiworld.worlds[player].has_progressive_bows: for item in items: if item.code == 0x64: # Progressive Bow item.code = 0x65 # Progressive Bow (Alt) break if clock_mode is not None: - world.clock_mode[player] = clock_mode + multiworld.clock_mode[player] = clock_mode if treasure_hunt_count is not None: - world.treasure_hunt_count[player] = treasure_hunt_count % 999 + multiworld.treasure_hunt_count[player] = treasure_hunt_count % 999 if treasure_hunt_icon is not None: - world.treasure_hunt_icon[player] = treasure_hunt_icon - - dungeon_items = [item for item in get_dungeon_item_pool_player(world, player) - if item.name not in world.worlds[player].dungeon_local_item_names] - dungeon_item_replacements = difficulties[world.difficulty[player]].extras[0]\ - + difficulties[world.difficulty[player]].extras[1]\ - + difficulties[world.difficulty[player]].extras[2]\ - + difficulties[world.difficulty[player]].extras[3]\ - + difficulties[world.difficulty[player]].extras[4] - world.random.shuffle(dungeon_item_replacements) - if world.goal[player] == 'icerodhunt': + multiworld.treasure_hunt_icon[player] = treasure_hunt_icon + + dungeon_items = [item for item in get_dungeon_item_pool_player(world) + if item.name not in multiworld.worlds[player].dungeon_local_item_names] + dungeon_item_replacements = difficulties[multiworld.difficulty[player]].extras[0]\ + + difficulties[multiworld.difficulty[player]].extras[1]\ + + difficulties[multiworld.difficulty[player]].extras[2]\ + + difficulties[multiworld.difficulty[player]].extras[3]\ + + difficulties[multiworld.difficulty[player]].extras[4] + multiworld.random.shuffle(dungeon_item_replacements) + if multiworld.goal[player] == 'icerodhunt': for item in dungeon_items: - world.itempool.append(ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)) - world.push_precollected(item) + multiworld.itempool.append(ItemFactory(GetBeemizerItem(multiworld, player, 'Nothing'), player)) + multiworld.push_precollected(item) else: for x in range(len(dungeon_items)-1, -1, -1): item = dungeon_items[x] - if ((world.smallkey_shuffle[player] == smallkey_shuffle.option_start_with and item.type == 'SmallKey') - or (world.bigkey_shuffle[player] == bigkey_shuffle.option_start_with and item.type == 'BigKey') - or (world.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass') - or (world.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')): + if ((multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_start_with and item.type == 'SmallKey') + or (multiworld.bigkey_shuffle[player] == bigkey_shuffle.option_start_with and item.type == 'BigKey') + or (multiworld.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass') + or (multiworld.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')): dungeon_items.remove(item) - world.push_precollected(item) - world.itempool.append(ItemFactory(dungeon_item_replacements.pop(), player)) - world.itempool.extend([item for item in dungeon_items]) + multiworld.push_precollected(item) + multiworld.itempool.append(ItemFactory(dungeon_item_replacements.pop(), player)) + multiworld.itempool.extend([item for item in dungeon_items]) # logic has some branches where having 4 hearts is one possible requirement (of several alternatives) # rather than making all hearts/heart pieces progression items (which slows down generation considerably) # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode) - if world.goal[player] != 'icerodhunt' and world.difficulty[player] in ['easy', 'normal', 'hard'] and not (world.custom and world.customitemarray[30] == 0): + if multiworld.goal[player] != 'icerodhunt' and multiworld.difficulty[player] in ['easy', 'normal', 'hard'] and not (multiworld.custom and multiworld.customitemarray[30] == 0): next(item for item in items if item.name == 'Boss Heart Container').classification = ItemClassification.progression - elif world.goal[player] != 'icerodhunt' and world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[29] < 4): + elif multiworld.goal[player] != 'icerodhunt' and multiworld.difficulty[player] in ['expert'] and not (multiworld.custom and multiworld.customitemarray[29] < 4): adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart') for i in range(4): next(adv_heart_pieces).classification = ItemClassification.progression @@ -412,41 +412,41 @@ def generate_itempool(world): if item.advancement or item.type: progressionitems.append(item) else: - nonprogressionitems.append(GetBeemizerItem(world, item.player, item)) - world.random.shuffle(nonprogressionitems) + nonprogressionitems.append(GetBeemizerItem(multiworld, item.player, item)) + multiworld.random.shuffle(nonprogressionitems) if additional_triforce_pieces: if additional_triforce_pieces > len(nonprogressionitems): raise FillError(f"Not enough non-progression items to replace with Triforce pieces found for player " - f"{world.get_player_name(player)}.") + f"{multiworld.get_player_name(player)}.") progressionitems += [ItemFactory("Triforce Piece", player) for _ in range(additional_triforce_pieces)] nonprogressionitems.sort(key=lambda item: int("Heart" in item.name)) # try to keep hearts in the pool nonprogressionitems = nonprogressionitems[additional_triforce_pieces:] - world.random.shuffle(nonprogressionitems) + multiworld.random.shuffle(nonprogressionitems) # shuffle medallions - if world.required_medallions[player][0] == "random": - mm_medallion = world.random.choice(['Ether', 'Quake', 'Bombos']) + if multiworld.required_medallions[player][0] == "random": + mm_medallion = multiworld.random.choice(['Ether', 'Quake', 'Bombos']) else: - mm_medallion = world.required_medallions[player][0] - if world.required_medallions[player][1] == "random": - tr_medallion = world.random.choice(['Ether', 'Quake', 'Bombos']) + mm_medallion = multiworld.required_medallions[player][0] + if multiworld.required_medallions[player][1] == "random": + tr_medallion = multiworld.random.choice(['Ether', 'Quake', 'Bombos']) else: - tr_medallion = world.required_medallions[player][1] - world.required_medallions[player] = (mm_medallion, tr_medallion) + tr_medallion = multiworld.required_medallions[player][1] + multiworld.required_medallions[player] = (mm_medallion, tr_medallion) - place_bosses(world, player) - set_up_shops(world, player) + place_bosses(world) + set_up_shops(multiworld, player) - if world.shop_shuffle[player]: - shuffle_shops(world, nonprogressionitems, player) + if multiworld.shop_shuffle[player]: + shuffle_shops(multiworld, nonprogressionitems, player) - world.itempool += progressionitems + nonprogressionitems + multiworld.itempool += progressionitems + nonprogressionitems - if world.retro_caves[player]: - set_up_take_anys(world, player) # depends on world.itempool to be set + if multiworld.retro_caves[player]: + set_up_take_anys(multiworld, player) # depends on world.itempool to be set # set_up_take_anys needs to run first - create_dynamic_shop_locations(world, player) + create_dynamic_shop_locations(multiworld, player) take_any_locations = { diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 2f5fc0cc07d5..64f678a68a4b 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -279,7 +279,9 @@ def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_rand rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette) -def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_directory): +def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory): + player = world.player + multiworld = world.multiworld check_enemizer(enemizercli) randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{player}.sfc')) options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{player}.json')) @@ -287,18 +289,18 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct # write options file for enemizer options = { - 'RandomizeEnemies': world.enemy_shuffle[player].value, + 'RandomizeEnemies': multiworld.enemy_shuffle[player].value, 'RandomizeEnemiesType': 3, - 'RandomizeBushEnemyChance': world.bush_shuffle[player].value, - 'RandomizeEnemyHealthRange': world.enemy_health[player] != 'default', + 'RandomizeBushEnemyChance': multiworld.bush_shuffle[player].value, + 'RandomizeEnemyHealthRange': multiworld.enemy_health[player] != 'default', 'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[ - world.enemy_health[player]], + multiworld.enemy_health[player]], 'OHKO': False, - 'RandomizeEnemyDamage': world.enemy_damage[player] != 'default', + 'RandomizeEnemyDamage': multiworld.enemy_damage[player] != 'default', 'AllowEnemyZeroDamage': True, - 'ShuffleEnemyDamageGroups': world.enemy_damage[player] != 'default', - 'EnemyDamageChaosMode': world.enemy_damage[player] == 'chaos', - 'EasyModeEscape': world.mode[player] == "standard", + 'ShuffleEnemyDamageGroups': multiworld.enemy_damage[player] != 'default', + 'EnemyDamageChaosMode': multiworld.enemy_damage[player] == 'chaos', + 'EasyModeEscape': multiworld.mode[player] == "standard", 'EnemiesAbsorbable': False, 'AbsorbableSpawnRate': 10, 'AbsorbableTypes': { @@ -327,7 +329,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct 'GrayscaleMode': False, 'GenerateSpoilers': False, 'RandomizeLinkSpritePalette': False, - 'RandomizePots': world.pot_shuffle[player].value, + 'RandomizePots': multiworld.pot_shuffle[player].value, 'ShuffleMusic': False, 'BootlegMagic': True, 'CustomBosses': False, @@ -340,7 +342,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct 'BeesLevel': 0, 'RandomizeTileTrapPattern': False, 'RandomizeTileTrapFloorTile': False, - 'AllowKillableThief': world.killable_thieves[player].value, + 'AllowKillableThief': multiworld.killable_thieves[player].value, 'RandomizeSpriteOnHit': False, 'DebugMode': False, 'DebugForceEnemy': False, @@ -352,26 +354,26 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct 'DebugShowRoomIdInRupeeCounter': False, 'UseManualBosses': True, 'ManualBosses': { - 'EasternPalace': world.get_dungeon("Eastern Palace", player).boss.enemizer_name, - 'DesertPalace': world.get_dungeon("Desert Palace", player).boss.enemizer_name, - 'TowerOfHera': world.get_dungeon("Tower of Hera", player).boss.enemizer_name, + 'EasternPalace': world.dungeons["Eastern Palace"].boss.enemizer_name, + 'DesertPalace': world.dungeons["Desert Palace"].boss.enemizer_name, + 'TowerOfHera': world.dungeons["Tower of Hera"].boss.enemizer_name, 'AgahnimsTower': 'Agahnim', - 'PalaceOfDarkness': world.get_dungeon("Palace of Darkness", player).boss.enemizer_name, - 'SwampPalace': world.get_dungeon("Swamp Palace", player).boss.enemizer_name, - 'SkullWoods': world.get_dungeon("Skull Woods", player).boss.enemizer_name, - 'ThievesTown': world.get_dungeon("Thieves Town", player).boss.enemizer_name, - 'IcePalace': world.get_dungeon("Ice Palace", player).boss.enemizer_name, - 'MiseryMire': world.get_dungeon("Misery Mire", player).boss.enemizer_name, - 'TurtleRock': world.get_dungeon("Turtle Rock", player).boss.enemizer_name, + 'PalaceOfDarkness': world.dungeons["Palace of Darkness"].boss.enemizer_name, + 'SwampPalace': world.dungeons["Swamp Palace"].boss.enemizer_name, + 'SkullWoods': world.dungeons["Skull Woods"].boss.enemizer_name, + 'ThievesTown': world.dungeons["Thieves Town"].boss.enemizer_name, + 'IcePalace': world.dungeons["Ice Palace"].boss.enemizer_name, + 'MiseryMire': world.dungeons["Misery Mire"].boss.enemizer_name, + 'TurtleRock': world.dungeons["Turtle Rock"].boss.enemizer_name, 'GanonsTower1': - world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower', - player).bosses['bottom'].enemizer_name, + world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else + "Inverted Ganons Tower"].bosses['bottom'].enemizer_name, 'GanonsTower2': - world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower', - player).bosses['middle'].enemizer_name, + world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else + "Inverted Ganons Tower"].bosses['middle'].enemizer_name, 'GanonsTower3': - world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower', - player).bosses['top'].enemizer_name, + world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else + "Inverted Ganons Tower"].bosses['top'].enemizer_name, 'GanonsTower4': 'Agahnim2', 'Ganon': 'Ganon', } @@ -384,7 +386,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct max_enemizer_tries = 5 for i in range(max_enemizer_tries): - enemizer_seed = str(world.per_slot_randoms[player].randint(0, 999999999)) + enemizer_seed = str(multiworld.per_slot_randoms[player].randint(0, 999999999)) enemizer_command = [os.path.abspath(enemizercli), '--rom', randopatch_path, '--seed', enemizer_seed, @@ -414,7 +416,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct continue for j in range(i + 1, max_enemizer_tries): - world.per_slot_randoms[player].randint(0, 999999999) + multiworld.per_slot_randoms[player].randint(0, 999999999) # Sacrifice all remaining random numbers that would have been used for unused enemizer tries. # This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness break @@ -422,7 +424,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct rom.read_from_file(enemizer_output_path) os.remove(enemizer_output_path) - if world.get_dungeon("Thieves Town", player).boss.enemizer_name == "Blind": + if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind": rom.write_byte(0x04DE81, 6) rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry. diff --git a/worlds/alttp/SubClasses.py b/worlds/alttp/SubClasses.py index e791b73e75d9..64e4adaec9a2 100644 --- a/worlds/alttp/SubClasses.py +++ b/worlds/alttp/SubClasses.py @@ -1,9 +1,13 @@ """Module extending BaseClasses.py for aLttP""" -from typing import Optional +from typing import Optional, TYPE_CHECKING from enum import IntEnum from BaseClasses import Location, Item, ItemClassification, Region, MultiWorld +if TYPE_CHECKING: + from .Dungeons import Dungeon + from .Regions import LTTPRegion + class ALttPLocation(Location): game: str = "A Link to the Past" @@ -13,6 +17,7 @@ class ALttPLocation(Location): shop_slot: Optional[int] = None """If given as integer, shop_slot is the shop's inventory index.""" shop_slot_disabled: bool = False + parent_region: "LTTPRegion" def __init__(self, player: int, name: str, address: Optional[int] = None, crystal: bool = False, hint_text: Optional[str] = None, parent=None, player_address: Optional[int] = None): @@ -86,6 +91,7 @@ class LTTPRegion(Region): is_dark_world: bool = False shop: Optional = None + dungeon: Optional["Dungeon"] = None def __init__(self, name: str, type_: LTTPRegionType, hint: str, player: int, multiworld: MultiWorld): super().__init__(name, player, multiworld, hint) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 1ea927fb6d10..40788a8b139b 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -6,7 +6,7 @@ import Utils from BaseClasses import Item, CollectionState, Tutorial, MultiWorld -from .Dungeons import create_dungeons +from .Dungeons import create_dungeons, Dungeon from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect, \ indirect_connections, indirect_connections_inverted, indirect_connections_not_inverted from .InvertedRegions import create_inverted_regions, mark_dark_world_regions @@ -223,11 +223,19 @@ class ALTTPWorld(World): if os.path.isabs(Utils.get_options()["generator"]["enemizer_path"]) \ else Utils.local_path(Utils.get_options()["generator"]["enemizer_path"]) + # custom instance vars + dungeon_local_item_names: typing.Set[str] + dungeon_specific_item_names: typing.Set[str] + rom_name_available_event: threading.Event + has_progressive_bows: bool + dungeons: typing.Dict[str, Dungeon] + def __init__(self, *args, **kwargs): self.dungeon_local_item_names = set() self.dungeon_specific_item_names = set() self.rom_name_available_event = threading.Event() self.has_progressive_bows = False + self.dungeons = {} super(ALTTPWorld, self).__init__(*args, **kwargs) @classmethod @@ -290,6 +298,8 @@ def generate_early(self): world.non_local_items[player].value -= item_name_groups['Pendants'] world.non_local_items[player].value -= item_name_groups['Crystals'] + create_dungeons = create_dungeons + def create_regions(self): player = self.player world = self.multiworld @@ -302,7 +312,7 @@ def create_regions(self): else: create_inverted_regions(world, player) create_shops(world, player) - create_dungeons(world, player) + self.create_dungeons() if world.logic[player] not in ["noglitches", "minorglitches"] and world.shuffle[player] in \ {"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}: @@ -468,50 +478,50 @@ def use_enemizer(self) -> bool: or world.killable_thieves[player]) def generate_output(self, output_directory: str): - world = self.multiworld + multiworld = self.multiworld player = self.player try: use_enemizer = self.use_enemizer rom = LocalRom(get_base_rom_path()) - patch_rom(world, rom, player, use_enemizer) + patch_rom(multiworld, rom, player, use_enemizer) if use_enemizer: - patch_enemizer(world, player, rom, self.enemizer_path, output_directory) + patch_enemizer(self, rom, self.enemizer_path, output_directory) - if world.is_race: - patch_race_rom(rom, world, player) + if multiworld.is_race: + patch_race_rom(rom, multiworld, player) - world.spoiler.hashes[player] = get_hash_string(rom.hash) + multiworld.spoiler.hashes[player] = get_hash_string(rom.hash) palettes_options = { - 'dungeon': world.uw_palettes[player], - 'overworld': world.ow_palettes[player], - 'hud': world.hud_palettes[player], - 'sword': world.sword_palettes[player], - 'shield': world.shield_palettes[player], + 'dungeon': multiworld.uw_palettes[player], + 'overworld': multiworld.ow_palettes[player], + 'hud': multiworld.hud_palettes[player], + 'sword': multiworld.sword_palettes[player], + 'shield': multiworld.shield_palettes[player], # 'link': world.link_palettes[player] } palettes_options = {key: option.current_key for key, option in palettes_options.items()} - apply_rom_settings(rom, world.heartbeep[player].current_key, - world.heartcolor[player].current_key, - world.quickswap[player], - world.menuspeed[player].current_key, - world.music[player], - world.sprite[player], + apply_rom_settings(rom, multiworld.heartbeep[player].current_key, + multiworld.heartcolor[player].current_key, + multiworld.quickswap[player], + multiworld.menuspeed[player].current_key, + multiworld.music[player], + multiworld.sprite[player], None, - palettes_options, world, player, True, - reduceflashing=world.reduceflashing[player] or world.is_race, - triforcehud=world.triforcehud[player].current_key, - deathlink=world.death_link[player], - allowcollect=world.allow_collect[player]) + palettes_options, multiworld, player, True, + reduceflashing=multiworld.reduceflashing[player] or multiworld.is_race, + triforcehud=multiworld.triforcehud[player].current_key, + deathlink=multiworld.death_link[player], + allowcollect=multiworld.allow_collect[player]) rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc") rom.write_to_file(rompath) patch = LttPDeltaPatch(os.path.splitext(rompath)[0]+LttPDeltaPatch.patch_file_ending, player=player, - player_name=world.player_name[player], patched_path=rompath) + player_name=multiworld.player_name[player], patched_path=rompath) patch.write() os.unlink(rompath) self.rom_name = rom.name @@ -629,35 +639,34 @@ def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: if self.multiworld.boss_shuffle[self.player] != "none": def create_boss_map() -> typing.Dict: boss_map = { - "Eastern Palace": self.multiworld.get_dungeon("Eastern Palace", self.player).boss.name, - "Desert Palace": self.multiworld.get_dungeon("Desert Palace", self.player).boss.name, - "Tower Of Hera": self.multiworld.get_dungeon("Tower of Hera", self.player).boss.name, + "Eastern Palace": self.dungeons["Eastern Palace"].boss.name, + "Desert Palace": self.dungeons["Desert Palace"].boss.name, + "Tower Of Hera": self.dungeons["Tower of Hera"].boss.name, "Hyrule Castle": "Agahnim", - "Palace Of Darkness": self.multiworld.get_dungeon("Palace of Darkness", - self.player).boss.name, - "Swamp Palace": self.multiworld.get_dungeon("Swamp Palace", self.player).boss.name, - "Skull Woods": self.multiworld.get_dungeon("Skull Woods", self.player).boss.name, - "Thieves Town": self.multiworld.get_dungeon("Thieves Town", self.player).boss.name, - "Ice Palace": self.multiworld.get_dungeon("Ice Palace", self.player).boss.name, - "Misery Mire": self.multiworld.get_dungeon("Misery Mire", self.player).boss.name, - "Turtle Rock": self.multiworld.get_dungeon("Turtle Rock", self.player).boss.name, + "Palace Of Darkness": self.dungeons["Palace of Darkness"].boss.name, + "Swamp Palace": self.dungeons["Swamp Palace"].boss.name, + "Skull Woods": self.dungeons["Skull Woods"].boss.name, + "Thieves Town": self.dungeons["Thieves Town"].boss.name, + "Ice Palace": self.dungeons["Ice Palace"].boss.name, + "Misery Mire": self.dungeons["Misery Mire"].boss.name, + "Turtle Rock": self.dungeons["Turtle Rock"].boss.name, "Ganons Tower": "Agahnim 2", "Ganon": "Ganon" } if self.multiworld.mode[self.player] != 'inverted': boss_map.update({ "Ganons Tower Basement": - self.multiworld.get_dungeon("Ganons Tower", self.player).bosses["bottom"].name, - "Ganons Tower Middle": self.multiworld.get_dungeon("Ganons Tower", self.player).bosses[ + self.dungeons["Ganons Tower"].bosses["bottom"].name, + "Ganons Tower Middle": self.dungeons["Ganons Tower"].bosses[ "middle"].name, - "Ganons Tower Top": self.multiworld.get_dungeon("Ganons Tower", self.player).bosses[ + "Ganons Tower Top": self.dungeons["Ganons Tower"].bosses[ "top"].name }) else: boss_map.update({ - "Ganons Tower Basement": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["bottom"].name, - "Ganons Tower Middle": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["middle"].name, - "Ganons Tower Top": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["top"].name + "Ganons Tower Basement": self.dungeons["Inverted Ganons Tower"].bosses["bottom"].name, + "Ganons Tower Middle": self.dungeons["Inverted Ganons Tower"].bosses["middle"].name, + "Ganons Tower Top": self.dungeons["Inverted Ganons Tower"].bosses["top"].name }) return boss_map @@ -709,11 +718,10 @@ def get_filler_item_name(self) -> str: def get_pre_fill_items(self): res = [] if self.dungeon_local_item_names: - for (name, player), dungeon in self.multiworld.dungeons.items(): - if player == self.player: - for item in dungeon.all_items: - if item.name in self.dungeon_local_item_names: - res.append(item) + 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 diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py index ba1752bf69a1..893e6919c2d0 100644 --- a/worlds/alttp/test/dungeons/TestDungeon.py +++ b/worlds/alttp/test/dungeons/TestDungeon.py @@ -2,13 +2,12 @@ from argparse import Namespace from BaseClasses import MultiWorld, CollectionState, ItemClassification -from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool +from worlds.alttp.Dungeons import get_dungeon_item_pool from worlds.alttp.EntranceShuffle import mandatory_connections, connect_simple -from worlds.alttp.ItemPool import difficulties, generate_itempool +from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import ItemFactory from worlds.alttp.Regions import create_regions from worlds.alttp.Shops import create_shops -from worlds.alttp.Rules import set_rules from worlds import AutoWorld @@ -24,7 +23,7 @@ def setUp(self): self.remove_exits = [] # Block dungeon exits self.multiworld.difficulty_requirements[1] = difficulties['normal'] create_regions(self.multiworld, 1) - create_dungeons(self.multiworld, 1) + self.multiworld.worlds[1].create_dungeons() create_shops(self.multiworld, 1) for exitname, regionname in mandatory_connections: connect_simple(self.multiworld, exitname, regionname, 1) diff --git a/worlds/alttp/test/inverted/TestInverted.py b/worlds/alttp/test/inverted/TestInverted.py index 309a34d54b95..4e33444bb5ab 100644 --- a/worlds/alttp/test/inverted/TestInverted.py +++ b/worlds/alttp/test/inverted/TestInverted.py @@ -23,7 +23,7 @@ def setUp(self): self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.mode[1] = "inverted" create_inverted_regions(self.multiworld, 1) - create_dungeons(self.multiworld, 1) + self.multiworld.worlds[1].create_dungeons() create_shops(self.multiworld, 1) link_inverted_entrances(self.multiworld, 1) self.multiworld.worlds[1].create_items() diff --git a/worlds/alttp/test/inverted/TestInvertedBombRules.py b/worlds/alttp/test/inverted/TestInvertedBombRules.py index 47e177dd586e..0eeaf38564e9 100644 --- a/worlds/alttp/test/inverted/TestInvertedBombRules.py +++ b/worlds/alttp/test/inverted/TestInvertedBombRules.py @@ -23,7 +23,7 @@ def setUp(self): self.multiworld.set_default_common_options() self.multiworld.difficulty_requirements[1] = difficulties['normal'] create_inverted_regions(self.multiworld, 1) - create_dungeons(self.multiworld, 1) + self.multiworld.worlds[1].create_dungeons() #TODO: Just making sure I haven't missed an entrance. It would be good to test the rules make sense as well. def testInvertedBombRulesAreComplete(self): diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py index 7ea7980bbe60..afc683478c2d 100644 --- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py +++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py @@ -25,7 +25,7 @@ def setUp(self): self.multiworld.logic[1] = "minorglitches" self.multiworld.difficulty_requirements[1] = difficulties['normal'] create_inverted_regions(self.multiworld, 1) - create_dungeons(self.multiworld, 1) + self.multiworld.worlds[1].create_dungeons() create_shops(self.multiworld, 1) link_inverted_entrances(self.multiworld, 1) self.multiworld.worlds[1].create_items() diff --git a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py index 7dae3589296e..359a58e4ed2a 100644 --- a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py +++ b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py @@ -26,7 +26,7 @@ def setUp(self): self.multiworld.mode[1] = "inverted" self.multiworld.difficulty_requirements[1] = difficulties['normal'] create_inverted_regions(self.multiworld, 1) - create_dungeons(self.multiworld, 1) + self.multiworld.worlds[1].create_dungeons() create_shops(self.multiworld, 1) link_inverted_entrances(self.multiworld, 1) self.multiworld.worlds[1].create_items() diff --git a/worlds/oot/Hints.py b/worlds/oot/Hints.py index 4627abd00f25..e63e135e5045 100644 --- a/worlds/oot/Hints.py +++ b/worlds/oot/Hints.py @@ -474,7 +474,8 @@ def get_woth_hint(world, checked): locations = world.required_locations locations = list(filter(lambda location: location.name not in checked[location.player] - and not (world.woth_dungeon >= world.hint_dist_user['dungeons_woth_limit'] and location.parent_region.dungeon) + and not (world.woth_dungeon >= world.hint_dist_user['dungeons_woth_limit'] + and getattr(location.parent_region, "dungeon", None)) and location.name not in world.hint_exclusions and location.name not in world.hint_type_overrides['woth'] and location.item.name not in world.item_hint_type_overrides['woth'], @@ -486,7 +487,7 @@ def get_woth_hint(world, checked): location = world.hint_rng.choice(locations) checked[location.player].add(location.name) - if location.parent_region.dungeon: + if getattr(location.parent_region, "dungeon", None): world.woth_dungeon += 1 location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text else: @@ -570,7 +571,7 @@ def get_good_item_hint(world, checked): checked[location.player].add(location.name) item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text - if location.parent_region.dungeon: + if getattr(location.parent_region, "dungeon", None): location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), ['Green', 'Red']), location) @@ -613,8 +614,8 @@ def get_specific_item_hint(world, checked): location = world.hint_rng.choice(locations) checked[location.player].add(location.name) item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text - - if location.parent_region.dungeon: + + if getattr(location.parent_region, "dungeon", None): location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text if world.hint_dist_user.get('vague_named_items', False): return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location)