diff --git a/BaseClasses.py b/BaseClasses.py index 0f5e5e6b..4c103b4d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -813,6 +813,9 @@ class CollectionState(object): rules.append(self.has('Moon Pearl', player)) return all(rules) + def can_bomb_clip(self, region: Region, player: int) -> bool: + return self.is_not_bunny(region, player) and self.has('Pegasus Boots', player) + # Minecraft logic functions def has_iron_ingots(self, player: int): return self.has('Progressive Tools', player) and self.has('Ingot Crafting', player) diff --git a/Mystery.py b/Mystery.py index a80872b8..f250740c 100644 --- a/Mystery.py +++ b/Mystery.py @@ -582,11 +582,11 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): glitches_required = get_choice('glitches_required', weights) - if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']: - logging.warning("Only NMG, OWG and No Logic supported") + if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']: + logging.warning("Only NMG, OWG, HMG and No Logic supported") glitches_required = 'none' ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches', - 'minor_glitches': 'minorglitches'}[ + 'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[ glitches_required] ret.dark_room_logic = get_choice("dark_room_logic", weights, "lamp") diff --git a/Options.py b/Options.py index 3795b817..9582a3ea 100644 --- a/Options.py +++ b/Options.py @@ -146,8 +146,10 @@ class Logic(Choice): option_no_glitches = 0 option_minor_glitches = 1 option_overworld_glitches = 2 + option_hybrid_major_glitches = 3 option_no_logic = 4 alias_owg = 2 + alias_hmg = 3 class Objective(Choice): diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index f26c59ab..64d45d17 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -1,6 +1,6 @@ # ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave. from collections import defaultdict - +from worlds.alttp.UnderworldGlitchRules import underworld_glitch_connections def link_entrances(world, player): connect_two_way(world, 'Links House', 'Links House Exit', player) # unshuffled. For now @@ -1066,6 +1066,10 @@ def link_entrances(world, player): raise NotImplementedError( f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_names(player)}') + # mandatory hybrid major glitches connections + if world.logic[player] in ['hybridglitches', 'nologic']: + underworld_glitch_connections(world, player) + # check for swamp palace fix if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': world.swamp_patch_required[player] = True @@ -1767,6 +1771,10 @@ def link_inverted_entrances(world, player): else: raise NotImplementedError('Shuffling not supported yet') + # mandatory hybrid major glitches connections + if world.logic[player] in ['hybridglitches', 'nologic']: + underworld_glitch_connections(world, player) + # patch swamp drain if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': world.swamp_patch_required[player] = True diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index d03b686b..a42598d1 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -571,7 +571,7 @@ def get_pool_core(world, player: int): return world.random.choice([True, False]) if progressive == 'random' else progressive == 'on' # provide boots to major glitch dependent seeds - if logic in {'owglitches', 'nologic'} and world.glitch_boots[player] and goal != 'icerodhunt': + if logic in {'owglitches', 'hybridglitches', 'nologic'} and world.glitch_boots[player] and goal != 'icerodhunt': precollected_items.append('Pegasus Boots') pool.remove('Pegasus Boots') pool.append('Rupees (20)') diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index faf37308..6adb0f58 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -4,6 +4,7 @@ from worlds.alttp import OverworldGlitchRules from BaseClasses import RegionType, MultiWorld, Entrance from worlds.alttp.Items import ItemFactory, progression_items, item_name_groups from worlds.alttp.OverworldGlitchRules import overworld_glitches_rules, no_logic_rules +from worlds.alttp.UnderworldGlitchRules import underworld_glitches_rules from worlds.alttp.Bosses import GanonDefeatRule from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \ item_name @@ -47,12 +48,17 @@ def set_rules(world, player): if world.logic[player] == 'noglitches': no_glitches_rules(world, player) - elif world.logic[player] in ['owglitches', 'nologic']: + elif world.logic[player] == 'owglitches': # Initially setting no_glitches_rules to set the baseline rules for some # entrances. The overworld_glitches_rules set is primarily additive. no_glitches_rules(world, player) fake_flipper_rules(world, player) overworld_glitches_rules(world, player) + elif world.logic[player] in ['hybridglitches', 'nologic']: + no_glitches_rules(world, player) + fake_flipper_rules(world, player) + overworld_glitches_rules(world, player) + underworld_glitches_rules(world, player) elif world.logic[player] == 'minorglitches': no_glitches_rules(world, player) fake_flipper_rules(world, player) @@ -75,7 +81,8 @@ def set_rules(world, player): set_inverted_big_bomb_rules(world, player) # if swamp and dam have not been moved we require mirror for swamp palace - if not world.swamp_patch_required[player]: + # however there is mirrorless swamp in hybrid MG, so we don't necessarily want this. HMG handles this requirement itself. + if not world.swamp_patch_required[player] and world.logic[player] not in ['hybridglitches', 'nologic']: add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player)) # GT Entrance may be required for Turtle Rock for OWG and < 7 required diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py new file mode 100644 index 00000000..88c7474c --- /dev/null +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -0,0 +1,114 @@ + +from BaseClasses import Entrance +from worlds.generic.Rules import set_rule, add_rule + +# We actually need the logic to properly "mark" these regions as Light or Dark world. +# Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules. +def underworld_glitch_connections(world, player): + specrock = world.get_region('Spectacle Rock Cave (Bottom)', player) + mire = world.get_region('Misery Mire (West)', player) + + kikiskip = Entrance(player, 'Kiki Skip', specrock) + mire_to_hera = Entrance(player, 'Mire to Hera Clip', mire) + mire_to_swamp = Entrance(player, 'Hera to Swamp Clip', mire) + specrock.exits.append(kikiskip) + mire.exits.extend([mire_to_hera, mire_to_swamp]) + + if world.fix_fake_world[player]: + kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region) + mire_to_hera.connect(world.get_entrance('Tower of Hera Exit', player).connected_region) + mire_to_swamp.connect(world.get_entrance('Swamp Palace Exit', player).connected_region) + else: + kikiskip.connect(world.get_region('Palace of Darkness (Entrance)', player)) + mire_to_hera.connect(world.get_region('Tower of Hera (Bottom)', player)) + mire_to_swamp.connect(world.get_region('Swamp Palace (Entrance)', player)) + + +# For some entrances, we need to fake having pearl, because we're in fake DW/LW. +# This creates a copy of the input state that has Moon Pearl. +def fake_pearl_state(state, player): + if state.has('Moon Pearl', player): + return state + fake_state = state.copy() + fake_state.prog_items['Moon Pearl', player] += 1 + return fake_state + + +# 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): + fix_dungeon_exits = world.fix_palaceofdarkness_exit[player] + fix_fake_worlds = world.fix_fake_world[player] + + dungeon_entrance = [r for r in world.get_region(dungeon_region, player).entrances if r.name != clip.name][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. + if dungeon_entrance.name == 'Skull Woods Final Section': + set_rule(clip, lambda state: False) # entrance doesn't exist until you fire rod it from the other side + elif dungeon_entrance.name == 'Misery Mire': + add_rule(clip, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) # open the dungeon + elif dungeon_entrance.name == 'Agahnims Tower': + add_rule(clip, lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier + # Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally. + add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state)) + elif not fix_fake_worlds: # full, dungeonsfull; fixed dungeon exits, but no fake worlds fix + # Entry requires the entrance's requirements plus a fake pearl, but you don't gain logical access to the surrounding region. + add_rule(clip, lambda state: dungeon_entrance.access_rule(fake_pearl_state(state, player))) + # exiting restriction + add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state)) + # Otherwise, the shuffle type is crossed, dungeonscrossed, or insanity; all of these do not need additional rules on where we can go, + # since the clip links directly to the exterior region. + + +def underworld_glitches_rules(world, player): + fix_dungeon_exits = world.fix_palaceofdarkness_exit[player] + fix_fake_worlds = world.fix_fake_world[player] + + # Ice Palace Entrance Clip + # This is the easiest one since it's a simple internal clip. Just need to also add melting to freezor chest since it's otherwise assumed. + add_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.can_bomb_clip(world.get_region('Ice Palace (Entrance)', player), player), combine='or') + add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: state.can_melt_things(player)) + + + # Kiki Skip + kikiskip = world.get_entrance('Kiki Skip', player) + set_rule(kikiskip, lambda state: state.can_bomb_clip(kikiskip.parent_region, player)) + dungeon_reentry_rules(world, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit') + + + # Mire -> Hera -> Swamp + # Using mire keys on other dungeon doors + mire = world.get_region('Misery Mire (West)', player) + mire_clip = lambda state: state.can_reach('Misery Mire (West)', 'Region', player) and state.can_bomb_clip(mire, player) and state.has_fire_source(player) + hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and state.can_bomb_clip(world.get_region('Tower of Hera (Top)', player), player) + add_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: mire_clip(state) and state.has('Big Key (Misery Mire)', player), combine='or') + add_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: mire_clip(state), combine='or') + add_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or') + + # Build the rule for SP moat. + # We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT. + # First we require a certain type of entrance shuffle, then build the rule from its pieces. + if not world.swamp_patch_required[player]: + mirrorless_moat_rules = [] + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']: + mirrorless_moat_rules.append(lambda state: state.can_reach('Old Man S&Q', 'Entrance', player) and mire_clip(state)) + rule_map = { + 'Misery Mire (Entrance)': (lambda state: True), + 'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player)) + } + inverted = world.mode[player] == 'inverted' + hera_rule = lambda state: (state.has('Moon Pearl', player) or not inverted) and \ + rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state) + gt_rule = lambda state: (state.has('Moon Pearl', player) or inverted) and \ + rule_map.get(world.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state) + add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or all([rule(state) for rule in mirrorless_moat_rules])) + else: + add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player)) + + # Using the entrances for various ER types. Hera -> Swamp never matters because you can only logically traverse with the mire keys + mire_to_hera = world.get_entrance('Mire to Hera Clip', player) + mire_to_swamp = world.get_entrance('Hera to Swamp Clip', player) + set_rule(mire_to_hera, mire_clip) + set_rule(mire_to_swamp, lambda state: mire_clip(state) and state.has('Flippers', player)) + dungeon_reentry_rules(world, player, mire_to_hera, 'Tower of Hera (Bottom)', 'Tower of Hera Exit') + dungeon_reentry_rules(world, player, mire_to_swamp, 'Swamp Palace (Entrance)', 'Swamp Palace Exit')