diff --git a/Main.py b/Main.py index 88a3c483..75c3a211 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, get_hybridmajor_connector_entrances +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) @@ -628,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) @@ -660,7 +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 - hmg_entrances = get_hybridmajor_connector_entrances() + + # 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 @@ -739,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 @@ -830,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) @@ -891,7 +898,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/UnderworldGlitchRules.py b/UnderworldGlitchRules.py index c645b719..330c915a 100644 --- a/UnderworldGlitchRules.py +++ b/UnderworldGlitchRules.py @@ -1,6 +1,9 @@ -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") @@ -26,44 +29,10 @@ 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") -] - - -mirehera_connectors = [ - ("Mire to Hera Connector", "Mire Torches Top", "Tower of Hera Exit") -] -heraswamp_connectors = [ - ("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, mirehera_spots, @@ -73,41 +42,33 @@ def create_hybridmajor_connections(world, player): 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, - mirehera_connectors, - heraswamp_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_connector_entrances(): - connectors = [] +def get_hybridmajor_connection_entrances(): + connections = [] for connector in ( - kikiskip_connectors - + mirehera_connectors - + heraswamp_connectors - + thievesdesert_connectors - + specrock_connectors - ): - connectors.append(connector[0]) - return set(connectors) + 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. @@ -123,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 @@ -186,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), @@ -222,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 @@ -259,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), @@ -312,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/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