LttP: extract Dungeon and Boss from core (#1787)
This commit is contained in:
		| @@ -1,10 +1,29 @@ | ||||
| import logging | ||||
| from typing import Optional, Union, List, Tuple, Callable, Dict | ||||
| from __future__ import annotations | ||||
|  | ||||
| import logging | ||||
| 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 @@ boss_location_table: List[Tuple[str, str]] = [ | ||||
|     ] | ||||
|  | ||||
|  | ||||
| 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 @@ for location in boss_location_table: | ||||
|                                                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)) | ||||
|   | ||||
| @@ -1,28 +1,83 @@ | ||||
| import typing | ||||
| from __future__ import annotations | ||||
|  | ||||
| from BaseClasses import CollectionState, Dungeon | ||||
| import typing | ||||
| from typing import List, Optional | ||||
|  | ||||
| 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 | ||||
|  | ||||
|  | ||||
| def create_dungeons(world, player): | ||||
| 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 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 create_dungeons(world, player): | ||||
|                       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 create_dungeons(world, player): | ||||
|     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 @@ dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], | ||||
|                            'Ice Palace - Prize': [0x155BF], | ||||
|                            'Misery Mire - Prize': [0x155B9], | ||||
|                            'Turtle Rock - Prize': [0x155C7, 0x155A7, 0x155AA, 0x155AB]} | ||||
|  | ||||
|   | ||||
| @@ -226,40 +226,40 @@ for diff in {'easy', 'normal', 'hard', 'expert'}: | ||||
|  | ||||
| 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}") | ||||
|         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 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) | ||||
|     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) | ||||
|         multiworld.push_item(multiworld.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}') | ||||
|     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 | ||||
|         multiworld.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': | ||||
|     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 = { | ||||
|   | ||||
| @@ -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. | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import typing | ||||
|  | ||||
| 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 @@ class ALTTPWorld(World): | ||||
|         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 @@ class ALTTPWorld(World): | ||||
|         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 @@ class ALTTPWorld(World): | ||||
|                     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 @@ class ALTTPWorld(World): | ||||
|         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 @@ class ALTTPWorld(World): | ||||
|     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 | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -2,13 +2,12 @@ import unittest | ||||
| 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 @@ class TestDungeon(unittest.TestCase): | ||||
|         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) | ||||
|   | ||||
| @@ -23,7 +23,7 @@ class TestInverted(TestBase): | ||||
|         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() | ||||
|   | ||||
| @@ -23,7 +23,7 @@ class TestInvertedBombRules(unittest.TestCase): | ||||
|         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): | ||||
|   | ||||
| @@ -25,7 +25,7 @@ class TestInvertedMinor(TestBase): | ||||
|         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() | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class TestInvertedOWG(TestBase): | ||||
|         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() | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Fabian Dill
					Fabian Dill