diff --git a/BaseClasses.py b/BaseClasses.py index 6abb3d12..e74c9ac5 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1658,14 +1658,15 @@ class Entrance(object): 'Missing Smith': ('Frog', True, False, True, True), 'Middle Aged Man': ('Dark Blacksmith Ruins', True, False, True, True), 'Old Man Drop Off': ('Lost Old Man', True, False, False, False), - #'Revealing Light': ('Suspicious Maiden', False, False, False, False) } + 'Revealing Light': ('Suspicious Maiden', False, False, False, False) } if self.name in multi_step_locations: if self not in state.path: world = self.parent_region.world - step_location = world.get_location(multi_step_locations[self.name][0], self.player) - if step_location.can_reach(state) and self.can_reach_thru(state, step_location, multi_step_locations[self.name][1], multi_step_locations[self.name][2], multi_step_locations[self.name][3], multi_step_locations[self.name][4]) and self.access_rule(state): + multi_step_loc = multi_step_locations[self.name] + step_location = world.get_location(multi_step_loc[0], self.player) + if step_location.can_reach(state) and self.can_reach_thru(state, step_location, multi_step_loc[1], multi_step_loc[2], multi_step_loc[3], multi_step_loc[4]) and self.access_rule(state): if not self in state.path: path = state.path.get(step_location.parent_region, (step_location.parent_region.name, None)) item_name = step_location.item.name if step_location.item else 'Pick Up Item' @@ -1729,7 +1730,8 @@ class Entrance(object): # this is checked first as this often the shortest path follower_region = start_region if follower_region.type not in [RegionType.LightWorld, RegionType.DarkWorld]: - follower_region = [i for i in start_region.entrances if i.parent_region.name != 'Menu'][0].parent_region + ent_list = [e for e in start_region.entrances if e.parent_region.type != RegionType.Menu] + follower_region = ent_list[0].parent_region if (follower_region.world.mode[self.player] != 'inverted') == (follower_region.type == RegionType.LightWorld): from OverworldShuffle import get_mirror_edges mirror_map = get_mirror_edges(follower_region.world, follower_region, self.player) @@ -1747,7 +1749,8 @@ class Entrance(object): path = (follower_region.name, (mirror_exit, path)) item_name = step_location.item.name if step_location.item else 'Pick Up Item' if start_region.name != follower_region.name: - path = (start_region.name, (start_region.entrances[0].name, path)) + ent_list = [e for e in start_region.entrances if e.parent_region.type != RegionType.Menu] + path = (start_region.name, (ent_list[0].name, path)) path = (f'{step_location.parent_region.name} Exit', ('Leave Item Area', (item_name, path))) else: path = (item_name, path) @@ -1758,25 +1761,36 @@ class Entrance(object): path = (self.parent_region.name, path) state.path[self] = (self.name, path) - if not found: - # check normal paths - traverse_paths(start_region.entrances[0].parent_region, self.parent_region) + for ent in start_region.entrances: + if not found: + # check normal paths + if ent.parent_region.type != RegionType.Menu and (ent.parent_region.type == start_region.type or \ + ent.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] or not ignore_underworld): + traverse_paths(ent.parent_region, self.parent_region) if not found and allow_save_quit: # check paths involving save and quit exit = self.parent_region.world.get_entrance('Links House S&Q', self.player) traverse_paths(exit.connected_region, self.parent_region, [exit]) + if not found: + world = self.parent_region.world + exit = world.get_entrance('Sanctuary S&Q', self.player) + # only allow save and quit at Sanc if the lobby is guaranteed vanilla + if exit.connected_region != 'Sanctuary' or world.mode[self.player] == 'standard' \ + or world.doorShuffle[self.player] == 'vanilla' or world.intensity[self.player] < 3: + traverse_paths(exit.connected_region, self.parent_region, [exit]) if not found and allow_mirror_reentry and state.has_Mirror(self.player): # check for paths using mirror portal re-entry at location of final destination # this is checked last as this is the most complicated/exhaustive check follower_region = start_region if follower_region.type not in [RegionType.LightWorld, RegionType.DarkWorld]: - follower_region = start_region.entrances[0].parent_region + ent_list = [e for e in start_region.entrances if e.parent_region.type != RegionType.Menu] + follower_region = ent_list[0].parent_region if (follower_region.world.mode[self.player] != 'inverted') == (follower_region.type == RegionType.LightWorld): dest_region = self.parent_region if dest_region.type not in [RegionType.LightWorld, RegionType.DarkWorld]: - dest_region = start_region.entrances[0].parent_region + dest_region = dest_region.entrances[0].parent_region if (dest_region.world.mode[self.player] != 'inverted') != (dest_region.type == RegionType.LightWorld): # loop thru potential places to leave a mirror portal from OverworldShuffle import get_mirror_edges @@ -1829,7 +1843,8 @@ class Entrance(object): self.target = target self.addresses = addresses self.vanilla = vanilla - region.entrances.append(self) + if self not in region.entrances: + region.entrances.append(self) def __str__(self): return str(self.__unicode__()) @@ -3119,9 +3134,9 @@ class Spoiler(object): self.bosses[str(player)] = OrderedDict() self.bosses[str(player)]["Eastern Palace"] = self.world.get_dungeon("Eastern Palace", player).boss.name self.bosses[str(player)]["Desert Palace"] = self.world.get_dungeon("Desert Palace", player).boss.name - self.bosses[str(player)]["Tower Of Hera"] = self.world.get_dungeon("Tower of Hera", player).boss.name + self.bosses[str(player)]["Tower of Hera"] = self.world.get_dungeon("Tower of Hera", player).boss.name self.bosses[str(player)]["Hyrule Castle"] = "Agahnim" - self.bosses[str(player)]["Palace Of Darkness"] = self.world.get_dungeon("Palace of Darkness", player).boss.name + self.bosses[str(player)]["Palace of Darkness"] = self.world.get_dungeon("Palace of Darkness", player).boss.name self.bosses[str(player)]["Swamp Palace"] = self.world.get_dungeon("Swamp Palace", player).boss.name self.bosses[str(player)]["Skull Woods"] = self.world.get_dungeon("Skull Woods", player).boss.name self.bosses[str(player)]["Thieves Town"] = self.world.get_dungeon("Thieves Town", player).boss.name diff --git a/CHANGELOG.md b/CHANGELOG.md index fcf77f70..62ed719d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.5.0.6 +- New improved Pseudoboots behavior +- Fixed issue with missing Blue Potion in Dark Lake Shop in Inverted +- Various generation and logic fixes for HMG/NL +- Fixed error with prize plando +- Fixed error with District ER +- Added TT Maiden pathing to spoiler log +- Corrected and improved some pathing logic + ## 0.5.0.5 - Fixed issue with vanilla HCBK acting like a small key diff --git a/DoorShuffle.py b/DoorShuffle.py index b5a1e1dd..02ce9226 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -3792,6 +3792,7 @@ logical_connections = [ ('Thieves Conveyor Block Path', 'Thieves Conveyor Bridge'), ("Thieves Blind's Cell Door", "Thieves Blind's Cell Interior"), ("Thieves Blind's Cell Exit", "Thieves Blind's Cell"), + ('Revealing Light', 'Revealing Light'), ('Thieves Town Boss', 'Thieves Boss Spoils'), ('Ice Cross Bottom Push Block Left', 'Ice Floor Switch'), diff --git a/Doors.py b/Doors.py index c5519d13..06d8770a 100644 --- a/Doors.py +++ b/Doors.py @@ -727,6 +727,7 @@ def create_doors(world, player): create_door(player, 'Thieves Big Chest Room ES', Intr).dir(Ea, 0x44, Bot, High).small_key().pos(1), create_door(player, 'Thieves Conveyor Block WN', Intr).dir(We, 0x44, Top, High).pos(0), create_door(player, 'Thieves Trap EN', Intr).dir(Ea, 0x44, Left, Top).pos(0), + create_door(player, 'Revealing Light', Lgcl), create_door(player, 'Thieves Town Boss', Lgcl), create_door(player, 'Ice Lobby SE', Nrml).dir(So, 0x0e, Right, High).pos(2).portal(X, 0x00), diff --git a/Dungeons.py b/Dungeons.py index 8d72760b..02734038 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -139,7 +139,8 @@ thieves_regions = [ 'Thieves Attic', 'Thieves Attic Hint', 'Thieves Attic Switch', 'Thieves Cricket Hall Left', 'Thieves Cricket Hall Right', 'Thieves Attic Window', 'Thieves Basement Block', 'Thieves Blocked Entry', 'Thieves Lonely Zazak', "Thieves Blind's Cell", "Thieves Blind's Cell Interior", 'Thieves Conveyor Bridge', - 'Thieves Conveyor Block', 'Thieves Big Chest Room', 'Thieves Trap', 'Thieves Boss Spoils', 'Thieves Town Portal' + 'Thieves Conveyor Block', 'Thieves Big Chest Room', 'Thieves Trap', 'Revealing Light', + 'Thieves Boss Spoils', 'Thieves Town Portal' ] ice_regions = [ diff --git a/ItemList.py b/ItemList.py index 5a6f33e7..35ff2d0a 100644 --- a/ItemList.py +++ b/ItemList.py @@ -520,8 +520,8 @@ def set_up_take_anys(world, player, skip_adjustments=False): world.regions.append(old_man_take_any) world.dynamic_regions.append(old_man_take_any) - reg = regions.pop() - entrance = world.get_region(reg, player).entrances[0] + reg = world.get_region(regions.pop(), player) + entrance = next((e for e in reg.entrances if e.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])) connect_entrance(world, entrance, old_man_take_any, player) entrance.target = 0x58 old_man_take_any.shop = Shop(old_man_take_any, 0x0112, ShopType.TakeAny, 0xE2, True, not world.shopsanity[player], 32) @@ -1669,16 +1669,16 @@ def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_ item_player = player if len(item_parts) < 2 else int(item_parts[1]) item_name = item_parts[0] event_flag = False - if is_dungeon_item(item_name, world, item_player): - item_to_place = next(x for x in dungeon_pool - if x.name == item_name and x.player == item_player) - dungeon_pool.remove(item_to_place) - event_flag = True - elif item_name in prize_set: + if item_name in prize_set: item_player = player # prizes must be for that player item_to_place = ItemFactory(item_name, item_player) prize_pool.remove(item_name) event_flag = True + elif is_dungeon_item(item_name, world, item_player): + item_to_place = next(x for x in dungeon_pool + if x.name == item_name and x.player == item_player) + dungeon_pool.remove(item_to_place) + event_flag = True else: matcher = lambda x: x.name == item_name and x.player == item_player if item_name == 'Bottle': diff --git a/Main.py b/Main.py index c7aecaac..b5250228 100644 --- a/Main.py +++ b/Main.py @@ -28,7 +28,7 @@ from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dunge from Fill import dungeon_tracking from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations, set_prize_drops from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, fill_specific_items, create_farm_locations -from UnderworldGlitchRules import create_hybridmajor_connections, create_hybridmajor_connectors +from UnderworldGlitchRules import create_hybridmajor_connections, get_hybridmajor_connection_entrances from Utils import output_path, parse_player_names from source.item.District import init_districts @@ -176,8 +176,6 @@ def main(args, seed=None, fish=None): world.data_tables[player] = init_data_tables(world, player) place_bosses(world, player) randomize_enemies(world, player) - if world.logic[player] in ('nologic', 'hybridglitches'): - create_hybridmajor_connections(world, player) adjust_locations(world, player) if any(world.potshuffle.values()): @@ -204,8 +202,6 @@ def main(args, seed=None, fish=None): for player in range(1, world.players + 1): link_entrances_new(world, player) - if world.logic[player] in ('nologic', 'hybridglitches'): - create_hybridmajor_connectors(world, player) logger.info(world.fish.translate("cli", "cli", "shuffling.prep")) @@ -227,6 +223,8 @@ def main(args, seed=None, fish=None): create_farm_locations(world, player) for player in range(1, world.players + 1): + if world.logic[player] in ('nologic', 'hybridglitches'): + create_hybridmajor_connections(world, player) generate_itempool(world, player) verify_item_pool_config(world) @@ -372,6 +370,10 @@ def main(args, seed=None, fish=None): if 'debug' in world.spoiler.settings: world.spoiler.extras(output_path(f'{outfilebase}_Spoiler.txt')) + player_logics = set(world.logic.values()) + if len(player_logics) == 1 and 'nologic' in player_logics: + args.skip_playthrough = True + if not args.skip_playthrough: logger.info(world.fish.translate("cli","cli","calc.playthrough")) create_playthrough(world) @@ -624,8 +626,6 @@ def copy_world(world): create_owg_connections(ret, player) create_dynamic_exits(ret, player) create_dungeon_regions(ret, player) - if world.logic[player] in ('nologic', 'hybridglitches'): - create_hybridmajor_connections(ret, player) create_owedges(ret, player) create_shops(ret, player) #create_doors(ret, player) @@ -656,6 +656,11 @@ def copy_world(world): # connect copied world copied_locations = {(loc.name, loc.player): loc for loc in ret.get_locations()} # caches all locations + + # We have to skip these for now. They require both the rest of the entrances _and_ the dungeon portals to be copied first + # We will connect them later + hmg_entrances = get_hybridmajor_connection_entrances() + for region in world.regions: copied_region = ret.get_region(region.name, region.player) copied_region.is_light_world = region.is_light_world @@ -665,6 +670,8 @@ def copy_world(world): for location in copied_region.locations: location.parent_region = copied_region for entrance in region.entrances: + if entrance.name in hmg_entrances: + continue ret.get_entrance(entrance.name, entrance.player).connect(copied_region) for exit in region.exits: if exit.connected_region: @@ -732,7 +739,16 @@ def copy_world(world): categorize_world_regions(ret, player) create_farm_locations(ret, player) if world.logic[player] in ('nologic', 'hybridglitches'): - create_hybridmajor_connectors(ret, player) + create_hybridmajor_connections(ret, player) + + for region in world.regions: + copied_region = ret.get_region(region.name, region.player) + for entrance in region.entrances: + if entrance.name not in hmg_entrances: + continue + ret.get_entrance(entrance.name, entrance.player).connect(copied_region) + + for player in range(1, world.players + 1): set_rules(ret, player) return ret @@ -823,8 +839,6 @@ def copy_world_premature(world, player): create_owg_connections(ret, player) create_dynamic_exits(ret, player) create_dungeon_regions(ret, player) - if world.logic[player] in ('nologic', 'hybridglitches'): - create_hybridmajor_connections(ret, player) create_owedges(ret, player) create_shops(ret, player) create_doors(ret, player) @@ -849,7 +863,9 @@ def copy_world_premature(world, player): for location in copied_region.locations: location.parent_region = copied_region for entrance in region.entrances: - copied_region.entrances.append(ret.get_entrance(entrance.name, entrance.player)) + ent = ret.get_entrance(entrance.name, entrance.player) + if ent not in copied_region.entrances: + copied_region.entrances.append(ent) for exit in region.exits: if exit.connected_region: dest_region = ret.get_region(exit.connected_region.name, region.player) @@ -884,7 +900,7 @@ def copy_world_premature(world, player): connect_portal(portal, ret, player) if world.logic[player] in ('nologic', 'hybridglitches'): - create_hybridmajor_connectors(ret, player) + create_hybridmajor_connections(ret, player) set_rules(ret, player) diff --git a/OverworldGlitchRules.py b/OverworldGlitchRules.py index 4f6c2454..c01a53bc 100644 --- a/OverworldGlitchRules.py +++ b/OverworldGlitchRules.py @@ -2,7 +2,7 @@ Helper functions to deliver entrance/exit/region sets to OWG rules. """ -from BaseClasses import Entrance +from BaseClasses import Entrance, Region from OWEdges import OWTileRegions # Cave regions that superbunny can get through - but only with a sword. @@ -327,10 +327,18 @@ def add_additional_rule(entrance, rule): entrance.access_rule = lambda state: old_rule(state) and rule(state) -def create_no_logic_connections(player, world, connections): +def create_no_logic_connections(player, world, connections, connect_external=False): for entrance, parent_region, target_region, *_ in connections: parent = world.get_region(parent_region, player) - target = world.get_region(target_region, player) + + if isinstance(target_region, Region): + target_region = target_region.name + + if connect_external and target_region.endswith(" Portal"): + target = world.get_portal(target_region[:-7], player).find_portal_entrance().parent_region + else: + target = world.get_region(target_region, player) + connection = Entrance(player, entrance, parent) connection.spot_type = 'OWG' parent.exits.append(connection) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 283d6422..4d5fe3af 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -8,7 +8,7 @@ from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitType from OverworldGlitchRules import create_owg_connections from Utils import bidict -version_number = '0.5.0.5' +version_number = '0.5.0.6' # branch indicator is intentionally different across branches version_branch = '' diff --git a/Regions.py b/Regions.py index 6dd82146..205e05d0 100644 --- a/Regions.py +++ b/Regions.py @@ -718,7 +718,8 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'Thieves Compass Room', 'Thieves\' Town', ['Thieves\' Town - Compass Chest'], ['Thieves Compass Room NW Edge', 'Thieves Compass Room N Edge', 'Thieves Compass Room WS Edge', 'Thieves Compass Room W']), create_dungeon_region(player, 'Thieves Big Chest Nook', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest'], ['Thieves Big Chest Nook ES Edge']), create_dungeon_region(player, 'Thieves Hallway', 'Thieves\' Town', ['Thieves\' Town - Hallway Pot Key'], ['Thieves Hallway SE', 'Thieves Hallway NE', 'Thieves Hallway WN', 'Thieves Hallway WS']), - create_dungeon_region(player, 'Thieves Boss', 'Thieves\' Town', ['Revealing Light'], ['Thieves Boss SE', 'Thieves Town Boss']), + create_dungeon_region(player, 'Thieves Boss', 'Thieves\' Town', None, ['Thieves Boss SE', 'Revealing Light']), + create_dungeon_region(player, 'Revealing Light', 'Thieves\' Town', ['Revealing Light'], ['Thieves Town Boss']), create_dungeon_region(player, 'Thieves Boss Spoils', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize', 'Thieves\' Town - Boss Kill']), create_dungeon_region(player, 'Thieves Pot Alcove Mid', 'Thieves\' Town', None, ['Thieves Pot Alcove Mid ES', 'Thieves Pot Alcove Mid WS']), create_dungeon_region(player, 'Thieves Pot Alcove Bottom', 'Thieves\' Town', None, ['Thieves Pot Alcove Bottom SW']), @@ -1161,7 +1162,7 @@ def create_shops(world, player): world.shops[player] = [] for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram) in shop_table.items(): if world.mode[player] == 'inverted': - if (not world.is_tile_swapped(0x35, player) and region_name == 'Dark Lake Hylia Shop') \ + if (world.is_tile_swapped(0x35, player) and region_name == 'Dark Lake Hylia Shop') \ or (not world.is_tile_swapped(0x35, player) and region_name == 'Lake Hylia Shop'): locked = True inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)] diff --git a/Rom.py b/Rom.py index aa2ca374..a19590e8 100644 --- a/Rom.py +++ b/Rom.py @@ -43,7 +43,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '2d658871687c966f35dd9597a4195256' +RANDOMIZERBASEHASH = 'a51679f0e31ee29f59247545d705992a' class JsonRom(object): diff --git a/Rules.py b/Rules.py index 6a7296ab..896b3c12 100644 --- a/Rules.py +++ b/Rules.py @@ -98,7 +98,8 @@ def set_rules(world, player): # These rules go here because they overwrite/add to some of the above rules if world.logic[player] == 'hybridglitches': - underworld_glitches_rules(world, player) + if not world.is_copied_world: + underworld_glitches_rules(world, player) def mirrorless_path_to_location(world, startName, targetName, player): # If Agahnim is defeated then the courtyard needs to be accessible without using the mirror for the mirror offset glitch. diff --git a/UnderworldGlitchRules.py b/UnderworldGlitchRules.py index e15dad41..330c915a 100644 --- a/UnderworldGlitchRules.py +++ b/UnderworldGlitchRules.py @@ -1,17 +1,19 @@ -from BaseClasses import Entrance +import functools +from BaseClasses import Entrance, DoorType +from DoorShuffle import connect_simple_door import Rules from OverworldGlitchRules import create_no_logic_connections +from Doors import create_door kikiskip_spots = [ ("Kiki Skip", "Spectacle Rock Cave (Bottom)", "Palace of Darkness Portal") ] -mireheraswamp_spots = [ - ("Mire to Hera Clip", "Mire Torches Top", "Hera Portal"), - ("Hera to Swamp Clip", "Mire Torches Top", "Swamp Portal"), -] +mirehera_spots = [("Mire to Hera Clip", "Mire Torches Top", "Hera Portal")] -icepalace_spots = [("Ice Lobby Clip", "Ice Portal", "Ice Bomb Drop")] +heraswamp_spots = [("Hera to Swamp Clip", "Mire Torches Top", "Swamp Portal")] + +icepalace_spots = [("Ice Lobby Clip", "Ice Portal", "Ice Bomb Drop - Top")] thievesdesert_spots = [ ("Thieves to Desert West Clip", "Thieves Attic", "Desert West Portal"), @@ -27,74 +29,46 @@ paradox_spots = [ ("Paradox Front Teleport", "Paradox Cave Front", "Paradox Cave Chest Area") ] - -# We need to make connectors at a separate time from the connections, because of how dungeons are linked to regions -kikiskip_connectors = [ - ("Kiki Skip Connector", "Spectacle Rock Cave (Bottom)", "Palace of Darkness Exit") -] - - -mireheraswamp_connectors = [ - ("Mire to Hera Connector", "Mire Torches Top", "Tower of Hera Exit"), - ("Mire to Swamp Connector", "Mire Torches Top", "Swamp Palace Exit"), -] - - -thievesdesert_connectors = [ - ("Thieves to Desert West Connector", "Thieves Attic", "Desert Palace Exit (West)"), - ( - "Thieves to Desert South Connector", - "Thieves Attic", - "Desert Palace Exit (South)", - ), - ("Thieves to Desert East Connector", "Thieves Attic", "Desert Palace Exit (East)"), -] - -specrock_connectors = [ - ( - "Spec Rock Top Connector", - "Spectacle Rock Cave (Peak)", - "Spectacle Rock Cave Exit (Top)", - ), - ( - "Spec Rock Exit Connector", - "Spectacle Rock Cave (Peak)", - "Spectacle Rock Cave Exit", - ), -] - - # Create connections between dungeons/locations def create_hybridmajor_connections(world, player): + fix_fake_worlds = world.fix_fake_world[player] + for spots in [ kikiskip_spots, - mireheraswamp_spots, + mirehera_spots, + heraswamp_spots, icepalace_spots, thievesdesert_spots, specrock_spots, paradox_spots, ]: - create_no_logic_connections(player, world, spots) + create_no_logic_connections(player, world, spots, connect_external=fix_fake_worlds) + # Add the new Ice path (back of bomb drop to front) to the world and model it properly + clip_door = create_door(player, "Ice Bomb Drop Clip", DoorType.Logical) + world.doors += [clip_door] + world.initialize_doors([clip_door]) -# Turn dungeons into connectors -def create_hybridmajor_connectors(world, player): - for connectors in [ - kikiskip_connectors, - mireheraswamp_connectors, - thievesdesert_connectors, - specrock_connectors, - ]: - new_connectors = [ - ( - connector[0], - connector[1], - world.get_entrance(connector[2], player).connected_region, - ) - for connector in connectors - ] - new_connectors = [c for c in new_connectors if c[2] is not None] - create_no_logic_connections(player, world, new_connectors) + ice_bomb_top_reg = world.get_region("Ice Bomb Drop - Top", player) + ice_bomb_top_reg.exits.append( + Entrance(player, "Ice Bomb Drop Clip", ice_bomb_top_reg) + ) + connect_simple_door(world, "Ice Bomb Drop Clip", "Ice Bomb Drop", player) + +def get_hybridmajor_connection_entrances(): + connections = [] + for connector in ( + kikiskip_spots + + mirehera_spots + + heraswamp_spots + + icepalace_spots + + thievesdesert_spots + + specrock_spots + + paradox_spots + ): + connections.append(connector[0]) + connections.append('Ice Bomb Drop Clip') + return set(connections) # For some entrances, we need to fake having pearl, because we're in fake DW/LW. @@ -110,19 +84,40 @@ def fake_pearl_state(state, player): # Sets the rules on where we can actually go using this clip. # Behavior differs based on what type of ER shuffle we're playing. def dungeon_reentry_rules( - world, player, clip: Entrance, dungeon_region: str, dungeon_exit: str + world, + player, + clip: Entrance, + dungeon_region: str, ): fix_dungeon_exits = world.fix_palaceofdarkness_exit[player] fix_fake_worlds = world.fix_fake_world[player] + all_clips = [ + x[0] + for x in kikiskip_spots + + mirehera_spots + + heraswamp_spots + + icepalace_spots + + thievesdesert_spots + + specrock_spots + + paradox_spots + ] + dungeon_entrance = [ r for r in world.get_region(dungeon_region, player).entrances - if r.name != clip.name + if r.name not in all_clips ][0] - if ( - not fix_dungeon_exits - ): # vanilla, simple, restricted, dungeonssimple; should never have fake worlds fix + + dungeon_exit = [ + r + for r in world.get_region(dungeon_region, player).exits + if r.name not in all_clips + ][0] + + + if not fix_dungeon_exits: + # vanilla, simple, restricted, dungeonssimple; should never have fake worlds fix # Dungeons are only shuffled among themselves. We need to check SW, MM, and AT because they can't be reentered trivially. # entrance doesn't exist until you fire rod it from the other side @@ -173,32 +168,66 @@ def dungeon_reentry_rules( def underworld_glitches_rules(world, player): - # Ice Palace Entrance Clip, needs bombs or cane of somaria to exit bomb drop room - Rules.add_rule( - world.get_entrance("Ice Bomb Drop SE", player), - lambda state: state.can_dash_clip(world.get_region("Ice Lobby", player), player) - and (state.can_use_bombs(player) or state.has("Cane of Somaria", player)), - combine="or", - ) - - # Kiki Skip - kks = world.get_entrance("Kiki Skip", player) - Rules.set_rule(kks, lambda state: state.can_bomb_clip(kks.parent_region, player)) - dungeon_reentry_rules( - world, player, kks, "Palace of Darkness Portal", "Palace of Darkness Exit" - ) - - # Mire -> Hera -> Swamp def mire_clip(state): - return state.can_reach( - "Mire Torches Top", "Region", player - ) and state.can_dash_clip(world.get_region("Mire Torches Top", player), player) - - def hera_clip(state): - return state.can_reach("Hera 4F", "Region", player) and state.can_dash_clip( - world.get_region("Hera 4F", player), player + torches = world.get_region("Mire Torches Top", player) + return state.can_dash_clip(torches, player) or ( + state.can_bomb_clip(torches, player) and state.has_fire_source(player) ) + def hera_clip(state): + hera = world.get_region("Hera 4F", player) + return state.can_bomb_clip(hera, player) or state.can_dash_clip(hera, player) + + # We use these plus functool.partial because lambdas don't work in loops properly. + def bomb_clip(state, region, player): + return state.can_bomb_clip(region, player) + + def dash_clip(state, region, player): + return state.can_dash_clip(region, player) + # Bomb clips + for clip in ( + kikiskip_spots + + icepalace_spots + + thievesdesert_spots + + specrock_spots + ): + region = world.get_region(clip[1], player) + Rules.set_rule( + world.get_entrance(clip[0], player), + functools.partial(bomb_clip, region=region, player=player), + ) + # Dash clips + for clip in icepalace_spots: + region = world.get_region(clip[1], player) + Rules.add_rule( + world.get_entrance(clip[0], player), + functools.partial(dash_clip, region=region, player=player), + combine="or", + ) + + for spot in kikiskip_spots + thievesdesert_spots: + dungeon_reentry_rules( + world, + player, + world.get_entrance(spot[0], player), + spot[2], + ) + + for clip in mirehera_spots: + Rules.set_rule( + world.get_entrance(clip[0], player), + lambda state: mire_clip(state), + ) + + # Need to be able to escape by hitting the switch from the back + Rules.set_rule( + world.get_entrance("Ice Bomb Drop Clip", player), + lambda state: ( + state.can_use_bombs(player) or state.has("Cane of Somaria", player) + ), + ) + + # Allow mire big key to be used in Hera Rules.add_rule( world.get_entrance("Hera Startile Corner NW", player), lambda state: mire_clip(state) and state.has("Big Key (Misery Mire)", player), @@ -209,20 +238,10 @@ def underworld_glitches_rules(world, player): lambda state: mire_clip(state) and state.has("Big Key (Misery Mire)", player), combine="or", ) - - mire_to_hera = world.get_entrance("Mire to Hera Clip", player) - mire_to_swamp = world.get_entrance("Hera to Swamp Clip", player) - Rules.set_rule(mire_to_hera, mire_clip) + # This uses the mire clip because it's always expected to come from mire Rules.set_rule( - mire_to_swamp, lambda state: mire_clip(state) and state.has("Flippers", player) - ) - - # Using the entrances for various ER types. Hera -> Swamp never matters because you can only logically traverse with the mire keys - dungeon_reentry_rules( - world, player, mire_to_hera, "Hera Lobby", "Tower of Hera Exit" - ) - dungeon_reentry_rules( - world, player, mire_to_swamp, "Swamp Lobby", "Swamp Palace Exit" + world.get_entrance("Hera to Swamp Clip", player), + lambda state: mire_clip(state) and state.has("Flippers", player), ) # We need to set _all_ swamp doors to be openable with mire keys, otherwise the small key can't be behind them - 6 keys because of Pots # Flippers required for all of these doors to prevent locks when flooding @@ -246,7 +265,6 @@ def underworld_glitches_rules(world, player): and state.has("Flippers", player), combine="or", ) - # Rules.add_rule(world.get_entrance(door, player), lambda state: mire_clip(state) and state.has('Flippers', player), combine="or") Rules.add_rule( world.get_location("Trench 1 Switch", player), @@ -272,16 +290,16 @@ def underworld_glitches_rules(world, player): ) ), } - inverted = world.mode[player] == "inverted" + inverted_dm = (world.mode[player] == "inverted") != world.is_tile_swapped(0x03, player) def hera_rule(state): - return (state.has("Moon Pearl", player) or not inverted) and rule_map.get( + return (state.has("Moon Pearl", player) or not inverted_dm) and rule_map.get( world.get_entrance("Tower of Hera", player).connected_region.name, lambda state: False, )(state) def gt_rule(state): - return (state.has("Moon Pearl", player) or inverted) and rule_map.get( + return (state.has("Moon Pearl", player) or inverted_dm) and rule_map.get( world.get_entrance(("Ganons Tower"), player).connected_region.name, lambda state: False, )(state) @@ -299,21 +317,16 @@ def underworld_glitches_rules(world, player): lambda state: mirrorless_moat_rule(state), combine="or", ) + desert_exits = ["West", "South", "East"] - for desert_exit in ["East", "South", "West"]: + for desert_exit in desert_exits: Rules.add_rule( - world.get_entrance(f"Thieves to Desert {desert_exit} Connector", player), + world.get_entrance(f"Thieves to Desert {desert_exit} Clip", player), lambda state: state.can_dash_clip( world.get_region("Thieves Attic", player), player ), ) - dungeon_reentry_rules( - world, - player, - world.get_entrance(f"Thieves to Desert {desert_exit} Connector", player), - f"Desert {desert_exit} Portal", - f"Desert Palace Exit ({desert_exit})", - ) + # Collecting left chests in Paradox Cave using a dash clip -> dash citrus, 1f right, teleport up paradox_left_chests = [ diff --git a/data/base2current.bps b/data/base2current.bps index 617699b7..680045bc 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/source/enemizer/enemy_deny.yaml b/source/enemizer/enemy_deny.yaml index 301abda2..bf916dc3 100644 --- a/source/enemizer/enemy_deny.yaml +++ b/source/enemizer/enemy_deny.yaml @@ -21,7 +21,7 @@ UwGeneralDeny: - [ 0x0016, 1, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper", "GreenMimic", "RedMimic", "Pikit" ] ] #"Swamp Palace - Pool - Zol 2" - [ 0x0016, 2, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper", "GreenMimic", "RedMimic", "Pikit" ] ] #"Swamp Palace - Pool - Blue Bari" - [ 0x0016, 3, [ "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Swamp Palace - Pool - Zol 3" - - [ 0x0017, 5, [ "Beamos", "AntiFairyCircle", "SpikeBlock", "Bumper" ] ] #"Tower Of Hera - Bumper Room - Fire Bar (Clockwise)" + - [ 0x0017, 5, [ "Beamos", "AntiFairyCircle", "SpikeBlock", "Bumper" ] ] #"Tower of Hera - Bumper Room - Fire Bar (Clockwise)" - [ 0x0019, 0, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Palace of Darkness - Dark Maze - Kodongo 1" - [ 0x0019, 1, [ "RollerVerticalUp" ] ] #"Palace of Darkness - Dark Maze - Kodongo 2" - [ 0x0019, 2, [ "RollerVerticalDown", "RollerVerticalUp" ] ] #"Palace of Darkness - Dark Maze - Kodongo 3" @@ -46,11 +46,11 @@ UwGeneralDeny: - [ 0x0026, 9, [ "RollerHorizontalRight", "Statue" ] ] #"Swamp Palace - Big Spoon - Kyameron" - [ 0x0026, 10, [ "Statue" ] ] # multiple push statues in this room can cause issues - [ 0x0026, 11, [ "Statue" ] ] # multiple push statues in this room can cause issues - - [ 0x0027, 0, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalLeft", "FirebarCW" ] ] #"Tower Of Hera - Petting Zoo - Mini Moldorm 1" - - [ 0x0027, 1, [ "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "AntiFairyCircle", "SpikeBlock", "Bumper" ] ] #"Tower Of Hera - Petting Zoo - Mini Moldorm 2" - - [ 0x0027, 2, [ "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "AntiFairyCircle", "SpikeBlock", "Bumper" ] ] #"Tower Of Hera - Petting Zoo - Mini Moldorm 3" + - [ 0x0027, 0, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalLeft", "FirebarCW" ] ] #"Tower of Hera - Petting Zoo - Mini Moldorm 1" + - [ 0x0027, 1, [ "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "AntiFairyCircle", "SpikeBlock", "Bumper" ] ] #"Tower of Hera - Petting Zoo - Mini Moldorm 2" + - [ 0x0027, 2, [ "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "AntiFairyCircle", "SpikeBlock", "Bumper" ] ] #"Tower of Hera - Petting Zoo - Mini Moldorm 3" - [ 0x0027, 4, ["Bumper", "BigSpike", "AntiFairyCircle", "RollerVerticalDown", "RollerVerticalUp"]] - - [ 0x0027, 5, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Tower Of Hera - Petting Zoo - Kodongo 1" + - [ 0x0027, 5, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Tower of Hera - Petting Zoo - Kodongo 1" - [0x0028, 0, ["Raven", "Poe", "GreenZirro", "BlueZirro", "Swamola", "Zora"]] - [0x0028, 1, ["Raven", "Poe", "GreenZirro", "BlueZirro", "Swamola", "Zora"]] - [0x0028, 2, ["Raven", "Poe", "GreenZirro", "BlueZirro", "Swamola", "Zora"]] diff --git a/source/item/District.py b/source/item/District.py index baf46a82..1ad4fba6 100644 --- a/source/item/District.py +++ b/source/item/District.py @@ -111,7 +111,7 @@ def resolve_districts(world): visited = set() while len(queue) > 0: region = queue.pop() - if not region: + if region is None: RuntimeError(f'No region connected to entrance: {ent.name} Likely a missing entry in OWExitTypes') visited.add(region) if region.type == RegionType.Cave: diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 340430ac..994f5f50 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -907,7 +907,7 @@ def figure_out_connectors(exits, avail, cross_world=True): cave_list = list(Connector_List) if avail.assumed_loose_caves or (not avail.skull_handled and (cross_world or not avail.world.is_tile_swapped(0x00, avail.player))): skull_connector = [x for x in ['Skull Woods Second Section Exit (West)', 'Skull Woods Second Section Exit (East)'] if x in exits] - cave_list.extend(skull_connector) + cave_list.extend([skull_connector]) if avail.assumed_loose_caves or not avail.keep_drops_together: cave_list.extend([[entrance_map[e]] for e in linked_drop_map.values() if 'Inverted ' not in e and 'Skull Woods ' not in e]) @@ -1631,7 +1631,8 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): cave_entrances = [] for cave_exit in rnd_cave[:-1]: if avail.swapped and cave_exit not in avail.exits: - entrance = avail.world.get_entrance(cave_exit, avail.player).parent_region.entrances[0].name + entrance = avail.world.get_entrance(cave_exit, avail.player) + entrance = next((e for e in entrance.parent_region.entrances if e.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])).name cave_entrances.append(entrance) else: entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] and e not in must_exit @@ -1917,8 +1918,8 @@ def connect_exit(exit_name, entrancename, avail): exit.connected_region.entrances.remove(exit) dest_region = entrance.parent_region - if dest_region.name == 'Pyramid Crack': - # Needs to logically exit into greater Pyramid Area + if dest_region.name in ['Pyramid Crack', 'GT Stairs']: + # Needs to logically exit into greater OW area dest_region = entrance.parent_region.entrances[0].parent_region exit.connect(dest_region, door_addresses[entrance.name][1], exit_ids[exit.name][1]) diff --git a/test/suite/hmg/flippers_wraps.yaml b/test/suite/hmg/flippers_wraps.yaml index 3418b4ec..9971a38e 100644 --- a/test/suite/hmg/flippers_wraps.yaml +++ b/test/suite/hmg/flippers_wraps.yaml @@ -19,8 +19,6 @@ advanced_placements: locations: Link's House: True Magic Bat: False -advanced_placements: - 1: - type: Verification item: Progressive Glove locations: diff --git a/test/suite/hmg/inverted_inaccessible_desert.yaml b/test/suite/hmg/inverted_inaccessible_desert.yaml index bb8ad8aa..2a55777d 100644 --- a/test/suite/hmg/inverted_inaccessible_desert.yaml +++ b/test/suite/hmg/inverted_inaccessible_desert.yaml @@ -5,7 +5,6 @@ settings: logic: hybridglitches mode: inverted shuffle: crossed - start_inventory: 1: - Pegasus Boots @@ -15,6 +14,8 @@ start_inventory: placements: 1: Desert Palace - Boss: Moon Pearl + Desert Palace - Prize: Green Pendant + Sahasrahla: Magic Mirror entrances: 1: two-way: @@ -23,4 +24,6 @@ entrances: Thieves Town: Thieves Town Exit Hyrule Castle Entrance (East): Desert Palace Exit (South) Hyrule Castle Entrance (West): Desert Palace Exit (North) + entrances: + Agahnims Tower: Pyramid Fairy diff --git a/test/suite/hmg/inverted_moon_pearl_locs.yaml b/test/suite/hmg/inverted_moon_pearl_locs.yaml index 9ae1a9e1..23ec40fe 100644 --- a/test/suite/hmg/inverted_moon_pearl_locs.yaml +++ b/test/suite/hmg/inverted_moon_pearl_locs.yaml @@ -15,6 +15,9 @@ start_inventory: - Ether - Quake - Bombos + - Big Key (Tower of Hera) + - Big Key (Desert Palace) + - Big Key (Eastern Palace) advanced_placements: 1: - type: Verification diff --git a/test/suite/hmg/moon_pearl_locs.yaml b/test/suite/hmg/moon_pearl_locs.yaml index a18d53d1..b2d418b1 100644 --- a/test/suite/hmg/moon_pearl_locs.yaml +++ b/test/suite/hmg/moon_pearl_locs.yaml @@ -3,6 +3,7 @@ meta: settings: 1: logic: hybridglitches + shuffle: vanilla start_inventory: 1: - Pegasus Boots @@ -10,9 +11,8 @@ start_inventory: - Fire Rod - Book of Mudora - Progressive Sword - - Progressive Sword - Lamp - - Magic Mirror + - Hammer - Ether - Quake - Bombos @@ -41,6 +41,7 @@ advanced_placements: C-Shaped House: True Pyramid Fairy - Left: True Swamp Palace - Entrance: False + Swamp Palace - Boss: False Thieves' Town - Map Chest: False diff --git a/test/suite/hmg/no_east_dw_from_kikiskip.yaml b/test/suite/hmg/no_east_dw_from_kikiskip.yaml new file mode 100644 index 00000000..b709c49f --- /dev/null +++ b/test/suite/hmg/no_east_dw_from_kikiskip.yaml @@ -0,0 +1,49 @@ +meta: + players: 1 +settings: + 1: + logic: hybridglitches + shuffle: vanilla +start_inventory: + 1: + - Pegasus Boots + - Flippers + - Fire Rod + - Book of Mudora + - Progressive Sword + - Lamp + - Hammer + - Ether + - Quake + - Bombos + - Bombs (10) +placements: + 1: + # Put mirror in PF so we can't DMD + Pyramid Fairy - Right: Magic Mirror + # Lock all swords and cape behind pearl so we can't do aga 1 for pyramid access + Thieves' Town - Big Chest: Progressive Sword + Thieves' Town - Attic: Progressive Sword + Thieves' Town - Blind's Cell: Progressive Sword + Thieves' Town - Map Chest: Cape +advanced_placements: + 1: + - type: Verification + item: Moon Pearl + locations: + Palace of Darkness - Shooter Room: True + Palace of Darkness - The Arena - Bridge: True + Palace of Darkness - Stalfos Basement: True + Palace of Darkness - Big Key Chest: True + Palace of Darkness - The Arena - Ledge: True + Palace of Darkness - Map Chest: True + Palace of Darkness - Compass Chest: True + Palace of Darkness - Dark Basement - Left: True + Palace of Darkness - Dark Basement - Right: True + Palace of Darkness - Dark Maze - Top: True + Palace of Darkness - Dark Maze - Bottom: True + Palace of Darkness - Big Chest: True + Palace of Darkness - Harmless Hellway: True + Palace of Darkness - Boss: True + Pyramid Fairy - Left: False + Pyramid: False diff --git a/test/suite/hmg/pearlless_sw.yaml b/test/suite/hmg/pearlless_sw.yaml index c26ce1d9..3b097028 100644 --- a/test/suite/hmg/pearlless_sw.yaml +++ b/test/suite/hmg/pearlless_sw.yaml @@ -9,6 +9,7 @@ start_inventory: - Flippers - Pegasus Boots - Progressive Sword + - Progressive Sword - Hammer - Progressive Glove - Progressive Glove @@ -17,9 +18,16 @@ start_inventory: - Bottle - Magic Mirror - Lamp + - Beat Agahnim 1 + - Cane of Somaria + - Hookshot + - Quake + - Ether + - Bombos advanced_placements: 1: - type: Verification item: Moon Pearl locations: Skull Woods - Bridge Room: True + Skull Woods - Boss: True diff --git a/test/suite/hmg/pod_mp.yaml b/test/suite/hmg/pod_mp.yaml new file mode 100644 index 00000000..16c0c8b5 --- /dev/null +++ b/test/suite/hmg/pod_mp.yaml @@ -0,0 +1,13 @@ +meta: + players: 1 +settings: + 1: + logic: hybridglitches + mode: open + shuffle: vanilla +start_inventory: + 1: + - Pegasus Boots +placements: + 1: + Palace of Darkness - Shooter Room: Moon Pearl \ No newline at end of file