diff --git a/BaseClasses.py b/BaseClasses.py index ad51f12d..9acbaeaf 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -76,11 +76,12 @@ class World(object): self.dynamic_locations = [] self.spoiler = Spoiler(self) self.lamps_needed_for_dark_rooms = 1 - self.owswaps = {} - self.owwhirlpools = {} self.owedges = [] self._owedge_cache = {} + self.owswaps = {} + self.owwhirlpools = {} self.owflutespots = {} + self.owsectors = {} self.doors = [] self._door_cache = {} self.paired_doors = {} @@ -108,6 +109,7 @@ class World(object): set_player_attr('player_names', []) set_player_attr('owswaps', [[],[],[]]) set_player_attr('owwhirlpools', []) + set_player_attr('owsectors', None) set_player_attr('remote_items', False) set_player_attr('required_medallions', ['Ether', 'Quake']) set_player_attr('bottle_refills', ['Bottle (Green Potion)', 'Bottle (Green Potion)']) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93f9b2f2..e5860f08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### 0.2.3.3 +- Added OW Layout validation that reduces the cases where some screens are unreachable +- Fixed issue with mirror portals showing up in DW in Crossed OW +- Corrected Lost/Skull Woods Pass regions to be more accurate + ### 0.2.3.0/1/2 - Fixed issue in Crossed OW where mirror portal sprites would disappear when changing worlds - Added Big Red Bomb logic to support using residual mirror portals for later re-entry diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 5b3b821d..1017e6d7 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1,7 +1,8 @@ import logging from collections import defaultdict import RaceRandom as random -from BaseClasses import CollectionState, RegionType, Terrain +from BaseClasses import CollectionState, RegionType +from OverworldShuffle import build_accessible_region_list from OWEdges import OWTileRegions entrance_pool = list() @@ -28,7 +29,9 @@ def link_entrances(world, player): Old_Man_House = Old_Man_House_Base.copy() Cave_Three_Exits = Cave_Three_Exits_Base.copy() - sectors = build_sectors(world, player) + from OverworldShuffle import build_sectors + if not world.owsectors[player]: + world.owsectors[player] = build_sectors(world, player) # modifications to lists if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]): @@ -241,10 +244,10 @@ def link_entrances(world, player): if invFlag: # place dark sanc - place_dark_sanc(world, sectors, player) + place_dark_sanc(world, player) # place links house - links_house = place_links_house(world, sectors, player) + links_house = place_links_house(world, player) # place blacksmith, has limited options place_blacksmith(world, links_house, player) @@ -271,10 +274,10 @@ def link_entrances(world, player): # place dark sanc if invFlag: - place_dark_sanc(world, sectors, player) + place_dark_sanc(world, player) # place links house - links_house = place_links_house(world, sectors, player) + links_house = place_links_house(world, player) # place blacksmith, has limited options place_blacksmith(world, links_house, player) @@ -331,10 +334,10 @@ def link_entrances(world, player): # place dark sanc if invFlag: - place_dark_sanc(world, sectors, player, list(zip(*drop_connections + dropexit_connections))[0]) + place_dark_sanc(world, player, list(zip(*drop_connections + dropexit_connections))[0]) # place links house - links_house = place_links_house(world, sectors, player, list(zip(*drop_connections + dropexit_connections))[0]) + links_house = place_links_house(world, player, list(zip(*drop_connections + dropexit_connections))[0]) # determine pools lw_entrances = list() @@ -408,7 +411,7 @@ def link_entrances(world, player): scramble_holes(world, player) # place links house - links_house = place_links_house(world, sectors, player) + links_house = place_links_house(world, player) # place blacksmith, has limited options place_blacksmith(world, links_house, player) @@ -494,7 +497,7 @@ def link_entrances(world, player): scramble_holes(world, player) # place links house - links_house = place_links_house(world, sectors, player) + links_house = place_links_house(world, player) # place blacksmith, has limited options place_blacksmith(world, links_house, player) @@ -542,10 +545,10 @@ def link_entrances(world, player): # place dark sanc if invFlag: - place_dark_sanc(world, sectors, player) + place_dark_sanc(world, player) # place links house - links_house = place_links_house(world, sectors, player) + links_house = place_links_house(world, player) # place blacksmith, has limited options place_blacksmith(world, links_house, player) @@ -616,10 +619,10 @@ def link_entrances(world, player): # place dark sanc if invFlag: - place_dark_sanc(world, sectors, player) + place_dark_sanc(world, player) # place links house - links_house = place_links_house(world, sectors, player) + links_house = place_links_house(world, player) # place blacksmith, place sanc exit first for additional blacksmith candidates doors = list(entrance_pool) @@ -1371,7 +1374,7 @@ def full_shuffle_dungeons(world, Dungeon_Exits, player): connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) -def place_links_house(world, sectors, player, ignore_list=[]): +def place_links_house(world, player, ignore_list=[]): invFlag = world.mode[player] == 'inverted' if world.mode[player] == 'standard' or not world.shufflelinks[player]: links_house = 'Links House' if not invFlag else 'Big Bomb Shop' @@ -1383,9 +1386,9 @@ def place_links_house(world, sectors, player, ignore_list=[]): break if invFlag and isinstance(dark_sanc, str): - links_house_doors = [i for i in get_distant_entrances(world, dark_sanc, sectors, player) if i in entrance_pool] + links_house_doors = [i for i in get_distant_entrances(world, dark_sanc, player) if i in entrance_pool] else: - links_house_doors = [i for i in get_starting_entrances(world, sectors, player, world.shuffle[player] != 'insanity') if i in entrance_pool] + links_house_doors = [i for i in get_starting_entrances(world, player, world.shuffle[player] != 'insanity') if i in entrance_pool] if world.shuffle[player] in ['lite', 'lean']: links_house_doors = [e for e in links_house_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] @@ -1397,11 +1400,11 @@ def place_links_house(world, sectors, player, ignore_list=[]): return links_house -def place_dark_sanc(world, sectors, player, ignore_list=[]): +def place_dark_sanc(world, player, ignore_list=[]): if not world.shufflelinks[player]: - sanc_doors = [i for i in get_distant_entrances(world, 'Big Bomb Shop', sectors, player) if i in entrance_pool] + sanc_doors = [i for i in get_distant_entrances(world, 'Big Bomb Shop', player) if i in entrance_pool] else: - sanc_doors = [i for i in get_starting_entrances(world, sectors, player, world.shuffle[player] != 'insanity') if i in entrance_pool] + sanc_doors = [i for i in get_starting_entrances(world, player, world.shuffle[player] != 'insanity') if i in entrance_pool] if world.shuffle[player] in ['lite', 'lean']: sanc_doors = [e for e in sanc_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] @@ -1634,100 +1637,6 @@ def unbias_dungeons(Dungeon_Exits): tuplize_lists_in_list(Dungeon_Exits) -def build_sectors(world, player): - from Main import copy_world - from OWEdges import OWTileRegions - - # perform accessibility check on duplicate world - for player in range(1, world.players + 1): - world.key_logic[player] = {} - base_world = copy_world(world) - world.key_logic = {} - - # build lists of contiguous regions accessible with full inventory (excl portals/mirror/flute/entrances) - regions = list(OWTileRegions.copy().keys()) - sectors = list() - while(len(regions) > 0): - explored_regions = build_accessible_region_list(base_world, regions[0], player, False, False, False, False) - regions = [r for r in regions if r not in explored_regions] - unique_regions = [_ for i in range(len(sectors)) for _ in sectors[i]] - if (any(r in unique_regions for r in explored_regions)): - for s in range(len(sectors)): - if (any(r in sectors[s] for r in explored_regions)): - sectors[s] = set(list(sectors[s]) + list(explored_regions)) - break - else: - sectors.append(explored_regions) - - # remove water regions if Flippers not in starting inventory - if not any(map(lambda i: i.name == 'Flippers', world.precollected_items)): - for s in range(len(sectors)): - terrains = list() - for regionname in sectors[s]: - region = world.get_region(regionname, player) - if region.terrain == Terrain.Land: - terrains.append(regionname) - sectors[s] = terrains - - # within each group, split into contiguous regions accessible only with starting inventory - for s in range(len(sectors)): - regions = list(sectors[s]).copy() - sectors2 = list() - while(len(regions) > 0): - explored_regions = build_accessible_region_list(base_world, regions[0], player, False, False, True, False) - regions = [r for r in regions if r not in explored_regions] - unique_regions = [_ for i in range(len(sectors2)) for _ in sectors2[i]] - if (any(r in unique_regions for r in explored_regions)): - for s2 in range(len(sectors2)): - if (any(r in sectors2[s2] for r in explored_regions)): - sectors2[s2] = set(list(sectors2[s2]) + list(explored_regions)) - break - else: - sectors2.append(explored_regions) - sectors[s] = sectors2 - - return sectors - - -def build_accessible_region_list(world, start_region, player, build_copy_world=False, cross_world=False, region_rules=True, ignore_ledges = False): - from Main import copy_world - from Items import ItemFactory - - def explore_region(region_name, region=None): - explored_regions.add(region_name) - if not region: - region = base_world.get_region(region_name, player) - for exit in region.exits: - if exit.connected_region is not None: - if any(map(lambda i: i.name == 'Ocarina', base_world.precollected_items)) and exit.spot_type == 'Flute': - fluteregion = exit.connected_region - for flutespot in fluteregion.exits: - if flutespot.connected_region and flutespot.connected_region.name not in explored_regions: - explore_region(flutespot.connected_region.name, flutespot.connected_region) - elif exit.connected_region.name not in explored_regions \ - and (exit.connected_region.type == region.type or (cross_world and exit.connected_region.type in [RegionType.LightWorld, RegionType.DarkWorld])) \ - and (not region_rules or exit.access_rule(blank_state)) and (not ignore_ledges or exit.spot_type != 'Ledge'): - explore_region(exit.connected_region.name, exit.connected_region) - - if build_copy_world: - for player in range(1, world.players + 1): - world.key_logic[player] = {} - base_world = copy_world(world) - base_world.override_bomb_check = True - world.key_logic = {} - else: - base_world = world - - connect_simple(base_world, 'Links House S&Q', start_region, player) - blank_state = CollectionState(base_world) - if base_world.mode[player] == 'standard': - blank_state.collect(ItemFactory('Zelda Delivered', player), True) - explored_regions = set() - explore_region(start_region) - - return explored_regions - - def build_accessible_entrance_list(world, start_region, player, assumed_inventory=[], cross_world=False, region_rules=True, exit_rules=True, include_one_ways=False): from Main import copy_world from Items import ItemFactory @@ -1766,7 +1675,7 @@ def build_accessible_entrance_list(world, start_region, player, assumed_inventor return entrances -def get_starting_entrances(world, sectors, player, force_starting_world=True): +def get_starting_entrances(world, player, force_starting_world=True): invFlag = world.mode[player] == 'inverted' # find largest walkable sector @@ -1775,7 +1684,7 @@ def get_starting_entrances(world, sectors, player, force_starting_world=True): entrances = list() while not len(entrances): while (sector is None): - sector = max(sectors, key=lambda x: len(x) - (0 if x not in invalid_sectors else 1000)) + sector = max(world.owsectors[player], key=lambda x: len(x) - (0 if x not in invalid_sectors else 1000)) if not ((world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] not in ['none', 'polar']) \ and world.get_region(next(iter(next(iter(sector)))), player).type != (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): invalid_sectors.append(sector) @@ -1799,10 +1708,10 @@ def get_starting_entrances(world, sectors, player, force_starting_world=True): return entrances -def get_distant_entrances(world, start_entrance, sectors, player): +def get_distant_entrances(world, start_entrance, player): # get walkable sector in which initial entrance was placed start_region = world.get_entrance(start_entrance, player).parent_region.name - regions = next(s for s in sectors if any(start_region in w for w in s)) + regions = next(s for s in world.owsectors[player] if any(start_region in w for w in s)) regions = next(w for w in regions if start_region in w) # eliminate regions surrounding the initial entrance until less than half of the candidate regions remain diff --git a/OWEdges.py b/OWEdges.py index f13e3b32..fc307d8c 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -326,6 +326,8 @@ def create_owedges(world, player): world.initialize_owedges(edges) def create_owedge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff): + if name not in OWExitTypes['OWEdge']: + OWExitTypes['OWEdge'].append(name) return OWEdge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex) @@ -720,6 +722,7 @@ OWTileRegions = bidict({ 'Lost Woods Pass West Area': 0x10, 'Lost Woods Pass East Top Area': 0x10, + 'Lost Woods Pass Portal Area': 0x10, 'Lost Woods Pass East Bottom Area': 0x10, 'Kakariko Fortune Area': 0x11, @@ -862,6 +865,7 @@ OWTileRegions = bidict({ 'Skull Woods Pass West Area': 0x50, 'Skull Woods Pass East Top Area': 0x50, + 'Skull Woods Pass Portal Area': 0x50, 'Skull Woods Pass East Bottom Area': 0x50, 'Dark Fortune Area': 0x51, @@ -1390,6 +1394,7 @@ parallel_links = bidict({'Lost Woods SW': 'Skull Woods SW', }) OWExitTypes = { + 'OWEdge': [], 'Ledge': ['West Death Mountain Drop', 'Spectacle Rock Drop', 'East Death Mountain Spiral Ledge Drop', @@ -1518,6 +1523,10 @@ OWExitTypes = { 'Zora Waterfall Water Entry', 'Waterfall of Wishing Cave Entry', 'Zora Waterfall Landing', + 'Lost Woods Pass Hammer (North)', + 'Lost Woods Pass Hammer (South)', + 'Lost Woods Pass Rock (North)', + 'Lost Woods Pass Rock (South)', 'Kings Grave Outer Rocks', 'Graveyard Ladder (Bottom)', 'Graveyard Ladder (Top)', @@ -1587,8 +1596,10 @@ OWExitTypes = { 'Bumper Cave Entrance Rock', 'Skull Woods Pass Bush Row (West)', 'Skull Woods Pass Bush Row (East)', - 'Skull Woods Pass Rock (Top)', - 'Skull Woods Pass Rock (Bottom)', + 'Skull Woods Pass Bush (North)', + 'Skull Woods Pass Bush (South)', + 'Skull Woods Pass Rock (North)', + 'Skull Woods Pass Rock (South)', 'Dark Graveyard Bush (South)', 'Dark Graveyard Bush (North)', 'Qirn Jump East Water Drop', @@ -1632,8 +1643,7 @@ OWExitTypes = { 'Portal': ['West Death Mountain Teleporter', 'East Death Mountain Teleporter', 'TR Pegs Teleporter', - 'Kakariko Teleporter (Hammer)', - 'Kakariko Teleporter (Rock)', + 'Kakariko Teleporter', 'Top of Pyramid', 'Top of Pyramid (Inner)', 'East Hyrule Teleporter', @@ -1643,8 +1653,7 @@ OWExitTypes = { 'Dark Death Mountain Teleporter (West)', 'Dark Death Mountain Teleporter (East)', 'Turtle Rock Teleporter', - 'West Dark World Teleporter (Hammer)', - 'West Dark World Teleporter (Rock)', + 'West Dark World Teleporter', 'Post Aga Inverted Teleporter', 'East Dark World Teleporter', 'Misery Mire Teleporter', @@ -1687,6 +1696,7 @@ OWExitTypes = { 'Catfish Mirror Spot', 'Skull Woods Pass West Mirror Spot', 'Skull Woods Pass East Top Mirror Spot', + 'Skull Woods Pass Portal Mirror Spot', 'Skull Woods Pass East Bottom Mirror Spot', 'Outcast Fortune Mirror Spot', 'Outcast Pond Mirror Spot', @@ -1777,6 +1787,7 @@ OWExitTypes = { 'Zora Waterfall Mirror Spot', 'Lost Woods Pass West Mirror Spot', 'Lost Woods Pass East Top Mirror Spot', + 'Lost Woods Pass Portal Mirror Spot', 'Lost Woods Pass East Bottom Mirror Spot', 'Kakariko Fortune Mirror Spot', 'Kakariko Pond Mirror Spot', diff --git a/OverworldShuffle.py b/OverworldShuffle.py index f5729ab3..9a9ce9be 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -3,7 +3,7 @@ from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSl from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel -__version__ = '0.2.3.2-u' +__version__ = '0.2.3.3-u' def link_overworld(world, player): # setup mandatory connections @@ -140,8 +140,8 @@ def link_overworld(world, player): # update spoiler s = list(map(lambda x: ' ' if x not in world.owswaps[player][0] else 'S', [i for i in range(0x40)])) - text_output = tile_swap_spoiler_table.replace('s', '%s') % ( s[0x02], s[0x07], - s[0x00], s[0x03], s[0x05], + text_output = tile_swap_spoiler_table.replace('s', '%s') % ( s[0x02], s[0x07], + s[0x00], s[0x03], s[0x05], s[0x00], s[0x02],s[0x03], s[0x05], s[0x07], s[0x0a], s[0x0f], s[0x0a], s[0x0f], s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17], s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17], @@ -150,10 +150,10 @@ def link_overworld(world, player): s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f], s[0x18], s[0x1b], s[0x1e], s[0x30], s[0x32],s[0x33],s[0x34],s[0x35], s[0x37], s[0x22], s[0x25], s[0x3a],s[0x3b],s[0x3c], s[0x3f], - s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f], - s[0x32],s[0x33],s[0x34], s[0x37], - s[0x30], s[0x35], - s[0x3a],s[0x3b],s[0x3c], s[0x3f]) + s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f], + s[0x32],s[0x33],s[0x34], s[0x37], + s[0x30], s[0x35], + s[0x3a],s[0x3b],s[0x3c], s[0x3f]) world.spoiler.set_map('swaps', text_output, world.owswaps[player][0], player) # apply tile logical connections @@ -280,48 +280,58 @@ def link_overworld(world, player): trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player) groups = reorganize_groups(world, trimmed_groups, player) - if world.mode[player] == 'standard': - random.shuffle(groups[2:]) # keep first 2 groups (Standard) first - else: - random.shuffle(groups) + tries = 10 + valid_layout = False + connected_edge_cache = connected_edges.copy() + while not valid_layout and tries > 0: + connected_edges = connected_edge_cache.copy() - for (forward_edge_sets, back_edge_sets) in groups: - assert len(forward_edge_sets) == len(back_edge_sets) - random.shuffle(forward_edge_sets) - random.shuffle(back_edge_sets) - if len(forward_edge_sets) > 0: - f = 0 - b = 0 - while f < len(forward_edge_sets) and b < len(back_edge_sets): - forward_set = forward_edge_sets[f] - back_set = back_edge_sets[b] - while forward_set[0] in connected_edges: + if world.mode[player] == 'standard': + random.shuffle(groups[2:]) # keep first 2 groups (Standard) first + else: + random.shuffle(groups) + + for (forward_edge_sets, back_edge_sets) in groups: + assert len(forward_edge_sets) == len(back_edge_sets) + random.shuffle(forward_edge_sets) + random.shuffle(back_edge_sets) + if len(forward_edge_sets) > 0: + f = 0 + b = 0 + while f < len(forward_edge_sets) and b < len(back_edge_sets): + forward_set = forward_edge_sets[f] + back_set = back_edge_sets[b] + while forward_set[0] in connected_edges: + f += 1 + if f < len(forward_edge_sets): + forward_set = forward_edge_sets[f] + else: + forward_set = None + break f += 1 - if f < len(forward_edge_sets): - forward_set = forward_edge_sets[f] - else: - forward_set = None - break - f += 1 - while back_set[0] in connected_edges: + while back_set[0] in connected_edges: + b += 1 + if b < len(back_edge_sets): + back_set = back_edge_sets[b] + else: + back_set = None + break b += 1 - if b < len(back_edge_sets): - back_set = back_edge_sets[b] - else: - back_set = None - break - b += 1 - if forward_set is not None and back_set is not None: - assert len(forward_set) == len(back_set) - for (forward_edge, back_edge) in zip(forward_set, back_set): - connect_two_way(world, forward_edge, back_edge, player, connected_edges) - elif forward_set is not None: - logging.getLogger('').warning("Edge '%s' could not find a valid connection" % forward_set[0]) - elif back_set is not None: - logging.getLogger('').warning("Edge '%s' could not find a valid connection" % back_set[0]) - assert len(connected_edges) == len(default_connections) * 2, connected_edges + if forward_set is not None and back_set is not None: + assert len(forward_set) == len(back_set) + for (forward_edge, back_edge) in zip(forward_set, back_set): + connect_two_way(world, forward_edge, back_edge, player, connected_edges) + elif forward_set is not None: + logging.getLogger('').warning("Edge '%s' could not find a valid connection" % forward_set[0]) + elif back_set is not None: + logging.getLogger('').warning("Edge '%s' could not find a valid connection" % back_set[0]) + assert len(connected_edges) == len(default_connections) * 2, connected_edges + + world.owsectors[player] = build_sectors(world, player) + valid_layout = validate_layout(world, player) - # TODO: Reshuffle some areas if impossible to reach, exception if non-dungeon ER enabled or if area is LW with no portal and flute shuffle is enabled + tries -= 1 + assert valid_layout, 'Could not find a valid OW layout' # flute shuffle def connect_flutes(flute_destinations): @@ -820,6 +830,114 @@ def can_reach_smith(world, player): explore_region('Dark Sanctuary Hint') return found +def build_sectors(world, player): + from Main import copy_world + from OWEdges import OWTileRegions + + # perform accessibility check on duplicate world + for player in range(1, world.players + 1): + world.key_logic[player] = {} + base_world = copy_world(world) + world.key_logic = {} + + # build lists of contiguous regions accessible with full inventory (excl portals/mirror/flute/entrances) + regions = list(OWTileRegions.copy().keys()) + sectors = list() + while(len(regions) > 0): + explored_regions = build_accessible_region_list(base_world, regions[0], player, False, False, False, False) + regions = [r for r in regions if r not in explored_regions] + unique_regions = [_ for i in range(len(sectors)) for _ in sectors[i]] + if (any(r in unique_regions for r in explored_regions)): + for s in range(len(sectors)): + if (any(r in sectors[s] for r in explored_regions)): + sectors[s] = set(list(sectors[s]) + list(explored_regions)) + break + else: + sectors.append(explored_regions) + + # remove water regions if Flippers not in starting inventory + if not any(map(lambda i: i.name == 'Flippers', world.precollected_items)): + for s in range(len(sectors)): + terrains = list() + for regionname in sectors[s]: + region = world.get_region(regionname, player) + if region.terrain == Terrain.Land: + terrains.append(regionname) + sectors[s] = terrains + + # within each group, split into contiguous regions accessible only with starting inventory + for s in range(len(sectors)): + regions = list(sectors[s]).copy() + sectors2 = list() + while(len(regions) > 0): + explored_regions = build_accessible_region_list(base_world, regions[0], player, False, False, True, False) + regions = [r for r in regions if r not in explored_regions] + unique_regions = [_ for i in range(len(sectors2)) for _ in sectors2[i]] + if (any(r in unique_regions for r in explored_regions)): + for s2 in range(len(sectors2)): + if (any(r in sectors2[s2] for r in explored_regions)): + sectors2[s2] = set(list(sectors2[s2]) + list(explored_regions)) + break + else: + sectors2.append(explored_regions) + sectors[s] = sectors2 + + return sectors + +def build_accessible_region_list(world, start_region, player, build_copy_world=False, cross_world=False, region_rules=True, ignore_ledges = False): + from Main import copy_world + from BaseClasses import CollectionState + from Items import ItemFactory + + def explore_region(region_name, region=None): + explored_regions.add(region_name) + if not region: + region = base_world.get_region(region_name, player) + for exit in region.exits: + if exit.connected_region is not None: + if any(map(lambda i: i.name == 'Ocarina', base_world.precollected_items)) and exit.spot_type == 'Flute': + fluteregion = exit.connected_region + for flutespot in fluteregion.exits: + if flutespot.connected_region and flutespot.connected_region.name not in explored_regions: + explore_region(flutespot.connected_region.name, flutespot.connected_region) + elif exit.connected_region.name not in explored_regions \ + and (exit.connected_region.type == region.type + or exit.name in OWExitTypes['OWEdge'] or (cross_world and exit.name in OWExitTypes['Portal'])) \ + and (not region_rules or exit.access_rule(blank_state)) and (not ignore_ledges or exit.name not in OWExitTypes['Ledge']): + explore_region(exit.connected_region.name, exit.connected_region) + + if build_copy_world: + for player in range(1, world.players + 1): + world.key_logic[player] = {} + base_world = copy_world(world) + base_world.override_bomb_check = True + world.key_logic = {} + else: + base_world = world + + connect_simple(base_world, 'Links House S&Q', start_region, player) + blank_state = CollectionState(base_world) + if base_world.mode[player] == 'standard': + blank_state.collect(ItemFactory('Zelda Delivered', player), True) + explored_regions = set() + explore_region(start_region) + + return explored_regions + +def validate_layout(world, player): + sectors = [[r for l in s for r in l] for s in world.owsectors[player]] + for sector in sectors: + entrances_present = False + for region_name in sector: + region = world.get_region(region_name, player) + if any(x.spot_type == 'Entrance' for x in region.exits): + entrances_present = True + break + if not entrances_present and not all(r in isolated_regions for r in sector): + return False + + return True + test_connections = [ #('Links House ES', 'Octoballoon WS'), #('Links House NE', 'Lost Woods Pass SW') @@ -860,6 +978,10 @@ mandatory_connections = [# Intra-tile OW Connections ('Zora Waterfall Water Drop', 'Zora Waterfall Water'), #flippers ('Zora Waterfall Water Entry', 'Zora Waterfall Water'), #flippers ('Waterfall of Wishing Cave Entry', 'Waterfall of Wishing Cave'), #flippers + ('Lost Woods Pass Hammer (North)', 'Lost Woods Pass Portal Area'), #hammer + ('Lost Woods Pass Hammer (South)', 'Lost Woods Pass East Top Area'), #hammer + ('Lost Woods Pass Rock (North)', 'Lost Woods Pass East Bottom Area'), #mitts + ('Lost Woods Pass Rock (South)', 'Lost Woods Pass Portal Area'), #mitts ('Bonk Rock Ledge Drop', 'Sanctuary Area'), ('Graveyard Ledge Drop', 'Graveyard Area'), ('Kings Grave Outer Rocks', 'Kings Grave Area'), #mitts @@ -947,8 +1069,10 @@ mandatory_connections = [# Intra-tile OW Connections ('Bumper Cave Entrance Drop', 'Bumper Cave Area'), ('Skull Woods Pass Bush Row (West)', 'Skull Woods Pass East Top Area'), #pearl ('Skull Woods Pass Bush Row (East)', 'Skull Woods Pass West Area'), #pearl - ('Skull Woods Pass Rock (Top)', 'Skull Woods Pass East Bottom Area'), #mitts - ('Skull Woods Pass Rock (Bottom)', 'Skull Woods Pass East Top Area'), #mitts + ('Skull Woods Pass Bush (North)', 'Skull Woods Pass Portal Area'), #pearl + ('Skull Woods Pass Bush (South)', 'Skull Woods Pass East Top Area'), #pearl + ('Skull Woods Pass Rock (North)', 'Skull Woods Pass East Bottom Area'), #mitts + ('Skull Woods Pass Rock (South)', 'Skull Woods Pass Portal Area'), #mitts ('Dark Graveyard Bush (South)', 'Dark Graveyard North'), #pearl ('Dark Graveyard Bush (North)', 'Dark Graveyard Area'), #pearl ('Qirn Jump Water Drop', 'Qirn Jump Water'), #flippers @@ -1158,15 +1282,15 @@ ow_connections = { 0x10: ([ ('Lost Woods Pass West Mirror Spot', 'Lost Woods Pass West Area'), ('Lost Woods Pass East Top Mirror Spot', 'Lost Woods Pass East Top Area'), + ('Lost Woods Pass Portal Mirror Spot', 'Lost Woods Pass Portal Area'), ('Lost Woods Pass East Bottom Mirror Spot', 'Lost Woods Pass East Bottom Area'), - ('Kakariko Teleporter (Hammer)', 'Skull Woods Pass East Top Area'), - ('Kakariko Teleporter (Rock)', 'Skull Woods Pass East Top Area') + ('Kakariko Teleporter', 'Skull Woods Pass Portal Area') ], [ ('Skull Woods Pass West Mirror Spot', 'Skull Woods Pass West Area'), ('Skull Woods Pass East Top Mirror Spot', 'Skull Woods Pass East Top Area'), + ('Skull Woods Pass Portal Mirror Spot', 'Skull Woods Pass Portal Area'), ('Skull Woods Pass East Bottom Mirror Spot', 'Skull Woods Pass East Bottom Area'), - ('West Dark World Teleporter (Hammer)', 'Lost Woods Pass East Top Area'), - ('West Dark World Teleporter (Rock)', 'Lost Woods Pass East Bottom Area') + ('West Dark World Teleporter', 'Lost Woods Pass Portal Area') ]), 0x11: ([ ('Kakariko Fortune Mirror Spot', 'Kakariko Fortune Area') @@ -1570,6 +1694,11 @@ default_connections = [#('Lost Woods NW', 'Master Sword Meadow SC'), ('East Dark Death Mountain EN', 'Turtle Rock WN') ] +isolated_regions = [ + 'Death Mountain Floating Island', + 'Mimic Cave Ledge' +] + flute_data = { #Slot LW Region DW Region OWID VRAM BG Y BG X Link Y Link X Cam Y Cam X Unk1 Unk2 IconY IconX AltY AltX 0x09: (['Lost Woods East Area', 'Skull Woods Forest'], 0x00, 0x1042, 0x022e, 0x0202, 0x0290, 0x0288, 0x029b, 0x028f, 0xfff2, 0x000e, 0x0290, 0x0288, 0x0290, 0x0290), diff --git a/Regions.py b/Regions.py index a5d3309d..b4a48d73 100644 --- a/Regions.py +++ b/Regions.py @@ -34,8 +34,9 @@ def create_regions(world, player): create_lw_region(player, 'Waterfall of Wishing Cave', None, ['Zora Waterfall Water Drop', 'Waterfall of Wishing']), create_lw_region(player, 'Zoras Domain', ['King Zora', 'Zora\'s Ledge'], ['Zoras Domain SW']), create_lw_region(player, 'Lost Woods Pass West Area', None, ['Skull Woods Pass West Mirror Spot', 'Lost Woods Pass NW', 'Lost Woods Pass SW']), - create_lw_region(player, 'Lost Woods Pass East Top Area', None, ['Skull Woods Pass East Top Mirror Spot', 'Kakariko Teleporter (Hammer)', 'Lost Woods Pass NE']), - create_lw_region(player, 'Lost Woods Pass East Bottom Area', None, ['Skull Woods Pass East Bottom Mirror Spot', 'Kakariko Teleporter (Rock)', 'Lost Woods Pass SE']), + create_lw_region(player, 'Lost Woods Pass East Top Area', None, ['Skull Woods Pass East Top Mirror Spot', 'Lost Woods Pass Hammer (North)', 'Lost Woods Pass NE']), + create_lw_region(player, 'Lost Woods Pass Portal Area', None, ['Skull Woods Pass Portal Mirror Spot', 'Kakariko Teleporter', 'Lost Woods Pass Hammer (South)', 'Lost Woods Pass Rock (North)']), + create_lw_region(player, 'Lost Woods Pass East Bottom Area', None, ['Skull Woods Pass East Bottom Mirror Spot', 'Lost Woods Pass Rock (South)', 'Lost Woods Pass SE']), create_lw_region(player, 'Kakariko Fortune Area', None, ['Fortune Teller (Light)', 'Outcast Fortune Mirror Spot', 'Kakariko Fortune NE', 'Kakariko Fortune EN', 'Kakariko Fortune ES', 'Kakariko Fortune SC']), create_lw_region(player, 'Kakariko Pond Area', None, ['Outcast Pond Mirror Spot', 'Kakariko Pond NE', 'Kakariko Pond WN', 'Kakariko Pond WS', 'Kakariko Pond SW', 'Kakariko Pond SE', 'Kakariko Pond EN', 'Kakariko Pond ES', 'Kakariko Pond Whirlpool']), create_lw_region(player, 'Sanctuary Area', None, ['Sanctuary', 'Dark Chapel Mirror Spot', 'Sanctuary WS', 'Sanctuary EC']), @@ -144,8 +145,9 @@ def create_regions(world, player): create_dw_region(player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], ['Bumper Cave Entrance Drop', 'Bumper Cave (Top)', 'Mountain Entry Ledge Mirror Spot']), create_dw_region(player, 'Catfish Area', ['Catfish'], ['Zora Waterfall Mirror Spot', 'Catfish SE']), create_dw_region(player, 'Skull Woods Pass West Area', None, ['Skull Woods Pass Bush Row (West)', 'Lost Woods Pass West Mirror Spot', 'Skull Woods Pass NW', 'Skull Woods Pass SW']), - create_dw_region(player, 'Skull Woods Pass East Top Area', None, ['Skull Woods Pass Bush Row (East)', 'Skull Woods Pass Rock (Top)', 'West Dark World Teleporter (Hammer)', 'West Dark World Teleporter (Rock)', 'Lost Woods Pass East Top Mirror Spot', 'Skull Woods Pass NE']), - create_dw_region(player, 'Skull Woods Pass East Bottom Area', None, ['Skull Woods Pass Rock (Bottom)', 'Lost Woods Pass East Bottom Mirror Spot', 'Skull Woods Pass SE']), + create_dw_region(player, 'Skull Woods Pass East Top Area', None, ['Lost Woods Pass East Top Mirror Spot', 'Skull Woods Pass Bush Row (East)', 'Skull Woods Pass Bush (North)', 'Skull Woods Pass NE']), + create_dw_region(player, 'Skull Woods Pass Portal Area', None, ['Lost Woods Pass Portal Mirror Spot', 'West Dark World Teleporter', 'Skull Woods Pass Bush (South)', 'Skull Woods Pass Rock (North)']), + create_dw_region(player, 'Skull Woods Pass East Bottom Area', None, ['Lost Woods Pass East Bottom Mirror Spot', 'Skull Woods Pass Rock (South)', 'Skull Woods Pass SE']), create_dw_region(player, 'Dark Fortune Area', None, ['Fortune Teller (Dark)', 'Kakariko Fortune Mirror Spot', 'Dark Fortune NE', 'Dark Fortune EN', 'Dark Fortune ES', 'Dark Fortune SC']), create_dw_region(player, 'Outcast Pond Area', None, ['Kakariko Pond Mirror Spot', 'Outcast Pond NE', 'Outcast Pond WN', 'Outcast Pond WS', 'Outcast Pond SW', 'Outcast Pond SE', 'Outcast Pond EN', 'Outcast Pond ES']), create_dw_region(player, 'Dark Chapel Area', None, ['Dark Sanctuary Hint', 'Sanctuary Mirror Spot', 'Bonk Rock Ledge Mirror Spot', 'Dark Chapel WN', 'Dark Chapel WS', 'Dark Chapel EC']), diff --git a/Rom.py b/Rom.py index 30729de4..2fe1bdfa 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '383fbac5f195f82d2b74b62113229fac' +RANDOMIZERBASEHASH = '607ec23701b1099fdeb64a0caff190b6' class JsonRom(object): diff --git a/Rules.py b/Rules.py index 535eb6ec..efadaf6e 100644 --- a/Rules.py +++ b/Rules.py @@ -757,6 +757,10 @@ def default_rules(world, player): set_rule(world.get_entrance('TR Pegs Ledge Leave', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Mountain Entry Entrance Rock (West)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Mountain Entry Entrance Rock (East)', player), lambda state: state.can_lift_rocks(player)) + set_rule(world.get_entrance('Lost Woods Pass Hammer (North)', player), lambda state: state.has('Hammer', player)) + set_rule(world.get_entrance('Lost Woods Pass Hammer (South)', player), lambda state: state.has('Hammer', player)) + set_rule(world.get_entrance('Lost Woods Pass Rock (North)', player), lambda state: state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Lost Woods Pass Rock (South)', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Potion Shop Rock (South)', player), lambda state: state.can_lift_rocks(player)) @@ -779,8 +783,8 @@ def default_rules(world, player): set_rule(world.get_entrance('Skull Woods Bush Rock (West)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Skull Woods Bush Rock (East)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Skull Woods Pass Rock (Top)', player), lambda state: state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Skull Woods Pass Rock (Bottom)', player), lambda state: state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Skull Woods Pass Rock (North)', player), lambda state: state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Skull Woods Pass Rock (South)', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Dark Witch Rock (North)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Dark Witch Rock (South)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Catfish Approach Rocks (West)', player), lambda state: state.can_lift_heavy_rocks(player) or state.has_Boots(player)) @@ -930,15 +934,14 @@ def ow_rules(world, player): if (world.mode[player] == 'inverted') == (0x10 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Lost Woods Pass West Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lost Woods Pass East Top Mirror Spot', player), lambda state: state.has_Mirror(player)) + set_rule(world.get_entrance('Lost Woods Pass Portal Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lost Woods Pass East Bottom Mirror Spot', player), lambda state: state.has_Mirror(player)) - set_rule(world.get_entrance('Kakariko Teleporter (Hammer)', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) # bunny cannot lift bushes - set_rule(world.get_entrance('Kakariko Teleporter (Rock)', player), lambda state: state.can_lift_heavy_rocks(player) and state.has_Pearl(player)) # bunny cannot lift bushes + set_rule(world.get_entrance('Kakariko Teleporter', player), lambda state: state.can_lift_rocks(player)) else: set_rule(world.get_entrance('Skull Woods Pass West Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Skull Woods Pass East Top Mirror Spot', player), lambda state: state.has_Mirror(player)) + set_rule(world.get_entrance('Skull Woods Pass Portal Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Skull Woods Pass East Bottom Mirror Spot', player), lambda state: state.has_Mirror(player)) - set_rule(world.get_entrance('West Dark World Teleporter (Hammer)', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) - set_rule(world.get_entrance('West Dark World Teleporter (Rock)', player), lambda state: state.can_lift_heavy_rocks(player) and state.has_Pearl(player)) # bunny cannot lift bushes if (world.mode[player] == 'inverted') == (0x11 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Kakariko Fortune Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -1237,6 +1240,10 @@ def ow_bunny_rules(world, player): add_bunny_rule(world.get_entrance('TR Pegs Ledge Entry', player), player) add_bunny_rule(world.get_entrance('Mountain Entry Entrance Rock (West)', player), player) add_bunny_rule(world.get_entrance('Mountain Entry Entrance Rock (East)', player), player) + add_bunny_rule(world.get_entrance('Lost Woods Pass Hammer (North)', player), player) + add_bunny_rule(world.get_entrance('Lost Woods Pass Hammer (South)', player), player) + add_bunny_rule(world.get_entrance('Lost Woods Pass Rock (North)', player), player) + add_bunny_rule(world.get_entrance('Lost Woods Pass Rock (South)', player), player) add_bunny_rule(world.get_entrance('Kings Grave Outer Rocks', player), player) add_bunny_rule(world.get_entrance('Kings Grave Inner Rocks', player), player) add_bunny_rule(world.get_entrance('Potion Shop Rock (South)', player), player) @@ -1273,8 +1280,10 @@ def ow_bunny_rules(world, player): add_bunny_rule(world.get_entrance('Bumper Cave Entrance Rock', player), player) add_bunny_rule(world.get_entrance('Skull Woods Pass Bush Row (West)', player), player) add_bunny_rule(world.get_entrance('Skull Woods Pass Bush Row (East)', player), player) - add_bunny_rule(world.get_entrance('Skull Woods Pass Rock (Top)', player), player) - add_bunny_rule(world.get_entrance('Skull Woods Pass Rock (Bottom)', player), player) + add_bunny_rule(world.get_entrance('Skull Woods Pass Bush (North)', player), player) + add_bunny_rule(world.get_entrance('Skull Woods Pass Bush (South)', player), player) + add_bunny_rule(world.get_entrance('Skull Woods Pass Rock (North)', player), player) + add_bunny_rule(world.get_entrance('Skull Woods Pass Rock (South)', player), player) add_bunny_rule(world.get_entrance('Dark Graveyard Bush (South)', player), player) add_bunny_rule(world.get_entrance('Dark Graveyard Bush (North)', player), player) add_bunny_rule(world.get_entrance('Dark Witch Rock (North)', player), player) diff --git a/asm/owrando.asm b/asm/owrando.asm index 0fc9716a..f239b436 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -148,8 +148,12 @@ OWPreserveMirrorSprite: { lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq .vanilla rtl ; if OW Crossed, skip world check and continue + .vanilla - lda $7ef3ca : bne .deleteMirror + lda InvertedMode : beq + + lda $7ef3ca : beq .deleteMirror + rtl + + lda $7ef3ca : bne .deleteMirror rtl .deleteMirror @@ -407,6 +411,14 @@ OWWorldUpdate: ; x = owid of destination screen { lda.l OWTileWorldAssoc,x : cmp.l $7ef3ca : beq .return sta.l $7ef3ca ; change world + + ; moving mirror portal off screen when in DW + cmp #0 : beq + : lda #1 + + cmp.l InvertedMode : bne + + lda $1acf : and #$0f : sta $1acf : bra .playSfx ; bring portal back into position + + lda $1acf : eor #$80 : sta $1acf ; move portal off screen + + .playSfx lda #$38 : sta $012f ; play sfx - #$3b is an alternative ; toggle bunny mode diff --git a/data/base2current.bps b/data/base2current.bps index db61bf4d..022ded6a 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ