362 lines
12 KiB
Python
362 lines
12 KiB
Python
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")
|
|
]
|
|
|
|
mirehera_spots = [("Mire to Hera Clip", "Mire Torches Top", "Hera Portal")]
|
|
|
|
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"),
|
|
("Thieves to Desert South Clip", "Thieves Attic", "Desert South Portal"),
|
|
("Thieves to Desert East Clip", "Thieves Attic", "Desert East Portal"),
|
|
]
|
|
|
|
specrock_spots = [
|
|
("Spec Rock Clip", "Spectacle Rock Cave (Peak)", "Spectacle Rock Cave (Top)")
|
|
]
|
|
|
|
paradox_spots = [
|
|
("Paradox Front Teleport", "Paradox Cave Front", "Paradox Cave Chest Area")
|
|
]
|
|
|
|
# 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,
|
|
heraswamp_spots,
|
|
icepalace_spots,
|
|
thievesdesert_spots,
|
|
specrock_spots,
|
|
paradox_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])
|
|
|
|
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.
|
|
# 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,
|
|
):
|
|
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 not in all_clips
|
|
][0]
|
|
|
|
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
|
|
if dungeon_entrance.name == "Skull Woods Final Section":
|
|
Rules.set_rule(clip, lambda state: False)
|
|
|
|
elif dungeon_entrance.name == "Misery Mire":
|
|
if world.swords[player] == "swordless":
|
|
Rules.add_rule(
|
|
clip, lambda state: state.has_misery_mire_medallion(player)
|
|
)
|
|
else:
|
|
Rules.add_rule(
|
|
clip,
|
|
lambda state: state.has_sword(player)
|
|
and state.has_misery_mire_medallion(player),
|
|
)
|
|
|
|
elif dungeon_entrance.name == "Agahnims Tower":
|
|
Rules.add_rule(
|
|
clip,
|
|
lambda state: state.has("Cape", player)
|
|
or state.has_beam_sword(player)
|
|
or state.has("Beat Agahnim 1", player),
|
|
)
|
|
|
|
# Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally.
|
|
Rules.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.
|
|
Rules.add_rule(
|
|
clip,
|
|
lambda state: dungeon_entrance.access_rule(fake_pearl_state(state, player)),
|
|
)
|
|
# exiting restriction
|
|
Rules.add_rule(
|
|
world.get_entrance(dungeon_exit, player),
|
|
lambda state: dungeon_entrance.can_reach(state),
|
|
)
|
|
|
|
# Otherwise, the shuffle type is lean, lite, crossed, 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):
|
|
def mire_clip(state):
|
|
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),
|
|
combine="or",
|
|
)
|
|
Rules.add_rule(
|
|
world.get_location("Tower of Hera - Big Chest", player),
|
|
lambda state: mire_clip(state) and state.has("Big Key (Misery Mire)", player),
|
|
combine="or",
|
|
)
|
|
# This uses the mire clip because it's always expected to come from mire
|
|
Rules.set_rule(
|
|
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
|
|
for door in [
|
|
"Swamp Trench 1 Approach ES",
|
|
"Swamp Hammer Switch SW",
|
|
"Swamp Entrance Down Stairs",
|
|
"Swamp Pot Row WS",
|
|
"Swamp Trench 1 Key Ledge NW",
|
|
"Swamp Hub WN",
|
|
"Swamp Hub North Ledge N",
|
|
"Swamp Crystal Switch EN",
|
|
"Swamp Push Statue S",
|
|
"Swamp Waterway NW",
|
|
"Swamp T SW",
|
|
]:
|
|
Rules.add_rule(
|
|
world.get_entrance(door, player),
|
|
lambda state: mire_clip(state)
|
|
and state.has("Small Key (Misery Mire)", player, count=6)
|
|
and state.has("Flippers", player),
|
|
combine="or",
|
|
)
|
|
|
|
Rules.add_rule(
|
|
world.get_location("Trench 1 Switch", 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] and world.shuffle[player] in [
|
|
"vanilla",
|
|
"dungeonssimple",
|
|
"dungeonsfull",
|
|
]:
|
|
rule_map = {
|
|
"Mire Portal": (
|
|
lambda state: state.can_reach("Mire Torches Top", "Entrance", player)
|
|
),
|
|
"Hera Portal": (
|
|
lambda state: state.can_reach(
|
|
"Hera Startile Corner NW", "Entrance", player
|
|
)
|
|
),
|
|
}
|
|
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_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_dm) and rule_map.get(
|
|
world.get_entrance(("Ganons Tower"), player).connected_region.name,
|
|
lambda state: False,
|
|
)(state)
|
|
|
|
def mirrorless_moat_rule(state):
|
|
return (
|
|
state.can_reach("Old Man S&Q", "Entrance", player)
|
|
and state.has("Flippers", player)
|
|
and mire_clip(state)
|
|
and (hera_rule(state) or gt_rule(state))
|
|
)
|
|
|
|
Rules.add_rule(
|
|
world.get_entrance("Swamp Lobby Moat", player),
|
|
lambda state: mirrorless_moat_rule(state),
|
|
combine="or",
|
|
)
|
|
desert_exits = ["West", "South", "East"]
|
|
|
|
for desert_exit in desert_exits:
|
|
Rules.add_rule(
|
|
world.get_entrance(f"Thieves to Desert {desert_exit} Clip", player),
|
|
lambda state: state.can_dash_clip(
|
|
world.get_region("Thieves Attic", player), player
|
|
),
|
|
)
|
|
|
|
|
|
# Collecting left chests in Paradox Cave using a dash clip -> dash citrus, 1f right, teleport up
|
|
paradox_left_chests = [
|
|
"Paradox Cave Lower - Far Left",
|
|
"Paradox Cave Lower - Left",
|
|
"Paradox Cave Lower - Middle",
|
|
]
|
|
for location in paradox_left_chests:
|
|
Rules.add_rule(
|
|
world.get_location(location, player),
|
|
lambda state: state.can_dash_clip(
|
|
world.get_location(location, player).parent_region, player
|
|
),
|
|
"or",
|
|
)
|
|
|
|
# Collecting right chests in Paradox Cave using a dash clip on left side -> dash citrus, 1f right, teleport up, then hitting the switch
|
|
paradox_right_chests = [
|
|
"Paradox Cave Lower - Right",
|
|
"Paradox Cave Lower - Far Right",
|
|
]
|
|
for location in paradox_right_chests:
|
|
Rules.add_rule(
|
|
world.get_location(location, player),
|
|
lambda state: (
|
|
state.can_dash_clip(
|
|
world.get_location(location, player).parent_region, player
|
|
)
|
|
and state.can_hit_crystal(player)
|
|
),
|
|
"or",
|
|
)
|