OoT Entrance Randomizer (#125)
Add options: "shuffle_grotto_entrances": GrottoEntrances, "shuffle_dungeon_entrances": DungeonEntrances, "owl_drops": OwlDrops, "warp_songs": WarpSongs, "spawn_positions": SpawnPositions, Add Logic Trick: "Skip King Zora as Adult with Nothing"
This commit is contained in:
parent
80c86f34a4
commit
8eb1f0258c
|
@ -5,8 +5,9 @@ from .Regions import TimeOfDay
|
||||||
class OOTEntrance(Entrance):
|
class OOTEntrance(Entrance):
|
||||||
game: str = 'Ocarina of Time'
|
game: str = 'Ocarina of Time'
|
||||||
|
|
||||||
def __init__(self, player, name='', parent=None):
|
def __init__(self, player, world, name='', parent=None):
|
||||||
super(OOTEntrance, self).__init__(player, name, parent)
|
super(OOTEntrance, self).__init__(player, name, parent)
|
||||||
|
self.world = world
|
||||||
self.access_rules = []
|
self.access_rules = []
|
||||||
self.reverse = None
|
self.reverse = None
|
||||||
self.replaces = None
|
self.replaces = None
|
||||||
|
@ -17,3 +18,27 @@ class OOTEntrance(Entrance):
|
||||||
self.primary = False
|
self.primary = False
|
||||||
self.always = False
|
self.always = False
|
||||||
self.never = False
|
self.never = False
|
||||||
|
|
||||||
|
def bind_two_way(self, other_entrance):
|
||||||
|
self.reverse = other_entrance
|
||||||
|
other_entrance.reverse = self
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
self.connected_region.entrances.remove(self)
|
||||||
|
previously_connected = self.connected_region
|
||||||
|
self.connected_region = None
|
||||||
|
return previously_connected
|
||||||
|
|
||||||
|
def get_new_target(self):
|
||||||
|
root = self.world.get_region('Root Exits', self.player)
|
||||||
|
target_entrance = OOTEntrance(self.player, self.world, 'Root -> ' + self.connected_region.name, root)
|
||||||
|
target_entrance.connect(self.connected_region)
|
||||||
|
target_entrance.replaces = self
|
||||||
|
root.exits.append(target_entrance)
|
||||||
|
return target_entrance
|
||||||
|
|
||||||
|
def assume_reachable(self):
|
||||||
|
if self.assumed == None:
|
||||||
|
self.assumed = self.get_new_target()
|
||||||
|
self.disconnect()
|
||||||
|
return self.assumed
|
||||||
|
|
|
@ -1,25 +1,775 @@
|
||||||
|
from itertools import chain
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from worlds.generic.Rules import set_rule
|
||||||
|
|
||||||
|
from .Hints import get_hint_area, HintAreaNotFound
|
||||||
|
from .Regions import TimeOfDay
|
||||||
|
|
||||||
|
|
||||||
|
def set_all_entrances_data(world, player):
|
||||||
|
for type, forward_entry, *return_entry in entrance_shuffle_table:
|
||||||
|
forward_entrance = world.get_entrance(forward_entry[0], player)
|
||||||
|
forward_entrance.data = forward_entry[1]
|
||||||
|
forward_entrance.type = type
|
||||||
|
forward_entrance.primary = True
|
||||||
|
if type == 'Grotto':
|
||||||
|
forward_entrance.data['index'] = 0x1000 + forward_entrance.data['grotto_id']
|
||||||
|
if return_entry:
|
||||||
|
return_entry = return_entry[0]
|
||||||
|
return_entrance = world.get_entrance(return_entry[0], player)
|
||||||
|
return_entrance.data = return_entry[1]
|
||||||
|
return_entrance.type = type
|
||||||
|
forward_entrance.bind_two_way(return_entrance)
|
||||||
|
if type == 'Grotto':
|
||||||
|
return_entrance.data['index'] = 0x7FFF
|
||||||
|
|
||||||
|
|
||||||
|
def assume_entrance_pool(entrance_pool, ootworld):
|
||||||
|
assumed_pool = []
|
||||||
|
for entrance in entrance_pool:
|
||||||
|
assumed_forward = entrance.assume_reachable()
|
||||||
|
if entrance.reverse != None:
|
||||||
|
assumed_return = entrance.reverse.assume_reachable()
|
||||||
|
if (entrance.type in ('Dungeon', 'Grotto', 'Grave') and entrance.reverse.name != 'Spirit Temple Lobby -> Desert Colossus From Spirit Lobby') or \
|
||||||
|
(entrance.type == 'Interior' and ootworld.shuffle_special_interior_entrances):
|
||||||
|
# In most cases, Dungeon, Grotto/Grave and Simple Interior exits shouldn't be assumed able to give access to their parent region
|
||||||
|
set_rule(assumed_return, lambda state, **kwargs: False)
|
||||||
|
assumed_forward.bind_two_way(assumed_return)
|
||||||
|
assumed_pool.append(assumed_forward)
|
||||||
|
return assumed_pool
|
||||||
|
|
||||||
|
|
||||||
|
def build_one_way_targets(world, types_to_include, exclude=(), target_region_names=()):
|
||||||
|
one_way_entrances = []
|
||||||
|
for pool_type in types_to_include:
|
||||||
|
one_way_entrances += world.get_shufflable_entrances(type=pool_type)
|
||||||
|
valid_one_way_entrances = list(filter(lambda entrance: entrance.name not in exclude, one_way_entrances))
|
||||||
|
if target_region_names:
|
||||||
|
return [entrance.get_new_target() for entrance in valid_one_way_entrances
|
||||||
|
if entrance.connected_region.name in target_region_names]
|
||||||
|
return [entrance.get_new_target() for entrance in valid_one_way_entrances]
|
||||||
|
|
||||||
|
|
||||||
|
# Abbreviations
|
||||||
|
# DMC Death Mountain Crater
|
||||||
|
# DMT Death Mountain Trail
|
||||||
|
# GC Goron City
|
||||||
|
# GF Gerudo Fortress
|
||||||
|
# GS Gold Skulltula
|
||||||
|
# GV Gerudo Valley
|
||||||
|
# HC Hyrule Castle
|
||||||
|
# HF Hyrule Field
|
||||||
|
# KF Kokiri Forest
|
||||||
|
# LH Lake Hylia
|
||||||
|
# LLR Lon Lon Ranch
|
||||||
|
# LW Lost Woods
|
||||||
|
# OGC Outside Ganon's Castle
|
||||||
|
# SFM Sacred Forest Meadow
|
||||||
|
# ToT Temple of Time
|
||||||
|
# ZD Zora's Domain
|
||||||
|
# ZF Zora's Fountain
|
||||||
|
# ZR Zora's River
|
||||||
|
|
||||||
|
entrance_shuffle_table = [
|
||||||
|
('Dungeon', ('KF Outside Deku Tree -> Deku Tree Lobby', { 'index': 0x0000 }),
|
||||||
|
('Deku Tree Lobby -> KF Outside Deku Tree', { 'index': 0x0209, 'blue_warp': 0x0457 })),
|
||||||
|
('Dungeon', ('Death Mountain -> Dodongos Cavern Beginning', { 'index': 0x0004 }),
|
||||||
|
('Dodongos Cavern Beginning -> Death Mountain', { 'index': 0x0242, 'blue_warp': 0x047A })),
|
||||||
|
('Dungeon', ('Zoras Fountain -> Jabu Jabus Belly Beginning', { 'index': 0x0028 }),
|
||||||
|
('Jabu Jabus Belly Beginning -> Zoras Fountain', { 'index': 0x0221, 'blue_warp': 0x010E })),
|
||||||
|
('Dungeon', ('SFM Forest Temple Entrance Ledge -> Forest Temple Lobby', { 'index': 0x0169 }),
|
||||||
|
('Forest Temple Lobby -> SFM Forest Temple Entrance Ledge', { 'index': 0x0215, 'blue_warp': 0x0608 })),
|
||||||
|
('Dungeon', ('DMC Fire Temple Entrance -> Fire Temple Lower', { 'index': 0x0165 }),
|
||||||
|
('Fire Temple Lower -> DMC Fire Temple Entrance', { 'index': 0x024A, 'blue_warp': 0x0564 })),
|
||||||
|
('Dungeon', ('Lake Hylia -> Water Temple Lobby', { 'index': 0x0010 }),
|
||||||
|
('Water Temple Lobby -> Lake Hylia', { 'index': 0x021D, 'blue_warp': 0x060C })),
|
||||||
|
('Dungeon', ('Desert Colossus -> Spirit Temple Lobby', { 'index': 0x0082 }),
|
||||||
|
('Spirit Temple Lobby -> Desert Colossus From Spirit Lobby', { 'index': 0x01E1, 'blue_warp': 0x0610 })),
|
||||||
|
('Dungeon', ('Graveyard Warp Pad Region -> Shadow Temple Entryway', { 'index': 0x0037 }),
|
||||||
|
('Shadow Temple Entryway -> Graveyard Warp Pad Region', { 'index': 0x0205, 'blue_warp': 0x0580 })),
|
||||||
|
('Dungeon', ('Kakariko Village -> Bottom of the Well', { 'index': 0x0098 }),
|
||||||
|
('Bottom of the Well -> Kakariko Village', { 'index': 0x02A6 })),
|
||||||
|
('Dungeon', ('ZF Ice Ledge -> Ice Cavern Beginning', { 'index': 0x0088 }),
|
||||||
|
('Ice Cavern Beginning -> ZF Ice Ledge', { 'index': 0x03D4 })),
|
||||||
|
('Dungeon', ('Gerudo Fortress -> Gerudo Training Grounds Lobby', { 'index': 0x0008 }),
|
||||||
|
('Gerudo Training Grounds Lobby -> Gerudo Fortress', { 'index': 0x03A8 })),
|
||||||
|
|
||||||
|
('Interior', ('Kokiri Forest -> KF Midos House', { 'index': 0x0433 }),
|
||||||
|
('KF Midos House -> Kokiri Forest', { 'index': 0x0443 })),
|
||||||
|
('Interior', ('Kokiri Forest -> KF Sarias House', { 'index': 0x0437 }),
|
||||||
|
('KF Sarias House -> Kokiri Forest', { 'index': 0x0447 })),
|
||||||
|
('Interior', ('Kokiri Forest -> KF House of Twins', { 'index': 0x009C }),
|
||||||
|
('KF House of Twins -> Kokiri Forest', { 'index': 0x033C })),
|
||||||
|
('Interior', ('Kokiri Forest -> KF Know It All House', { 'index': 0x00C9 }),
|
||||||
|
('KF Know It All House -> Kokiri Forest', { 'index': 0x026A })),
|
||||||
|
('Interior', ('Kokiri Forest -> KF Kokiri Shop', { 'index': 0x00C1 }),
|
||||||
|
('KF Kokiri Shop -> Kokiri Forest', { 'index': 0x0266 })),
|
||||||
|
('Interior', ('Lake Hylia -> LH Lab', { 'index': 0x0043 }),
|
||||||
|
('LH Lab -> Lake Hylia', { 'index': 0x03CC })),
|
||||||
|
('Interior', ('LH Fishing Island -> LH Fishing Hole', { 'index': 0x045F }),
|
||||||
|
('LH Fishing Hole -> LH Fishing Island', { 'index': 0x0309 })),
|
||||||
|
('Interior', ('GV Fortress Side -> GV Carpenter Tent', { 'index': 0x03A0 }),
|
||||||
|
('GV Carpenter Tent -> GV Fortress Side', { 'index': 0x03D0 })),
|
||||||
|
('Interior', ('Market Entrance -> Market Guard House', { 'index': 0x007E }),
|
||||||
|
('Market Guard House -> Market Entrance', { 'index': 0x026E })),
|
||||||
|
('Interior', ('Market -> Market Mask Shop', { 'index': 0x0530 }),
|
||||||
|
('Market Mask Shop -> Market', { 'index': 0x01D1, 'addresses': [0xC6DA5E] })),
|
||||||
|
('Interior', ('Market -> Market Bombchu Bowling', { 'index': 0x0507 }),
|
||||||
|
('Market Bombchu Bowling -> Market', { 'index': 0x03BC })),
|
||||||
|
('Interior', ('Market -> Market Potion Shop', { 'index': 0x0388 }),
|
||||||
|
('Market Potion Shop -> Market', { 'index': 0x02A2 })),
|
||||||
|
('Interior', ('Market -> Market Treasure Chest Game', { 'index': 0x0063 }),
|
||||||
|
('Market Treasure Chest Game -> Market', { 'index': 0x01D5 })),
|
||||||
|
('Interior', ('Market Back Alley -> Market Bombchu Shop', { 'index': 0x0528 }),
|
||||||
|
('Market Bombchu Shop -> Market Back Alley', { 'index': 0x03C0 })),
|
||||||
|
('Interior', ('Market Back Alley -> Market Man in Green House', { 'index': 0x043B }),
|
||||||
|
('Market Man in Green House -> Market Back Alley', { 'index': 0x0067 })),
|
||||||
|
('Interior', ('Kakariko Village -> Kak Carpenter Boss House', { 'index': 0x02FD }),
|
||||||
|
('Kak Carpenter Boss House -> Kakariko Village', { 'index': 0x0349 })),
|
||||||
|
('Interior', ('Kakariko Village -> Kak House of Skulltula', { 'index': 0x0550 }),
|
||||||
|
('Kak House of Skulltula -> Kakariko Village', { 'index': 0x04EE })),
|
||||||
|
('Interior', ('Kakariko Village -> Kak Impas House', { 'index': 0x039C }),
|
||||||
|
('Kak Impas House -> Kakariko Village', { 'index': 0x0345 })),
|
||||||
|
('Interior', ('Kak Impas Ledge -> Kak Impas House Back', { 'index': 0x05C8 }),
|
||||||
|
('Kak Impas House Back -> Kak Impas Ledge', { 'index': 0x05DC })),
|
||||||
|
('Interior', ('Kak Backyard -> Kak Odd Medicine Building', { 'index': 0x0072 }),
|
||||||
|
('Kak Odd Medicine Building -> Kak Backyard', { 'index': 0x034D })),
|
||||||
|
('Interior', ('Graveyard -> Graveyard Dampes House', { 'index': 0x030D }),
|
||||||
|
('Graveyard Dampes House -> Graveyard', { 'index': 0x0355 })),
|
||||||
|
('Interior', ('Goron City -> GC Shop', { 'index': 0x037C }),
|
||||||
|
('GC Shop -> Goron City', { 'index': 0x03FC })),
|
||||||
|
('Interior', ('Zoras Domain -> ZD Shop', { 'index': 0x0380 }),
|
||||||
|
('ZD Shop -> Zoras Domain', { 'index': 0x03C4 })),
|
||||||
|
('Interior', ('Lon Lon Ranch -> LLR Talons House', { 'index': 0x004F }),
|
||||||
|
('LLR Talons House -> Lon Lon Ranch', { 'index': 0x0378 })),
|
||||||
|
('Interior', ('Lon Lon Ranch -> LLR Stables', { 'index': 0x02F9 }),
|
||||||
|
('LLR Stables -> Lon Lon Ranch', { 'index': 0x042F })),
|
||||||
|
('Interior', ('Lon Lon Ranch -> LLR Tower', { 'index': 0x05D0 }),
|
||||||
|
('LLR Tower -> Lon Lon Ranch', { 'index': 0x05D4 })),
|
||||||
|
('Interior', ('Market -> Market Bazaar', { 'index': 0x052C }),
|
||||||
|
('Market Bazaar -> Market', { 'index': 0x03B8, 'addresses': [0xBEFD74] })),
|
||||||
|
('Interior', ('Market -> Market Shooting Gallery', { 'index': 0x016D }),
|
||||||
|
('Market Shooting Gallery -> Market', { 'index': 0x01CD, 'addresses': [0xBEFD7C] })),
|
||||||
|
('Interior', ('Kakariko Village -> Kak Bazaar', { 'index': 0x00B7 }),
|
||||||
|
('Kak Bazaar -> Kakariko Village', { 'index': 0x0201, 'addresses': [0xBEFD72] })),
|
||||||
|
('Interior', ('Kakariko Village -> Kak Shooting Gallery', { 'index': 0x003B }),
|
||||||
|
('Kak Shooting Gallery -> Kakariko Village', { 'index': 0x0463, 'addresses': [0xBEFD7A] })),
|
||||||
|
('Interior', ('Desert Colossus -> Colossus Great Fairy Fountain', { 'index': 0x0588 }),
|
||||||
|
('Colossus Great Fairy Fountain -> Desert Colossus', { 'index': 0x057C, 'addresses': [0xBEFD82] })),
|
||||||
|
('Interior', ('Hyrule Castle Grounds -> HC Great Fairy Fountain', { 'index': 0x0578 }),
|
||||||
|
('HC Great Fairy Fountain -> Castle Grounds', { 'index': 0x0340, 'addresses': [0xBEFD80] })),
|
||||||
|
('Interior', ('Ganons Castle Grounds -> OGC Great Fairy Fountain', { 'index': 0x04C2 }),
|
||||||
|
('OGC Great Fairy Fountain -> Castle Grounds', { 'index': 0x0340, 'addresses': [0xBEFD6C] })),
|
||||||
|
('Interior', ('DMC Lower Nearby -> DMC Great Fairy Fountain', { 'index': 0x04BE }),
|
||||||
|
('DMC Great Fairy Fountain -> DMC Lower Local', { 'index': 0x0482, 'addresses': [0xBEFD6A] })),
|
||||||
|
('Interior', ('Death Mountain Summit -> DMT Great Fairy Fountain', { 'index': 0x0315 }),
|
||||||
|
('DMT Great Fairy Fountain -> Death Mountain Summit', { 'index': 0x045B, 'addresses': [0xBEFD68] })),
|
||||||
|
('Interior', ('Zoras Fountain -> ZF Great Fairy Fountain', { 'index': 0x0371 }),
|
||||||
|
('ZF Great Fairy Fountain -> Zoras Fountain', { 'index': 0x0394, 'addresses': [0xBEFD7E] })),
|
||||||
|
|
||||||
|
('SpecialInterior', ('Kokiri Forest -> KF Links House', { 'index': 0x0272 }),
|
||||||
|
('KF Links House -> Kokiri Forest', { 'index': 0x0211 })),
|
||||||
|
('SpecialInterior', ('ToT Entrance -> Temple of Time', { 'index': 0x0053 }),
|
||||||
|
('Temple of Time -> ToT Entrance', { 'index': 0x0472 })),
|
||||||
|
('SpecialInterior', ('Kakariko Village -> Kak Windmill', { 'index': 0x0453 }),
|
||||||
|
('Kak Windmill -> Kakariko Village', { 'index': 0x0351 })),
|
||||||
|
('SpecialInterior', ('Kakariko Village -> Kak Potion Shop Front', { 'index': 0x0384 }),
|
||||||
|
('Kak Potion Shop Front -> Kakariko Village', { 'index': 0x044B })),
|
||||||
|
('SpecialInterior', ('Kak Backyard -> Kak Potion Shop Back', { 'index': 0x03EC }),
|
||||||
|
('Kak Potion Shop Back -> Kak Backyard', { 'index': 0x04FF })),
|
||||||
|
|
||||||
|
('Grotto', ('Desert Colossus -> Colossus Grotto', { 'grotto_id': 0x00, 'entrance': 0x05BC, 'content': 0xFD, 'scene': 0x5C }),
|
||||||
|
('Colossus Grotto -> Desert Colossus', { 'grotto_id': 0x00 })),
|
||||||
|
('Grotto', ('Lake Hylia -> LH Grotto', { 'grotto_id': 0x01, 'entrance': 0x05A4, 'content': 0xEF, 'scene': 0x57 }),
|
||||||
|
('LH Grotto -> Lake Hylia', { 'grotto_id': 0x01 })),
|
||||||
|
('Grotto', ('Zora River -> ZR Storms Grotto', { 'grotto_id': 0x02, 'entrance': 0x05BC, 'content': 0xEB, 'scene': 0x54 }),
|
||||||
|
('ZR Storms Grotto -> Zora River', { 'grotto_id': 0x02 })),
|
||||||
|
('Grotto', ('Zora River -> ZR Fairy Grotto', { 'grotto_id': 0x03, 'entrance': 0x036D, 'content': 0xE6, 'scene': 0x54 }),
|
||||||
|
('ZR Fairy Grotto -> Zora River', { 'grotto_id': 0x03 })),
|
||||||
|
('Grotto', ('Zora River -> ZR Open Grotto', { 'grotto_id': 0x04, 'entrance': 0x003F, 'content': 0x29, 'scene': 0x54 }),
|
||||||
|
('ZR Open Grotto -> Zora River', { 'grotto_id': 0x04 })),
|
||||||
|
('Grotto', ('DMC Lower Nearby -> DMC Hammer Grotto', { 'grotto_id': 0x05, 'entrance': 0x05A4, 'content': 0xF9, 'scene': 0x61 }),
|
||||||
|
('DMC Hammer Grotto -> DMC Lower Local', { 'grotto_id': 0x05 })),
|
||||||
|
('Grotto', ('DMC Upper Nearby -> DMC Upper Grotto', { 'grotto_id': 0x06, 'entrance': 0x003F, 'content': 0x7A, 'scene': 0x61 }),
|
||||||
|
('DMC Upper Grotto -> DMC Upper Local', { 'grotto_id': 0x06 })),
|
||||||
|
('Grotto', ('GC Grotto Platform -> GC Grotto', { 'grotto_id': 0x07, 'entrance': 0x05A4, 'content': 0xFB, 'scene': 0x62 }),
|
||||||
|
('GC Grotto -> GC Grotto Platform', { 'grotto_id': 0x07 })),
|
||||||
|
('Grotto', ('Death Mountain -> DMT Storms Grotto', { 'grotto_id': 0x08, 'entrance': 0x003F, 'content': 0x57, 'scene': 0x60 }),
|
||||||
|
('DMT Storms Grotto -> Death Mountain', { 'grotto_id': 0x08 })),
|
||||||
|
('Grotto', ('Death Mountain Summit -> DMT Cow Grotto', { 'grotto_id': 0x09, 'entrance': 0x05FC, 'content': 0xF8, 'scene': 0x60 }),
|
||||||
|
('DMT Cow Grotto -> Death Mountain Summit', { 'grotto_id': 0x09 })),
|
||||||
|
('Grotto', ('Kak Backyard -> Kak Open Grotto', { 'grotto_id': 0x0A, 'entrance': 0x003F, 'content': 0x28, 'scene': 0x52 }),
|
||||||
|
('Kak Open Grotto -> Kak Backyard', { 'grotto_id': 0x0A })),
|
||||||
|
('Grotto', ('Kakariko Village -> Kak Redead Grotto', { 'grotto_id': 0x0B, 'entrance': 0x05A0, 'content': 0xE7, 'scene': 0x52 }),
|
||||||
|
('Kak Redead Grotto -> Kakariko Village', { 'grotto_id': 0x0B })),
|
||||||
|
('Grotto', ('Hyrule Castle Grounds -> HC Storms Grotto', { 'grotto_id': 0x0C, 'entrance': 0x05B8, 'content': 0xF6, 'scene': 0x5F }),
|
||||||
|
('HC Storms Grotto -> Castle Grounds', { 'grotto_id': 0x0C })),
|
||||||
|
('Grotto', ('Hyrule Field -> HF Tektite Grotto', { 'grotto_id': 0x0D, 'entrance': 0x05C0, 'content': 0xE1, 'scene': 0x51 }),
|
||||||
|
('HF Tektite Grotto -> Hyrule Field', { 'grotto_id': 0x0D })),
|
||||||
|
('Grotto', ('Hyrule Field -> HF Near Kak Grotto', { 'grotto_id': 0x0E, 'entrance': 0x0598, 'content': 0xE5, 'scene': 0x51 }),
|
||||||
|
('HF Near Kak Grotto -> Hyrule Field', { 'grotto_id': 0x0E })),
|
||||||
|
('Grotto', ('Hyrule Field -> HF Fairy Grotto', { 'grotto_id': 0x0F, 'entrance': 0x036D, 'content': 0xFF, 'scene': 0x51 }),
|
||||||
|
('HF Fairy Grotto -> Hyrule Field', { 'grotto_id': 0x0F })),
|
||||||
|
('Grotto', ('Hyrule Field -> HF Near Market Grotto', { 'grotto_id': 0x10, 'entrance': 0x003F, 'content': 0x00, 'scene': 0x51 }),
|
||||||
|
('HF Near Market Grotto -> Hyrule Field', { 'grotto_id': 0x10 })),
|
||||||
|
('Grotto', ('Hyrule Field -> HF Cow Grotto', { 'grotto_id': 0x11, 'entrance': 0x05A8, 'content': 0xE4, 'scene': 0x51 }),
|
||||||
|
('HF Cow Grotto -> Hyrule Field', { 'grotto_id': 0x11 })),
|
||||||
|
('Grotto', ('Hyrule Field -> HF Inside Fence Grotto', { 'grotto_id': 0x12, 'entrance': 0x059C, 'content': 0xE6, 'scene': 0x51 }),
|
||||||
|
('HF Inside Fence Grotto -> Hyrule Field', { 'grotto_id': 0x12 })),
|
||||||
|
('Grotto', ('Hyrule Field -> HF Open Grotto', { 'grotto_id': 0x13, 'entrance': 0x003F, 'content': 0x03, 'scene': 0x51 }),
|
||||||
|
('HF Open Grotto -> Hyrule Field', { 'grotto_id': 0x13 })),
|
||||||
|
('Grotto', ('Hyrule Field -> HF Southeast Grotto', { 'grotto_id': 0x14, 'entrance': 0x003F, 'content': 0x22, 'scene': 0x51 }),
|
||||||
|
('HF Southeast Grotto -> Hyrule Field', { 'grotto_id': 0x14 })),
|
||||||
|
('Grotto', ('Lon Lon Ranch -> LLR Grotto', { 'grotto_id': 0x15, 'entrance': 0x05A4, 'content': 0xFC, 'scene': 0x63 }),
|
||||||
|
('LLR Grotto -> Lon Lon Ranch', { 'grotto_id': 0x15 })),
|
||||||
|
('Grotto', ('SFM Entryway -> SFM Wolfos Grotto', { 'grotto_id': 0x16, 'entrance': 0x05B4, 'content': 0xED, 'scene': 0x56 }),
|
||||||
|
('SFM Wolfos Grotto -> SFM Entryway', { 'grotto_id': 0x16 })),
|
||||||
|
('Grotto', ('Sacred Forest Meadow -> SFM Storms Grotto', { 'grotto_id': 0x17, 'entrance': 0x05BC, 'content': 0xEE, 'scene': 0x56 }),
|
||||||
|
('SFM Storms Grotto -> Sacred Forest Meadow', { 'grotto_id': 0x17 })),
|
||||||
|
('Grotto', ('Sacred Forest Meadow -> SFM Fairy Grotto', { 'grotto_id': 0x18, 'entrance': 0x036D, 'content': 0xFF, 'scene': 0x56 }),
|
||||||
|
('SFM Fairy Grotto -> Sacred Forest Meadow', { 'grotto_id': 0x18 })),
|
||||||
|
('Grotto', ('LW Beyond Mido -> LW Scrubs Grotto', { 'grotto_id': 0x19, 'entrance': 0x05B0, 'content': 0xF5, 'scene': 0x5B }),
|
||||||
|
('LW Scrubs Grotto -> LW Beyond Mido', { 'grotto_id': 0x19 })),
|
||||||
|
('Grotto', ('Lost Woods -> LW Near Shortcuts Grotto', { 'grotto_id': 0x1A, 'entrance': 0x003F, 'content': 0x14, 'scene': 0x5B }),
|
||||||
|
('LW Near Shortcuts Grotto -> Lost Woods', { 'grotto_id': 0x1A })),
|
||||||
|
('Grotto', ('Kokiri Forest -> KF Storms Grotto', { 'grotto_id': 0x1B, 'entrance': 0x003F, 'content': 0x2C, 'scene': 0x55 }),
|
||||||
|
('KF Storms Grotto -> Kokiri Forest', { 'grotto_id': 0x1B })),
|
||||||
|
('Grotto', ('Zoras Domain -> ZD Storms Grotto', { 'grotto_id': 0x1C, 'entrance': 0x036D, 'content': 0xFF, 'scene': 0x58 }),
|
||||||
|
('ZD Storms Grotto -> Zoras Domain', { 'grotto_id': 0x1C })),
|
||||||
|
('Grotto', ('Gerudo Fortress -> GF Storms Grotto', { 'grotto_id': 0x1D, 'entrance': 0x036D, 'content': 0xFF, 'scene': 0x5D }),
|
||||||
|
('GF Storms Grotto -> Gerudo Fortress', { 'grotto_id': 0x1D })),
|
||||||
|
('Grotto', ('GV Fortress Side -> GV Storms Grotto', { 'grotto_id': 0x1E, 'entrance': 0x05BC, 'content': 0xF0, 'scene': 0x5A }),
|
||||||
|
('GV Storms Grotto -> GV Fortress Side', { 'grotto_id': 0x1E })),
|
||||||
|
('Grotto', ('GV Grotto Ledge -> GV Octorok Grotto', { 'grotto_id': 0x1F, 'entrance': 0x05AC, 'content': 0xF2, 'scene': 0x5A }),
|
||||||
|
('GV Octorok Grotto -> GV Grotto Ledge', { 'grotto_id': 0x1F })),
|
||||||
|
('Grotto', ('LW Beyond Mido -> Deku Theater', { 'grotto_id': 0x20, 'entrance': 0x05C4, 'content': 0xF3, 'scene': 0x5B }),
|
||||||
|
('Deku Theater -> LW Beyond Mido', { 'grotto_id': 0x20 })),
|
||||||
|
|
||||||
|
('Grave', ('Graveyard -> Graveyard Shield Grave', { 'index': 0x004B }),
|
||||||
|
('Graveyard Shield Grave -> Graveyard', { 'index': 0x035D })),
|
||||||
|
('Grave', ('Graveyard -> Graveyard Heart Piece Grave', { 'index': 0x031C }),
|
||||||
|
('Graveyard Heart Piece Grave -> Graveyard', { 'index': 0x0361 })),
|
||||||
|
('Grave', ('Graveyard -> Graveyard Composers Grave', { 'index': 0x002D }),
|
||||||
|
('Graveyard Composers Grave -> Graveyard', { 'index': 0x050B })),
|
||||||
|
('Grave', ('Graveyard -> Graveyard Dampes Grave', { 'index': 0x044F }),
|
||||||
|
('Graveyard Dampes Grave -> Graveyard', { 'index': 0x0359 })),
|
||||||
|
|
||||||
|
('Overworld', ('Kokiri Forest -> LW Bridge From Forest', { 'index': 0x05E0 }),
|
||||||
|
('LW Bridge -> Kokiri Forest', { 'index': 0x020D })),
|
||||||
|
('Overworld', ('Kokiri Forest -> Lost Woods', { 'index': 0x011E }),
|
||||||
|
('LW Forest Exit -> Kokiri Forest', { 'index': 0x0286 })),
|
||||||
|
('Overworld', ('Lost Woods -> GC Woods Warp', { 'index': 0x04E2 }),
|
||||||
|
('GC Woods Warp -> Lost Woods', { 'index': 0x04D6 })),
|
||||||
|
('Overworld', ('Lost Woods -> Zora River', { 'index': 0x01DD }),
|
||||||
|
('Zora River -> Lost Woods', { 'index': 0x04DA })),
|
||||||
|
('Overworld', ('LW Beyond Mido -> SFM Entryway', { 'index': 0x00FC }),
|
||||||
|
('SFM Entryway -> LW Beyond Mido', { 'index': 0x01A9 })),
|
||||||
|
('Overworld', ('LW Bridge -> Hyrule Field', { 'index': 0x0185 }),
|
||||||
|
('Hyrule Field -> LW Bridge', { 'index': 0x04DE })),
|
||||||
|
('Overworld', ('Hyrule Field -> Lake Hylia', { 'index': 0x0102 }),
|
||||||
|
('Lake Hylia -> Hyrule Field', { 'index': 0x0189 })),
|
||||||
|
('Overworld', ('Hyrule Field -> Gerudo Valley', { 'index': 0x0117 }),
|
||||||
|
('Gerudo Valley -> Hyrule Field', { 'index': 0x018D })),
|
||||||
|
('Overworld', ('Hyrule Field -> Market Entrance', { 'index': 0x0276 }),
|
||||||
|
('Market Entrance -> Hyrule Field', { 'index': 0x01FD })),
|
||||||
|
('Overworld', ('Hyrule Field -> Kakariko Village', { 'index': 0x00DB }),
|
||||||
|
('Kakariko Village -> Hyrule Field', { 'index': 0x017D })),
|
||||||
|
('Overworld', ('Hyrule Field -> ZR Front', { 'index': 0x00EA }),
|
||||||
|
('ZR Front -> Hyrule Field', { 'index': 0x0181 })),
|
||||||
|
('Overworld', ('Hyrule Field -> Lon Lon Ranch', { 'index': 0x0157 }),
|
||||||
|
('Lon Lon Ranch -> Hyrule Field', { 'index': 0x01F9 })),
|
||||||
|
('Overworld', ('Lake Hylia -> Zoras Domain', { 'index': 0x0328 }),
|
||||||
|
('Zoras Domain -> Lake Hylia', { 'index': 0x0560 })),
|
||||||
|
('Overworld', ('GV Fortress Side -> Gerudo Fortress', { 'index': 0x0129 }),
|
||||||
|
('Gerudo Fortress -> GV Fortress Side', { 'index': 0x022D })),
|
||||||
|
('Overworld', ('GF Outside Gate -> Wasteland Near Fortress', { 'index': 0x0130 }),
|
||||||
|
('Wasteland Near Fortress -> GF Outside Gate', { 'index': 0x03AC })),
|
||||||
|
('Overworld', ('Wasteland Near Colossus -> Desert Colossus', { 'index': 0x0123 }),
|
||||||
|
('Desert Colossus -> Wasteland Near Colossus', { 'index': 0x0365 })),
|
||||||
|
('Overworld', ('Market Entrance -> Market', { 'index': 0x00B1 }),
|
||||||
|
('Market -> Market Entrance', { 'index': 0x0033 })),
|
||||||
|
('Overworld', ('Market -> Castle Grounds', { 'index': 0x0138 }),
|
||||||
|
('Castle Grounds -> Market', { 'index': 0x025A })),
|
||||||
|
('Overworld', ('Market -> ToT Entrance', { 'index': 0x0171 }),
|
||||||
|
('ToT Entrance -> Market', { 'index': 0x025E })),
|
||||||
|
('Overworld', ('Kakariko Village -> Graveyard', { 'index': 0x00E4 }),
|
||||||
|
('Graveyard -> Kakariko Village', { 'index': 0x0195 })),
|
||||||
|
('Overworld', ('Kak Behind Gate -> Death Mountain', { 'index': 0x013D }),
|
||||||
|
('Death Mountain -> Kak Behind Gate', { 'index': 0x0191 })),
|
||||||
|
('Overworld', ('Death Mountain -> Goron City', { 'index': 0x014D }),
|
||||||
|
('Goron City -> Death Mountain', { 'index': 0x01B9 })),
|
||||||
|
('Overworld', ('GC Darunias Chamber -> DMC Lower Local', { 'index': 0x0246 }),
|
||||||
|
('DMC Lower Nearby -> GC Darunias Chamber', { 'index': 0x01C1 })),
|
||||||
|
('Overworld', ('Death Mountain Summit -> DMC Upper Local', { 'index': 0x0147 }),
|
||||||
|
('DMC Upper Nearby -> Death Mountain Summit', { 'index': 0x01BD })),
|
||||||
|
('Overworld', ('ZR Behind Waterfall -> Zoras Domain', { 'index': 0x0108 }),
|
||||||
|
('Zoras Domain -> ZR Behind Waterfall', { 'index': 0x019D })),
|
||||||
|
('Overworld', ('ZD Behind King Zora -> Zoras Fountain', { 'index': 0x0225 }),
|
||||||
|
('Zoras Fountain -> ZD Behind King Zora', { 'index': 0x01A1 })),
|
||||||
|
|
||||||
|
('OwlDrop', ('LH Owl Flight -> Hyrule Field', { 'index': 0x027E, 'addresses': [0xAC9F26] })),
|
||||||
|
('OwlDrop', ('DMT Owl Flight -> Kak Impas Rooftop', { 'index': 0x0554, 'addresses': [0xAC9EF2] })),
|
||||||
|
|
||||||
|
('Spawn', ('Child Spawn -> KF Links House', { 'index': 0x00BB, 'addresses': [0xB06342] })),
|
||||||
|
('Spawn', ('Adult Spawn -> Temple of Time', { 'index': 0x05F4, 'addresses': [0xB06332] })),
|
||||||
|
|
||||||
|
('WarpSong', ('Minuet of Forest Warp -> Sacred Forest Meadow', { 'index': 0x0600, 'addresses': [0xBF023C] })),
|
||||||
|
('WarpSong', ('Bolero of Fire Warp -> DMC Central Local', { 'index': 0x04F6, 'addresses': [0xBF023E] })),
|
||||||
|
('WarpSong', ('Serenade of Water Warp -> Lake Hylia', { 'index': 0x0604, 'addresses': [0xBF0240] })),
|
||||||
|
('WarpSong', ('Requiem of Spirit Warp -> Desert Colossus', { 'index': 0x01F1, 'addresses': [0xBF0242] })),
|
||||||
|
('WarpSong', ('Nocturne of Shadow Warp -> Graveyard Warp Pad Region', { 'index': 0x0568, 'addresses': [0xBF0244] })),
|
||||||
|
('WarpSong', ('Prelude of Light Warp -> Temple of Time', { 'index': 0x05F4, 'addresses': [0xBF0246] })),
|
||||||
|
|
||||||
|
('Extra', ('ZD Eyeball Frog Timeout -> Zoras Domain', { 'index': 0x0153 })),
|
||||||
|
('Extra', ('ZR Top of Waterfall -> Zora River', { 'index': 0x0199 })),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Basically, the entrances in the list above that go to:
|
||||||
|
# - DMC Central Local (child access for the bean and skull)
|
||||||
|
# - Desert Colossus (child access to colossus and spirit)
|
||||||
|
# - Graveyard Warp Pad Region (access to shadow, plus the gossip stone)
|
||||||
|
# We will always need to pick one from each list to receive a one-way entrance
|
||||||
|
# if shuffling warp songs (depending on other settings).
|
||||||
|
# Table maps: short key -> ([target regions], [allowed types])
|
||||||
|
priority_entrance_table = {
|
||||||
|
'Bolero': (['DMC Central Local'], ['OwlDrop', 'WarpSong']),
|
||||||
|
'Nocturne': (['Graveyard Warp Pad Region'], ['OwlDrop', 'Spawn', 'WarpSong']),
|
||||||
|
'Requiem': (['Desert Colossus', 'Desert Colossus From Spirit Lobby'], ['OwlDrop', 'Spawn', 'WarpSong']),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EntranceShuffleError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def shuffle_random_entrances(ootworld):
|
def shuffle_random_entrances(ootworld):
|
||||||
world = ootworld.world
|
world = ootworld.world
|
||||||
player = ootworld.player
|
player = ootworld.player
|
||||||
|
|
||||||
# Gather locations to keep reachable for validation
|
# Gather locations to keep reachable for validation
|
||||||
|
all_state = world.get_all_state(use_cache=True)
|
||||||
|
locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))}
|
||||||
|
|
||||||
# Set entrance data for all entrances
|
# Set entrance data for all entrances
|
||||||
|
set_all_entrances_data(world, player)
|
||||||
|
|
||||||
# Determine entrance pools based on settings
|
# Determine entrance pools based on settings
|
||||||
|
one_way_entrance_pools = {}
|
||||||
|
entrance_pools = {}
|
||||||
|
one_way_priorities = {}
|
||||||
|
|
||||||
|
if ootworld.owl_drops:
|
||||||
|
one_way_entrance_pools['OwlDrop'] = ootworld.get_shufflable_entrances(type='OwlDrop')
|
||||||
|
if ootworld.spawn_positions:
|
||||||
|
one_way_entrance_pools['Spawn'] = ootworld.get_shufflable_entrances(type='Spawn')
|
||||||
|
if ootworld.warp_songs:
|
||||||
|
one_way_entrance_pools['WarpSong'] = ootworld.get_shufflable_entrances(type='WarpSong')
|
||||||
|
if world.accessibility[player].current_key != 'minimal' and ootworld.logic_rules == 'glitchless':
|
||||||
|
one_way_priorities['Bolero'] = priority_entrance_table['Bolero']
|
||||||
|
one_way_priorities['Nocturne'] = priority_entrance_table['Nocturne']
|
||||||
|
if not ootworld.shuffle_dungeon_entrances and not ootworld.shuffle_overworld_entrances:
|
||||||
|
one_way_priorities['Requiem'] = priority_entrance_table['Requiem']
|
||||||
|
|
||||||
|
if ootworld.shuffle_dungeon_entrances:
|
||||||
|
entrance_pools['Dungeon'] = ootworld.get_shufflable_entrances(type='Dungeon', only_primary=True)
|
||||||
|
if ootworld.open_forest == 'closed':
|
||||||
|
entrance_pools['Dungeon'].remove(world.get_entrance('KF Outside Deku Tree -> Deku Tree Lobby', player))
|
||||||
|
if ootworld.shuffle_interior_entrances != 'off':
|
||||||
|
entrance_pools['Interior'] = ootworld.get_shufflable_entrances(type='Interior', only_primary=True)
|
||||||
|
if ootworld.shuffle_special_interior_entrances:
|
||||||
|
entrance_pools['Interior'] += ootworld.get_shufflable_entrances(type='SpecialInterior', only_primary=True)
|
||||||
|
if ootworld.shuffle_grotto_entrances:
|
||||||
|
entrance_pools['GrottoGrave'] = ootworld.get_shufflable_entrances(type='Grotto', only_primary=True)
|
||||||
|
entrance_pools['GrottoGrave'] += ootworld.get_shufflable_entrances(type='Grave', only_primary=True)
|
||||||
|
if ootworld.shuffle_overworld_entrances:
|
||||||
|
entrance_pools['Overworld'] = ootworld.get_shufflable_entrances(type='Overworld')
|
||||||
|
|
||||||
# Mark shuffled entrances
|
# Mark shuffled entrances
|
||||||
|
for entrance in chain(chain.from_iterable(one_way_entrance_pools.values()), chain.from_iterable(entrance_pools.values())):
|
||||||
|
entrance.shuffled = True
|
||||||
|
if entrance.reverse:
|
||||||
|
entrance.reverse.shuffled = True
|
||||||
|
|
||||||
# Build target entrance pools
|
# Build target entrance pools
|
||||||
|
one_way_target_entrance_pools = {}
|
||||||
|
for pool_type, entrance_pool in one_way_entrance_pools.items():
|
||||||
|
if pool_type == 'OwlDrop':
|
||||||
|
valid_target_types = ('WarpSong', 'OwlDrop', 'Overworld', 'Extra')
|
||||||
|
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time'])
|
||||||
|
for target in one_way_target_entrance_pools[pool_type]:
|
||||||
|
set_rule(target, lambda state: state._oot_reach_as_age(target.parent_region, 'child', player))
|
||||||
|
elif pool_type in {'Spawn', 'WarpSong'}:
|
||||||
|
valid_target_types = ('Spawn', 'WarpSong', 'OwlDrop', 'Overworld', 'Interior', 'SpecialInterior', 'Extra')
|
||||||
|
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types)
|
||||||
|
# Ensure that the last entrance doesn't assume the rest of the targets are reachable?
|
||||||
|
# Disconnect one-way entrances for priority placement
|
||||||
|
for entrance in chain.from_iterable(one_way_entrance_pools.values()):
|
||||||
|
entrance.disconnect()
|
||||||
|
|
||||||
|
target_entrance_pools = {}
|
||||||
|
for pool_type, entrance_pool in entrance_pools.items():
|
||||||
|
target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld)
|
||||||
|
|
||||||
|
# Build all_state and none_state
|
||||||
|
all_state = world.get_all_state(use_cache=False)
|
||||||
|
all_state.child_reachable_regions[player] = set()
|
||||||
|
all_state.adult_reachable_regions[player] = set()
|
||||||
|
all_state.child_blocked_connections[player] = set()
|
||||||
|
all_state.adult_blocked_connections[player] = set()
|
||||||
|
all_state.day_reachable_regions[player] = set()
|
||||||
|
all_state.dampe_reachable_regions[player] = set()
|
||||||
|
all_state.stale[player] = True
|
||||||
|
none_state = all_state.copy()
|
||||||
|
for item_tuple in none_state.prog_items:
|
||||||
|
if item_tuple[1] == player:
|
||||||
|
none_state.prog_items[item_tuple] = 0
|
||||||
|
|
||||||
|
# Plando entrances?
|
||||||
|
|
||||||
# Place priority entrances
|
# Place priority entrances
|
||||||
|
shuffle_one_way_priority_entrances(ootworld, one_way_priorities, one_way_entrance_pools, one_way_target_entrance_pools, locations_to_ensure_reachable, all_state, none_state, retry_count=2)
|
||||||
|
|
||||||
# Delete priority targets from one-way pools
|
# Delete priority targets from one-way pools
|
||||||
|
replaced_entrances = [entrance.replaces for entrance in chain.from_iterable(one_way_entrance_pools.values())]
|
||||||
|
for remaining_target in chain.from_iterable(one_way_target_entrance_pools.values()):
|
||||||
|
if remaining_target.replaces in replaced_entrances:
|
||||||
|
delete_target_entrance(remaining_target)
|
||||||
|
|
||||||
|
for pool_type, entrance_pool in one_way_entrance_pools.items():
|
||||||
|
shuffle_entrance_pool(ootworld, entrance_pool, one_way_target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state, check_all=True, retry_count=5)
|
||||||
|
replaced_entrances = [entrance.replaces for entrance in entrance_pool]
|
||||||
|
for remaining_target in chain.from_iterable(one_way_target_entrance_pools.values()):
|
||||||
|
if remaining_target.replaces in replaced_entrances:
|
||||||
|
delete_target_entrance(remaining_target)
|
||||||
|
for unused_target in one_way_target_entrance_pools[pool_type]:
|
||||||
|
delete_target_entrance(unused_target)
|
||||||
|
|
||||||
# Shuffle all entrance pools, in order
|
# Shuffle all entrance pools, in order
|
||||||
|
for pool_type, entrance_pool in entrance_pools.items():
|
||||||
|
shuffle_entrance_pool(ootworld, entrance_pool, target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state)
|
||||||
|
|
||||||
# Verification steps:
|
# Multiple checks after shuffling to ensure everything is OK
|
||||||
# All entrances are properly connected to a region
|
# Check that all entrances hook up correctly
|
||||||
|
for entrance in ootworld.get_shuffled_entrances():
|
||||||
|
if entrance.connected_region == None:
|
||||||
|
logging.getLogger('').error(f'{entrance} was shuffled but is not connected to any region')
|
||||||
|
if entrance.replaces == None:
|
||||||
|
logging.getLogger('').error(f'{entrance} was shuffled but does not replace any entrance')
|
||||||
|
if len(ootworld.get_region('Root Exits').exits) > 8:
|
||||||
|
for exit in ootworld.get_region('Root Exits').exits:
|
||||||
|
logging.getLogger('').error(f'Root Exit: {exit} -> {exit.connected_region}')
|
||||||
|
logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances')
|
||||||
# Game is beatable
|
# Game is beatable
|
||||||
|
new_all_state = world.get_all_state(use_cache=False)
|
||||||
|
if not world.has_beaten_game(new_all_state, player):
|
||||||
|
raise EntranceShuffleError('Cannot beat game')
|
||||||
# Validate world
|
# Validate world
|
||||||
|
validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_entrance(ootworld, entrance, target, rollbacks, locations_to_ensure_reachable, all_state, none_state):
|
||||||
|
try:
|
||||||
|
check_entrances_compatibility(entrance, target, rollbacks)
|
||||||
|
change_connections(entrance, target)
|
||||||
|
validate_world(ootworld, entrance, locations_to_ensure_reachable, all_state, none_state)
|
||||||
|
rollbacks.append((entrance, target))
|
||||||
|
return True
|
||||||
|
except EntranceShuffleError as e:
|
||||||
|
logging.getLogger('').debug(f'Failed to connect {entrance} to {target}, reason: {e}')
|
||||||
|
if entrance.connected_region:
|
||||||
|
restore_connections(entrance, target)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def shuffle_one_way_priority_entrances(ootworld, one_way_priorities, one_way_entrance_pools, one_way_target_entrance_pools,
|
||||||
|
locations_to_ensure_reachable, all_state, none_state, retry_count=2):
|
||||||
|
|
||||||
|
ootworld.priority_entrances = []
|
||||||
|
|
||||||
|
while retry_count:
|
||||||
|
retry_count -= 1
|
||||||
|
rollbacks = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
for key, (regions, types) in one_way_priorities.items():
|
||||||
|
place_one_way_priority_entrance(ootworld, key, regions, types, rollbacks, locations_to_ensure_reachable,
|
||||||
|
all_state, none_state, one_way_entrance_pools, one_way_target_entrance_pools)
|
||||||
|
for entrance, target in rollbacks:
|
||||||
|
confirm_replacement(entrance, target)
|
||||||
|
return
|
||||||
|
except EntranceShuffleError as error:
|
||||||
|
for entrance, target in rollbacks:
|
||||||
|
restore_connections(entrance, target)
|
||||||
|
logging.getLogger('').debug(f'Failed to place all priority one-way entrances, retrying {retry_count} more times')
|
||||||
|
|
||||||
|
raise EntranceShuffleError(f'Priority one-way entrance placement attempt count exceeded for world {ootworld.player}')
|
||||||
|
|
||||||
|
def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, allowed_types, rollbacks, locations_to_ensure_reachable,
|
||||||
|
all_state, none_state, one_way_entrance_pools, one_way_target_entrance_pools):
|
||||||
|
|
||||||
|
avail_pool = list(chain.from_iterable(one_way_entrance_pools[t] for t in allowed_types if t in one_way_entrance_pools))
|
||||||
|
ootworld.world.random.shuffle(avail_pool)
|
||||||
|
|
||||||
|
for entrance in avail_pool:
|
||||||
|
if entrance.replaces:
|
||||||
|
continue
|
||||||
|
if entrance.parent_region.name == 'Adult Spawn' and (priority_name != 'Nocturne' or ootworld.hints == 'mask'):
|
||||||
|
continue
|
||||||
|
if not ootworld.shuffle_dungeon_entrances and priority_name == 'Nocturne':
|
||||||
|
if entrance.type != 'WarpSong' and entrance.parent_region.name != 'Adult Spawn':
|
||||||
|
continue
|
||||||
|
for target in one_way_target_entrance_pools[entrance.type]:
|
||||||
|
if target.connected_region and target.connected_region.name in allowed_regions:
|
||||||
|
if replace_entrance(ootworld, entrance, target, rollbacks, locations_to_ensure_reachable, all_state, none_state):
|
||||||
|
logging.getLogger('').debug(f'Priority placing {entrance} as {target} for {priority_name}')
|
||||||
|
ootworld.priority_entrances.append(entrance)
|
||||||
|
return
|
||||||
|
raise EntranceShuffleError(f'Unable to place priority one-way entrance for {priority_name} in world {ootworld.player}')
|
||||||
|
|
||||||
|
|
||||||
|
def shuffle_entrance_pool(ootworld, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=100):
|
||||||
|
|
||||||
|
restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances)
|
||||||
|
|
||||||
|
while retry_count:
|
||||||
|
retry_count -= 1
|
||||||
|
rollbacks = []
|
||||||
|
try:
|
||||||
|
shuffle_entrances(ootworld, restrictive_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state)
|
||||||
|
if check_all:
|
||||||
|
shuffle_entrances(ootworld, soft_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state)
|
||||||
|
else:
|
||||||
|
shuffle_entrances(ootworld, soft_entrances, target_entrances, rollbacks, set(), all_state, none_state)
|
||||||
|
|
||||||
|
validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state)
|
||||||
|
for entrance, target in rollbacks:
|
||||||
|
confirm_replacement(entrance, target)
|
||||||
|
return
|
||||||
|
except EntranceShuffleError as e:
|
||||||
|
for entrance, target in rollbacks:
|
||||||
|
restore_connections(entrance, target)
|
||||||
|
logging.getLogger('').debug(f'Failed to place all entrances in pool, retrying {retry_count} more times')
|
||||||
|
|
||||||
|
raise EntranceShuffleError(f'Entrance placement attempt count exceeded for world {ootworld.player}')
|
||||||
|
|
||||||
|
def shuffle_entrances(ootworld, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state):
|
||||||
|
ootworld.world.random.shuffle(entrances)
|
||||||
|
for entrance in entrances:
|
||||||
|
if entrance.connected_region != None:
|
||||||
|
continue
|
||||||
|
ootworld.world.random.shuffle(target_entrances)
|
||||||
|
for target in target_entrances:
|
||||||
|
if target.connected_region == None:
|
||||||
|
continue
|
||||||
|
if replace_entrance(ootworld, entrance, target, rollbacks, locations_to_ensure_reachable, all_state, none_state):
|
||||||
|
break
|
||||||
|
if entrance.connected_region == None:
|
||||||
|
raise EntranceShuffleError('No more valid entrances')
|
||||||
|
|
||||||
|
|
||||||
|
def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entrances):
|
||||||
|
world = ootworld.world
|
||||||
|
player = ootworld.player
|
||||||
|
|
||||||
|
# Disconnect all root assumed entrances and save original connections
|
||||||
|
original_connected_regions = {}
|
||||||
|
entrances_to_disconnect = set(assumed_entrances).union(entrance.reverse for entrance in assumed_entrances if entrance.reverse)
|
||||||
|
for entrance in entrances_to_disconnect:
|
||||||
|
if entrance.connected_region:
|
||||||
|
original_connected_regions[entrance] = entrance.disconnect()
|
||||||
|
|
||||||
|
all_state = world.get_all_state(use_cache=False)
|
||||||
|
|
||||||
|
restrictive_entrances = []
|
||||||
|
soft_entrances = []
|
||||||
|
|
||||||
|
for entrance in entrances_to_split:
|
||||||
|
all_state.age[player] = 'child'
|
||||||
|
if not all_state.can_reach(entrance, 'Entrance', player):
|
||||||
|
restrictive_entrances.append(entrance)
|
||||||
|
continue
|
||||||
|
all_state.age[player] = 'adult'
|
||||||
|
if not all_state.can_reach(entrance, 'Entrance', player):
|
||||||
|
restrictive_entrances.append(entrance)
|
||||||
|
continue
|
||||||
|
all_state.age[player] = None
|
||||||
|
if not all_state._oot_reach_at_time(entrance.parent_region.name, TimeOfDay.ALL, [], player):
|
||||||
|
restrictive_entrances.append(entrance)
|
||||||
|
continue
|
||||||
|
soft_entrances.append(entrance)
|
||||||
|
|
||||||
|
# Reconnect assumed entrances
|
||||||
|
for entrance in entrances_to_disconnect:
|
||||||
|
if entrance in original_connected_regions:
|
||||||
|
entrance.connect(original_connected_regions[entrance])
|
||||||
|
|
||||||
|
return restrictive_entrances, soft_entrances
|
||||||
|
|
||||||
|
|
||||||
|
# Check to ensure the world is valid.
|
||||||
|
# TODO: improve this function
|
||||||
|
def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all_state_orig, none_state_orig):
|
||||||
|
|
||||||
|
world = ootworld.world
|
||||||
|
player = ootworld.player
|
||||||
|
|
||||||
|
all_state = all_state_orig.copy()
|
||||||
|
none_state = none_state_orig.copy()
|
||||||
|
|
||||||
|
if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions:
|
||||||
|
time_travel_state = none_state.copy()
|
||||||
|
time_travel_state.collect(ootworld.create_item('Time Travel'), event=True)
|
||||||
|
time_travel_state._oot_update_age_reachable_regions(player)
|
||||||
|
|
||||||
|
# For various reasons, we don't want the player to end up through certain entrances as the wrong age
|
||||||
|
# This means we need to hard check that none of the relevant entrances are ever reachable as that age
|
||||||
|
# This is mostly relevant when shuffling special interiors (such as windmill or kak potion shop)
|
||||||
|
# Warp Songs and Overworld Spawns can also end up inside certain indoors so those need to be handled as well
|
||||||
|
CHILD_FORBIDDEN = ['OGC Great Fairy Fountain -> Castle Grounds', 'GV Carpenter Tent -> GV Fortress Side']
|
||||||
|
ADULT_FORBIDDEN = ['HC Great Fairy Fountain -> Castle Grounds', 'HC Storms Grotto -> Castle Grounds']
|
||||||
|
|
||||||
|
for entrance in ootworld.get_shufflable_entrances():
|
||||||
|
if entrance.shuffled and entrance.replaces:
|
||||||
|
if entrance.replaces.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.replaces.reverse]):
|
||||||
|
raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential child access')
|
||||||
|
if entrance.replaces.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.replaces.reverse]):
|
||||||
|
raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential adult access')
|
||||||
|
else:
|
||||||
|
if entrance.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.reverse]):
|
||||||
|
raise EntranceShuffleError(f'{entrance.name} potentially accessible as child')
|
||||||
|
if entrance.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.reverse]):
|
||||||
|
raise EntranceShuffleError(f'{entrance.name} potentially accessible as adult')
|
||||||
|
|
||||||
|
# Check if all locations are reachable if not beatable-only or game is not yet complete
|
||||||
|
if locations_to_ensure_reachable:
|
||||||
|
if world.accessibility[player].current_key != 'minimal' or not world.can_beat_game(all_state):
|
||||||
|
for loc in locations_to_ensure_reachable:
|
||||||
|
if not all_state.can_reach(loc, 'Location', player):
|
||||||
|
raise EntranceShuffleError(f'{loc} is unreachable')
|
||||||
|
|
||||||
|
if ootworld.shuffle_interior_entrances and (entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']):
|
||||||
|
# Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints
|
||||||
|
potion_front_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
|
||||||
|
potion_back_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
|
||||||
|
if potion_front_entrance is not None and potion_back_entrance is not None and not same_hint_area(potion_front_entrance, potion_back_entrance):
|
||||||
|
raise EntranceShuffleError('Kak Potion Shop entrances are not in the same hint area')
|
||||||
|
|
||||||
|
# When cows are shuffled, ensure the same thing for Impa's House, since the cow is reachable from both sides
|
||||||
|
if ootworld.shuffle_cows:
|
||||||
|
impas_front_entrance = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player)
|
||||||
|
impas_back_entrance = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player)
|
||||||
|
if impas_front_entrance is not None and impas_back_entrance is not None and not same_hint_area(impas_front_entrance, impas_back_entrance):
|
||||||
|
raise EntranceShuffleError('Kak Impas House entrances are not in the same hint area')
|
||||||
|
|
||||||
|
# Check basic refills, time passing, return to ToT
|
||||||
|
if (ootworld.shuffle_special_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions) and \
|
||||||
|
(entrance_placed == None or entrance_placed.type in ['SpecialInterior', 'Overworld', 'Spawn', 'WarpSong', 'OwlDrop']):
|
||||||
|
|
||||||
|
valid_starting_regions = {'Kokiri Forest', 'Kakariko Village'}
|
||||||
|
if not any(region for region in valid_starting_regions if none_state.can_reach(region, 'Region', player)):
|
||||||
|
raise EntranceShuffleError('Invalid starting area')
|
||||||
|
|
||||||
|
if not (any(region for region in time_travel_state.child_reachable_regions[player] if region.time_passes) and
|
||||||
|
any(region for region in time_travel_state.adult_reachable_regions[player] if region.time_passes)):
|
||||||
|
raise EntranceShuffleError('Time passing is not guaranteed as both ages')
|
||||||
|
|
||||||
|
if ootworld.starting_age == 'child' and (world.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]):
|
||||||
|
raise EntranceShuffleError('Path to ToT as adult not guaranteed')
|
||||||
|
if ootworld.starting_age == 'adult' and (world.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]):
|
||||||
|
raise EntranceShuffleError('Path to ToT as child not guaranteed')
|
||||||
|
|
||||||
|
if (ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances) and \
|
||||||
|
(entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior', 'Overworld', 'Spawn', 'WarpSong', 'OwlDrop']):
|
||||||
|
# Ensure big poe shop is always reachable as adult
|
||||||
|
if world.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]:
|
||||||
|
raise EntranceShuffleError('Big Poe Shop access not guaranteed as adult')
|
||||||
|
if ootworld.shopsanity == 'off':
|
||||||
|
# Ensure that Goron and Zora shops are accessible as adult
|
||||||
|
if world.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]:
|
||||||
|
raise EntranceShuffleError('Goron City Shop not accessible as adult')
|
||||||
|
if world.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]:
|
||||||
|
raise EntranceShuffleError('Zora\'s Domain Shop not accessible as adult')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Recursively check if a given entrance is unreachable as a given age
|
||||||
|
def entrance_unreachable_as(entrance, age, already_checked=[]):
|
||||||
|
already_checked.append(entrance)
|
||||||
|
|
||||||
|
if entrance.type in {'WarpSong', 'Overworld'}:
|
||||||
|
return False
|
||||||
|
elif entrance.type == 'OwlDrop':
|
||||||
|
return age == 'adult'
|
||||||
|
elif entrance.name == 'Child Spawn -> KF Links House':
|
||||||
|
return age == 'adult'
|
||||||
|
elif entrance.name == 'Adult Spawn -> Temple of Time':
|
||||||
|
return age == 'child'
|
||||||
|
|
||||||
|
for parent_entrance in entrance.parent_region.entrances:
|
||||||
|
if parent_entrance in already_checked:
|
||||||
|
continue
|
||||||
|
unreachable = entrance_unreachable_as(parent_entrance, age, already_checked)
|
||||||
|
if not unreachable:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def same_hint_area(first, second):
|
||||||
|
try:
|
||||||
|
return get_hint_area(first) == get_hint_area(second)
|
||||||
|
except HintAreaNotFound:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_entrance_replacing(region, entrance_name, player):
|
||||||
|
original_entrance = region.world.get_entrance(entrance_name, player)
|
||||||
|
if not original_entrance.shuffled:
|
||||||
|
return original_entrance
|
||||||
|
|
||||||
|
try:
|
||||||
|
return next(filter(lambda entrance: entrance.replaces and entrance.replaces.name == entrance_name and \
|
||||||
|
entrance.parent_region and entrance.parent_region.name != 'Root Exits' and \
|
||||||
|
entrance.type not in ('OwlDrop', 'Spawn', 'WarpSong') and entrance.player == player,
|
||||||
|
region.entrances))
|
||||||
|
except StopIteration:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def change_connections(entrance, target):
|
||||||
|
entrance.connect(target.disconnect())
|
||||||
|
entrance.replaces = target.replaces
|
||||||
|
if entrance.reverse:
|
||||||
|
target.replaces.reverse.connect(entrance.reverse.assumed.disconnect())
|
||||||
|
target.replaces.reverse.replaces = entrance.reverse
|
||||||
|
|
||||||
|
def restore_connections(entrance, target):
|
||||||
|
target.connect(entrance.disconnect())
|
||||||
|
entrance.replaces = None
|
||||||
|
if entrance.reverse:
|
||||||
|
entrance.reverse.assumed.connect(target.replaces.reverse.disconnect())
|
||||||
|
target.replaces.reverse.replaces = None
|
||||||
|
|
||||||
|
def check_entrances_compatibility(entrance, target, rollbacks):
|
||||||
|
# An entrance shouldn't be connected to its own scene
|
||||||
|
if entrance.parent_region.get_scene() and entrance.parent_region.get_scene() == target.connected_region.get_scene():
|
||||||
|
raise EntranceShuffleError('Self-scene connections are forbidden')
|
||||||
|
|
||||||
|
# One-way entrances shouldn't lead to the same scene as other one-ways
|
||||||
|
if entrance.type in {'OwlDrop', 'Spawn', 'WarpSong'} and \
|
||||||
|
any([rollback[0].connected_region.get_scene() == target.connected_region.get_scene() for rollback in rollbacks]):
|
||||||
|
raise EntranceShuffleError('Another one-way entrance leads to the same scene')
|
||||||
|
|
||||||
|
def confirm_replacement(entrance, target):
|
||||||
|
delete_target_entrance(target)
|
||||||
|
logging.getLogger('').debug(f'Connected {entrance} to {entrance.connected_region}')
|
||||||
|
if entrance.reverse:
|
||||||
|
replaced_reverse = target.replaces.reverse
|
||||||
|
delete_target_entrance(entrance.reverse.assumed)
|
||||||
|
logging.getLogger('').debug(f'Connected {replaced_reverse} to {replaced_reverse.connected_region}')
|
||||||
|
|
||||||
|
|
||||||
|
def delete_target_entrance(target):
|
||||||
|
if target.connected_region != None:
|
||||||
|
target.disconnect()
|
||||||
|
if target.parent_region != None:
|
||||||
|
target.parent_region.exits.remove(target)
|
||||||
|
target.parent_region = None
|
||||||
|
|
|
@ -397,6 +397,8 @@ def get_barren_hint(world, checked):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
area_weights = [world.empty_areas[area]['weight'] for area in areas]
|
area_weights = [world.empty_areas[area]['weight'] for area in areas]
|
||||||
|
if not any(area_weights):
|
||||||
|
return None
|
||||||
|
|
||||||
area = world.hint_rng.choices(areas, weights=area_weights)[0]
|
area = world.hint_rng.choices(areas, weights=area_weights)[0]
|
||||||
if world.empty_areas[area]['dungeon']:
|
if world.empty_areas[area]['dungeon']:
|
||||||
|
|
|
@ -727,6 +727,14 @@ known_logic_tricks = {
|
||||||
To kill it, the logic normally guarantees one of
|
To kill it, the logic normally guarantees one of
|
||||||
Hookshot, Bow, or Magic.
|
Hookshot, Bow, or Magic.
|
||||||
'''},
|
'''},
|
||||||
|
'Skip King Zora as Adult with Nothing': {
|
||||||
|
'name' : 'logic_king_zora_skip',
|
||||||
|
'tags' : ("Zora's Domain",),
|
||||||
|
'tooltip' : '''\
|
||||||
|
With a precise jump as adult, it is possible to
|
||||||
|
get on the fence next to King Zora from the front
|
||||||
|
to access Zora's Fountain.
|
||||||
|
'''},
|
||||||
'Shadow Temple River Statue with Bombchu': {
|
'Shadow Temple River Statue with Bombchu': {
|
||||||
'name' : 'logic_shadow_statue',
|
'name' : 'logic_shadow_statue',
|
||||||
'tags' : ("Shadow Temple",),
|
'tags' : ("Shadow Temple",),
|
||||||
|
|
|
@ -94,12 +94,37 @@ class StartingAge(Choice):
|
||||||
option_adult = 1
|
option_adult = 1
|
||||||
|
|
||||||
|
|
||||||
# TODO: document and name ER options
|
|
||||||
class InteriorEntrances(Choice):
|
class InteriorEntrances(Choice):
|
||||||
|
"""Shuffles interior entrances. "Simple" shuffles houses and Great Fairies; "All" includes Windmill, Link's House, Temple of Time, and Kak potion shop."""
|
||||||
option_off = 0
|
option_off = 0
|
||||||
option_simple = 1
|
option_simple = 1
|
||||||
option_all = 2
|
option_all = 2
|
||||||
alias_false = 0
|
alias_false = 0
|
||||||
|
alias_true = 2
|
||||||
|
|
||||||
|
|
||||||
|
class GrottoEntrances(Toggle):
|
||||||
|
"""Shuffles grotto and grave entrances."""
|
||||||
|
|
||||||
|
|
||||||
|
class DungeonEntrances(Toggle):
|
||||||
|
"""Shuffles dungeon entrances, excluding Ganon's Castle. Opens Deku, Fire and BotW to both ages."""
|
||||||
|
|
||||||
|
|
||||||
|
class OverworldEntrances(Toggle):
|
||||||
|
"""Shuffles overworld loading zones."""
|
||||||
|
|
||||||
|
|
||||||
|
class OwlDrops(Toggle):
|
||||||
|
"""Randomizes owl drops from Lake Hylia or Death Mountain Trail as child."""
|
||||||
|
|
||||||
|
|
||||||
|
class WarpSongs(Toggle):
|
||||||
|
"""Randomizes warp song destinations."""
|
||||||
|
|
||||||
|
|
||||||
|
class SpawnPositions(Toggle):
|
||||||
|
"""Randomizes the starting position on loading a save. Consistent between savewarps."""
|
||||||
|
|
||||||
|
|
||||||
class TriforceHunt(Toggle):
|
class TriforceHunt(Toggle):
|
||||||
|
@ -139,12 +164,12 @@ class MQDungeons(Range):
|
||||||
world_options: typing.Dict[str, type(Option)] = {
|
world_options: typing.Dict[str, type(Option)] = {
|
||||||
"starting_age": StartingAge,
|
"starting_age": StartingAge,
|
||||||
# "shuffle_interior_entrances": InteriorEntrances,
|
# "shuffle_interior_entrances": InteriorEntrances,
|
||||||
# "shuffle_grotto_entrances": Toggle,
|
"shuffle_grotto_entrances": GrottoEntrances,
|
||||||
# "shuffle_dungeon_entrances": Toggle,
|
"shuffle_dungeon_entrances": DungeonEntrances,
|
||||||
# "shuffle_overworld_entrances": Toggle,
|
# "shuffle_overworld_entrances": OverworldEntrances,
|
||||||
# "owl_drops": Toggle,
|
"owl_drops": OwlDrops,
|
||||||
# "warp_songs": Toggle,
|
"warp_songs": WarpSongs,
|
||||||
# "spawn_positions": Toggle,
|
"spawn_positions": SpawnPositions,
|
||||||
"triforce_hunt": TriforceHunt,
|
"triforce_hunt": TriforceHunt,
|
||||||
"triforce_goal": TriforceGoal,
|
"triforce_goal": TriforceGoal,
|
||||||
"extra_triforce_percentage": ExtraTriforces,
|
"extra_triforce_percentage": ExtraTriforces,
|
||||||
|
|
|
@ -451,14 +451,16 @@ class Rule_AST_Transformer(ast.NodeTransformer):
|
||||||
if self.world.ensure_tod_access:
|
if self.world.ensure_tod_access:
|
||||||
# tod has DAY or (tod == NONE and (ss or find a path from a provider))
|
# tod has DAY or (tod == NONE and (ss or find a path from a provider))
|
||||||
# parsing is better than constructing this expression by hand
|
# parsing is better than constructing this expression by hand
|
||||||
return ast.parse("(tod & TimeOfDay.DAY) if tod else (state.has_all(('Ocarina', 'Suns Song')) or state.search.can_reach(spot.parent_region, age=age, tod=TimeOfDay.DAY))", mode='eval').body
|
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
|
||||||
|
return ast.parse(f"(state.has('Ocarina', player) and state.has('Suns Song', player)) or state._oot_reach_at_time('{r.name}', TimeOfDay.DAY, [], player)", mode='eval').body
|
||||||
return ast.NameConstant(True)
|
return ast.NameConstant(True)
|
||||||
|
|
||||||
def at_dampe_time(self, node):
|
def at_dampe_time(self, node):
|
||||||
if self.world.ensure_tod_access:
|
if self.world.ensure_tod_access:
|
||||||
# tod has DAMPE or (tod == NONE and (find a path from a provider))
|
# tod has DAMPE or (tod == NONE and (find a path from a provider))
|
||||||
# parsing is better than constructing this expression by hand
|
# parsing is better than constructing this expression by hand
|
||||||
return ast.parse("(tod & TimeOfDay.DAMPE) if tod else state.search.can_reach(spot.parent_region, age=age, tod=TimeOfDay.DAMPE)", mode='eval').body
|
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
|
||||||
|
return ast.parse(f"state._oot_reach_at_time('{r.name}', TimeOfDay.DAMPE, [], player)", mode='eval').body
|
||||||
return ast.NameConstant(True)
|
return ast.NameConstant(True)
|
||||||
|
|
||||||
def at_night(self, node):
|
def at_night(self, node):
|
||||||
|
@ -468,7 +470,8 @@ class Rule_AST_Transformer(ast.NodeTransformer):
|
||||||
if self.world.ensure_tod_access:
|
if self.world.ensure_tod_access:
|
||||||
# tod has DAMPE or (tod == NONE and (ss or find a path from a provider))
|
# tod has DAMPE or (tod == NONE and (ss or find a path from a provider))
|
||||||
# parsing is better than constructing this expression by hand
|
# parsing is better than constructing this expression by hand
|
||||||
return ast.parse("(tod & TimeOfDay.DAMPE) if tod else (state.has_all(('Ocarina', 'Suns Song')) or state.search.can_reach(spot.parent_region, age=age, tod=TimeOfDay.DAMPE))", mode='eval').body
|
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
|
||||||
|
return ast.parse(f"(state.has('Ocarina', player) and state.has('Suns Song', player)) or state._oot_reach_at_time('{r.name}', TimeOfDay.DAMPE, [], player)", mode='eval').body
|
||||||
return ast.NameConstant(True)
|
return ast.NameConstant(True)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ from collections import deque
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .SaveContext import SaveContext
|
from .SaveContext import SaveContext
|
||||||
|
from .Regions import TimeOfDay
|
||||||
|
|
||||||
from BaseClasses import CollectionState
|
from BaseClasses import CollectionState
|
||||||
from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item
|
from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item
|
||||||
|
@ -42,6 +43,36 @@ class OOTLogic(LogicMixin):
|
||||||
return can_reach
|
return can_reach
|
||||||
return self.age[player] == age
|
return self.age[player] == age
|
||||||
|
|
||||||
|
def _oot_reach_at_time(self, regionname, tod, already_checked, player):
|
||||||
|
name_map = {
|
||||||
|
TimeOfDay.DAY: self.day_reachable_regions[player],
|
||||||
|
TimeOfDay.DAMPE: self.dampe_reachable_regions[player],
|
||||||
|
TimeOfDay.ALL: self.day_reachable_regions[player].intersection(self.dampe_reachable_regions[player])
|
||||||
|
}
|
||||||
|
if regionname in name_map[tod]:
|
||||||
|
return True
|
||||||
|
region = self.world.get_region(regionname, player)
|
||||||
|
if region.provides_time == TimeOfDay.ALL or regionname == 'Root':
|
||||||
|
self.day_reachable_regions[player].add(regionname)
|
||||||
|
self.dampe_reachable_regions[player].add(regionname)
|
||||||
|
return True
|
||||||
|
if region.provides_time == TimeOfDay.DAMPE:
|
||||||
|
self.dampe_reachable_regions[player].add(regionname)
|
||||||
|
return tod == TimeOfDay.DAMPE
|
||||||
|
for entrance in region.entrances:
|
||||||
|
if entrance.parent_region.name in already_checked:
|
||||||
|
continue
|
||||||
|
if self._oot_reach_at_time(entrance.parent_region.name, tod, already_checked + [regionname], player):
|
||||||
|
if tod == TimeOfDay.DAY:
|
||||||
|
self.day_reachable_regions[player].add(regionname)
|
||||||
|
elif tod == TimeOfDay.DAMPE:
|
||||||
|
self.dampe_reachable_regions[player].add(regionname)
|
||||||
|
elif tod == TimeOfDay.ALL:
|
||||||
|
self.day_reachable_regions[player].add(regionname)
|
||||||
|
self.dampe_reachable_regions[player].add(regionname)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
# Store the age before calling this!
|
# Store the age before calling this!
|
||||||
def _oot_update_age_reachable_regions(self, player):
|
def _oot_update_age_reachable_regions(self, player):
|
||||||
self.stale[player] = False
|
self.stale[player] = False
|
||||||
|
@ -62,6 +93,8 @@ class OOTLogic(LogicMixin):
|
||||||
while queue:
|
while queue:
|
||||||
connection = queue.popleft()
|
connection = queue.popleft()
|
||||||
new_region = connection.connected_region
|
new_region = connection.connected_region
|
||||||
|
if new_region is None:
|
||||||
|
continue
|
||||||
if new_region in rrp:
|
if new_region in rrp:
|
||||||
bc.remove(connection)
|
bc.remove(connection)
|
||||||
elif connection.can_reach(self):
|
elif connection.can_reach(self):
|
||||||
|
|
|
@ -7,7 +7,7 @@ logger = logging.getLogger("Ocarina of Time")
|
||||||
|
|
||||||
from .Location import OOTLocation, LocationFactory, location_name_to_id
|
from .Location import OOTLocation, LocationFactory, location_name_to_id
|
||||||
from .Entrance import OOTEntrance
|
from .Entrance import OOTEntrance
|
||||||
from .EntranceShuffle import shuffle_random_entrances
|
from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table
|
||||||
from .Items import OOTItem, item_table, oot_data_to_ap_id
|
from .Items import OOTItem, item_table, oot_data_to_ap_id
|
||||||
from .ItemPool import generate_itempool, add_dungeon_items, get_junk_item, get_junk_pool
|
from .ItemPool import generate_itempool, add_dungeon_items, get_junk_item, get_junk_pool
|
||||||
from .Regions import OOTRegion, TimeOfDay
|
from .Regions import OOTRegion, TimeOfDay
|
||||||
|
@ -66,6 +66,8 @@ class OOTWorld(World):
|
||||||
self.adult_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
|
self.adult_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
|
||||||
self.child_blocked_connections = {player: set() for player in range(1, parent.players + 1)}
|
self.child_blocked_connections = {player: set() for player in range(1, parent.players + 1)}
|
||||||
self.adult_blocked_connections = {player: set() for player in range(1, parent.players + 1)}
|
self.adult_blocked_connections = {player: set() for player in range(1, parent.players + 1)}
|
||||||
|
self.day_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
|
||||||
|
self.dampe_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
|
||||||
self.age = {player: None for player in range(1, parent.players + 1)}
|
self.age = {player: None for player in range(1, parent.players + 1)}
|
||||||
|
|
||||||
def oot_copy(self):
|
def oot_copy(self):
|
||||||
|
@ -78,6 +80,10 @@ class OOTWorld(World):
|
||||||
range(1, self.world.players + 1)}
|
range(1, self.world.players + 1)}
|
||||||
ret.adult_blocked_connections = {player: copy.copy(self.adult_blocked_connections[player]) for player in
|
ret.adult_blocked_connections = {player: copy.copy(self.adult_blocked_connections[player]) for player in
|
||||||
range(1, self.world.players + 1)}
|
range(1, self.world.players + 1)}
|
||||||
|
ret.day_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
|
||||||
|
range(1, self.world.players + 1)}
|
||||||
|
ret.dampe_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
|
||||||
|
range(1, self.world.players + 1)}
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
CollectionState.__init__ = oot_init
|
CollectionState.__init__ = oot_init
|
||||||
|
@ -88,6 +94,8 @@ class OOTWorld(World):
|
||||||
world.state.adult_reachable_regions = {player: set() for player in range(1, world.players + 1)}
|
world.state.adult_reachable_regions = {player: set() for player in range(1, world.players + 1)}
|
||||||
world.state.child_blocked_connections = {player: set() for player in range(1, world.players + 1)}
|
world.state.child_blocked_connections = {player: set() for player in range(1, world.players + 1)}
|
||||||
world.state.adult_blocked_connections = {player: set() for player in range(1, world.players + 1)}
|
world.state.adult_blocked_connections = {player: set() for player in range(1, world.players + 1)}
|
||||||
|
world.state.day_reachable_regions = {player: set() for player in range(1, world.players + 1)}
|
||||||
|
world.state.dampe_reachable_regions = {player: set() for player in range(1, world.players + 1)}
|
||||||
world.state.age = {player: None for player in range(1, world.players + 1)}
|
world.state.age = {player: None for player in range(1, world.players + 1)}
|
||||||
|
|
||||||
return super().__new__(cls)
|
return super().__new__(cls)
|
||||||
|
@ -178,14 +186,8 @@ class OOTWorld(World):
|
||||||
self.mq_dungeons_random = False # this will be a deprecated option later
|
self.mq_dungeons_random = False # this will be a deprecated option later
|
||||||
self.ocarina_songs = False # just need to pull in the OcarinaSongs module
|
self.ocarina_songs = False # just need to pull in the OcarinaSongs module
|
||||||
self.big_poe_count = 1 # disabled due to client-side issues for now
|
self.big_poe_count = 1 # disabled due to client-side issues for now
|
||||||
# ER options
|
|
||||||
self.shuffle_interior_entrances = 'off'
|
self.shuffle_interior_entrances = 'off'
|
||||||
self.shuffle_grotto_entrances = False
|
self.shuffle_overworld_entrances = False # disabled due to stability issues
|
||||||
self.shuffle_dungeon_entrances = False
|
|
||||||
self.shuffle_overworld_entrances = False
|
|
||||||
self.owl_drops = False
|
|
||||||
self.warp_songs = False
|
|
||||||
self.spawn_positions = False
|
|
||||||
|
|
||||||
# Set internal names used by the OoT generator
|
# Set internal names used by the OoT generator
|
||||||
self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld']
|
self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld']
|
||||||
|
@ -318,7 +320,7 @@ class OOTWorld(World):
|
||||||
new_location.show_in_spoiler = False
|
new_location.show_in_spoiler = False
|
||||||
if 'exits' in region:
|
if 'exits' in region:
|
||||||
for exit, rule in region['exits'].items():
|
for exit, rule in region['exits'].items():
|
||||||
new_exit = OOTEntrance(self.player, '%s => %s' % (new_region.name, exit), new_region)
|
new_exit = OOTEntrance(self.player, self.world, '%s -> %s' % (new_region.name, exit), new_region)
|
||||||
new_exit.vanilla_connected_region = exit
|
new_exit.vanilla_connected_region = exit
|
||||||
new_exit.rule_string = rule
|
new_exit.rule_string = rule
|
||||||
if self.world.logic_rules != 'none':
|
if self.world.logic_rules != 'none':
|
||||||
|
@ -437,7 +439,7 @@ class OOTWorld(World):
|
||||||
world_type = 'Glitched World'
|
world_type = 'Glitched World'
|
||||||
overworld_data_path = data_path(world_type, 'Overworld.json')
|
overworld_data_path = data_path(world_type, 'Overworld.json')
|
||||||
menu = OOTRegion('Menu', None, None, self.player)
|
menu = OOTRegion('Menu', None, None, self.player)
|
||||||
start = OOTEntrance(self.player, 'New Game', menu)
|
start = OOTEntrance(self.player, self.world, 'New Game', menu)
|
||||||
menu.exits.append(start)
|
menu.exits.append(start)
|
||||||
self.world.regions.append(menu)
|
self.world.regions.append(menu)
|
||||||
self.load_regions_from_json(overworld_data_path)
|
self.load_regions_from_json(overworld_data_path)
|
||||||
|
@ -449,14 +451,10 @@ class OOTWorld(World):
|
||||||
self.random_shop_prices()
|
self.random_shop_prices()
|
||||||
self.set_scrub_prices()
|
self.set_scrub_prices()
|
||||||
|
|
||||||
# logger.info('Setting Entrances.')
|
# Bind entrances to vanilla
|
||||||
# set_entrances(self)
|
|
||||||
# Enforce vanilla for now
|
|
||||||
for region in self.regions:
|
for region in self.regions:
|
||||||
for exit in region.exits:
|
for exit in region.exits:
|
||||||
exit.connect(self.world.get_region(exit.vanilla_connected_region, self.player))
|
exit.connect(self.world.get_region(exit.vanilla_connected_region, self.player))
|
||||||
if self.entrance_shuffle:
|
|
||||||
shuffle_random_entrances(self)
|
|
||||||
|
|
||||||
def create_items(self):
|
def create_items(self):
|
||||||
# Generate itempool
|
# Generate itempool
|
||||||
|
@ -487,6 +485,22 @@ class OOTWorld(World):
|
||||||
self.remove_from_start_inventory.extend(removed_items)
|
self.remove_from_start_inventory.extend(removed_items)
|
||||||
|
|
||||||
def set_rules(self):
|
def set_rules(self):
|
||||||
|
# This has to run AFTER creating items but BEFORE set_entrances_based_rules
|
||||||
|
if self.entrance_shuffle:
|
||||||
|
shuffle_random_entrances(self)
|
||||||
|
all_entrances = self.get_shuffled_entrances()
|
||||||
|
all_entrances.sort(key=lambda x: x.name)
|
||||||
|
all_entrances.sort(key=lambda x: x.type)
|
||||||
|
for loadzone in all_entrances:
|
||||||
|
if loadzone.primary:
|
||||||
|
entrance = loadzone
|
||||||
|
else:
|
||||||
|
entrance = loadzone.reverse
|
||||||
|
if entrance.reverse is not None:
|
||||||
|
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'both', self.player)
|
||||||
|
else:
|
||||||
|
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
|
||||||
|
|
||||||
set_rules(self)
|
set_rules(self)
|
||||||
set_entrances_based_rules(self)
|
set_entrances_based_rules(self)
|
||||||
|
|
||||||
|
@ -512,7 +526,7 @@ class OOTWorld(World):
|
||||||
all_locations = self.get_locations()
|
all_locations = self.get_locations()
|
||||||
reachable = self.world.get_reachable_locations(all_state, self.player)
|
reachable = self.world.get_reachable_locations(all_state, self.player)
|
||||||
unreachable = [loc for loc in all_locations if
|
unreachable = [loc for loc in all_locations if
|
||||||
loc.internal and loc.event and loc.locked and loc not in reachable]
|
(loc.internal or loc.type == 'Drop') and loc.event and loc.locked and loc not in reachable]
|
||||||
for loc in unreachable:
|
for loc in unreachable:
|
||||||
loc.parent_region.locations.remove(loc)
|
loc.parent_region.locations.remove(loc)
|
||||||
# Exception: Sell Big Poe is an event which is only reachable if Bottle with Big Poe is in the item pool.
|
# Exception: Sell Big Poe is an event which is only reachable if Bottle with Big Poe is in the item pool.
|
||||||
|
@ -624,9 +638,27 @@ class OOTWorld(World):
|
||||||
songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.world.itempool))
|
songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.world.itempool))
|
||||||
for song in songs:
|
for song in songs:
|
||||||
self.world.itempool.remove(song)
|
self.world.itempool.remove(song)
|
||||||
|
|
||||||
|
important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or
|
||||||
|
self.warp_songs or self.spawn_positions)
|
||||||
|
song_order = {
|
||||||
|
'Zeldas Lullaby': 1,
|
||||||
|
'Eponas Song': 1,
|
||||||
|
'Sarias Song': 3 if important_warps else 0,
|
||||||
|
'Suns Song': 0,
|
||||||
|
'Song of Time': 0,
|
||||||
|
'Song of Storms': 3,
|
||||||
|
'Minuet of Forest': 2 if important_warps else 0,
|
||||||
|
'Bolero of Fire': 2 if important_warps else 0,
|
||||||
|
'Serenade of Water': 2 if important_warps else 0,
|
||||||
|
'Requiem of Spirit': 2,
|
||||||
|
'Nocturne of Shadow': 2,
|
||||||
|
'Prelude of Light': 2 if important_warps else 0,
|
||||||
|
}
|
||||||
|
songs.sort(key=lambda song: song_order.get(song.name, 0))
|
||||||
|
|
||||||
while tries:
|
while tries:
|
||||||
try:
|
try:
|
||||||
self.world.random.shuffle(songs) # shuffling songs makes it less likely to fail by placing ZL last
|
|
||||||
self.world.random.shuffle(song_locations)
|
self.world.random.shuffle(song_locations)
|
||||||
fill_restrictive(self.world, self.world.get_all_state(False), song_locations[:], songs[:],
|
fill_restrictive(self.world, self.world.get_all_state(False), song_locations[:], songs[:],
|
||||||
True, True)
|
True, True)
|
||||||
|
@ -635,7 +667,7 @@ class OOTWorld(World):
|
||||||
except FillError as e:
|
except FillError as e:
|
||||||
tries -= 1
|
tries -= 1
|
||||||
if tries == 0:
|
if tries == 0:
|
||||||
raise e
|
raise Exception(f"Failed placing songs for player {self.player}. Error cause: {e}")
|
||||||
logger.debug(f"Failed placing songs for player {self.player}. Retries left: {tries}")
|
logger.debug(f"Failed placing songs for player {self.player}. Retries left: {tries}")
|
||||||
# undo what was done
|
# undo what was done
|
||||||
for song in songs:
|
for song in songs:
|
||||||
|
@ -796,6 +828,23 @@ class OOTWorld(World):
|
||||||
autoworld.hint_data_available.set()
|
autoworld.hint_data_available.set()
|
||||||
|
|
||||||
def modify_multidata(self, multidata: dict):
|
def modify_multidata(self, multidata: dict):
|
||||||
|
|
||||||
|
hint_entrances = set()
|
||||||
|
for entrance in entrance_shuffle_table:
|
||||||
|
hint_entrances.add(entrance[1][0])
|
||||||
|
if len(entrance) > 2:
|
||||||
|
hint_entrances.add(entrance[2][0])
|
||||||
|
|
||||||
|
def get_entrance_to_region(region):
|
||||||
|
if region.name == 'Root':
|
||||||
|
return None
|
||||||
|
for entrance in region.entrances:
|
||||||
|
if entrance.name in hint_entrances:
|
||||||
|
return entrance
|
||||||
|
for entrance in region.entrances:
|
||||||
|
return get_entrance_to_region(entrance.parent_region)
|
||||||
|
|
||||||
|
# Remove undesired items from start_inventory
|
||||||
for item_name in self.remove_from_start_inventory:
|
for item_name in self.remove_from_start_inventory:
|
||||||
item_id = self.item_name_to_id.get(item_name, None)
|
item_id = self.item_name_to_id.get(item_name, None)
|
||||||
try:
|
try:
|
||||||
|
@ -803,10 +852,26 @@ class OOTWorld(World):
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.warning(f"Attempted to remove nonexistent item id {item_id} from OoT precollected items ({item_name})")
|
logger.warning(f"Attempted to remove nonexistent item id {item_id} from OoT precollected items ({item_name})")
|
||||||
|
|
||||||
|
# Add ER hint data
|
||||||
|
if self.shuffle_interior_entrances != 'off' or self.shuffle_dungeon_entrances or self.shuffle_grotto_entrances:
|
||||||
|
er_hint_data = {}
|
||||||
|
for region in self.regions:
|
||||||
|
main_entrance = get_entrance_to_region(region)
|
||||||
|
if main_entrance is not None and main_entrance.shuffled:
|
||||||
|
for location in region.locations:
|
||||||
|
if type(location.address) == int:
|
||||||
|
er_hint_data[location.address] = main_entrance.name
|
||||||
|
multidata['er_hint_data'][self.player] = er_hint_data
|
||||||
|
|
||||||
|
|
||||||
# Helper functions
|
# Helper functions
|
||||||
def get_shuffled_entrances(self):
|
def get_shufflable_entrances(self, type=None, only_primary=False):
|
||||||
return [] # later this will return all entrances modified by ER. patching process needs it now though
|
return [entrance for entrance in self.world.get_entrances() if (entrance.player == self.player and
|
||||||
|
(type == None or entrance.type == type) and
|
||||||
|
(not only_primary or entrance.primary))]
|
||||||
|
|
||||||
|
def get_shuffled_entrances(self, type=None, only_primary=False):
|
||||||
|
return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if entrance.shuffled]
|
||||||
|
|
||||||
def get_locations(self):
|
def get_locations(self):
|
||||||
for region in self.regions:
|
for region in self.regions:
|
||||||
|
@ -819,6 +884,9 @@ class OOTWorld(World):
|
||||||
def get_region(self, region):
|
def get_region(self, region):
|
||||||
return self.world.get_region(region, self.player)
|
return self.world.get_region(region, self.player)
|
||||||
|
|
||||||
|
def get_entrance(self, entrance):
|
||||||
|
return self.world.get_entrance(entrance, self.player)
|
||||||
|
|
||||||
def is_major_item(self, item: OOTItem):
|
def is_major_item(self, item: OOTItem):
|
||||||
if item.type == 'Token':
|
if item.type == 'Token':
|
||||||
return self.bridge == 'tokens' or self.lacs_condition == 'tokens'
|
return self.bridge == 'tokens' or self.lacs_condition == 'tokens'
|
||||||
|
|
|
@ -1720,7 +1720,8 @@
|
||||||
"Lake Hylia": "is_child and can_dive",
|
"Lake Hylia": "is_child and can_dive",
|
||||||
"ZD Behind King Zora": "
|
"ZD Behind King Zora": "
|
||||||
Deliver_Letter or zora_fountain == 'open' or
|
Deliver_Letter or zora_fountain == 'open' or
|
||||||
(zora_fountain == 'adult' and is_adult)",
|
(zora_fountain == 'adult' and is_adult) or
|
||||||
|
(logic_king_zora_skip and is_adult)",
|
||||||
"ZD Shop": "is_child or Blue_Fire",
|
"ZD Shop": "is_child or Blue_Fire",
|
||||||
"ZD Storms Grotto": "can_open_storm_grotto"
|
"ZD Storms Grotto": "can_open_storm_grotto"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue