diff --git a/.github/workflows/release-create.yml b/.github/workflows/release-create.yml index d169bb8b..65bdb4b8 100644 --- a/.github/workflows/release-create.yml +++ b/.github/workflows/release-create.yml @@ -2,24 +2,17 @@ name: ⏱️Test/🔨Build/🚀Deploy # fire on -on: [ - push, - pull_request -] - -# on: -# push: -# branches: -# - DoorDevUnstable -# - DoorDev -# - OverworldShuffleDev -# - OverworldShuffle -# pull_request: -# branches: -# - DoorDevUnstable -# - DoorDev -# - OverworldShuffleDev -# - OverworldShuffle +on: + push: + branches: + # - DoorDevUnstable + # - DoorDev + - OverworldShuffle + pull_request: + branches: + # - DoorDevUnstable + # - DoorDev + - OverworldShuffle # stuff to do jobs: @@ -43,7 +36,6 @@ jobs: os-name: [ # ubuntu-latest, # ubuntu-22.04 ubuntu-22.04, - ubuntu-20.04, macos-latest, # macos-12 windows-latest # windows-2022 ] @@ -102,7 +94,6 @@ jobs: os-name: [ # ubuntu-latest, # ubuntu-22.04 ubuntu-22.04, - ubuntu-20.04, macos-latest, # macos-12 windows-latest # windows-2022 ] @@ -178,7 +169,6 @@ jobs: os-name: [ # ubuntu-latest, # ubuntu-22.04 ubuntu-22.04, - ubuntu-20.04, macos-latest, # macos-12 windows-latest # windows-2022 ] @@ -220,7 +210,6 @@ jobs: os-name: [ # ubuntu-latest, # ubuntu-22.04 ubuntu-22.04, - ubuntu-20.04, macos-latest, # macos-12 windows-latest # windows-2022 ] @@ -358,8 +347,10 @@ jobs: echo "MacOS Asset: ${{ steps.identify-macos-asset.outputs.asset_macos }}" echo "Windows Asset: ${{ steps.identify-windows-asset.outputs.asset_windows }}" - # create a release (MASTER) - - name: 📀->🚀Create a Release (MASTER) + # create a release (OverworldShuffle) + # if: contains(github.ref, 'OverworldShuffle') # branch or tag name + # if: contains(github.event.head_commit.message, 'Version bump') # commit message + - name: 📀->🚀Create a Release (OverworldShuffle) id: create_release uses: actions/create-release@v1.1.4 env: @@ -369,11 +360,11 @@ jobs: release_name: ${{ steps.debug_info.outputs.release_name }} body_path: CHANGELOG.md # draft: true - if: contains(github.ref, 'OverworldShuffle') && contains(github.event.head_commit.message, 'Version bump') # branch/tag name and commit message + if: contains(github.event.head_commit.message, "Merge 'OverworldShuffleDev' into OverworldShuffle") # branch/tag name and commit message - # upload linux archive asset (MASTER) + # upload linux archive asset (OverworldShuffle) #TODO: Make sure we're firing on the proper branches - - name: 🔼Upload Linux Archive Asset (MASTER) + - name: 🔼Upload Linux Archive Asset (OverworldShuffle) id: upload-linux-asset uses: actions/upload-release-asset@v1.0.2 env: @@ -383,11 +374,11 @@ jobs: asset_path: ${{ steps.parentDir.outputs.parentDir }}/deploy/linux/${{ steps.identify-linux-asset.outputs.asset_linux }} asset_name: ${{ steps.debug_info.outputs.asset_prefix }}-linux-focal.tar.gz asset_content_type: application/gzip - if: contains(github.ref, 'OverworldShuffle') && contains(github.event.head_commit.message, 'Version bump') # branch/tag name and commit message + if: contains(github.event.head_commit.message, "Merge 'OverworldShuffleDev' into OverworldShuffle") # branch/tag name and commit message - # upload macos archive asset (MASTER) + # upload macos archive asset (OverworldShuffle) #TODO: Make sure we're firing on the proper branches - - name: 🔼Upload MacOS Archive Asset (MASTER) + - name: 🔼Upload MacOS Archive Asset (OverworldShuffle) id: upload-macos-asset uses: actions/upload-release-asset@v1.0.2 env: @@ -397,11 +388,11 @@ jobs: asset_path: ${{ steps.parentDir.outputs.parentDir }}/deploy/macos/${{ steps.identify-macos-asset.outputs.asset_macos }} asset_name: ${{ steps.debug_info.outputs.asset_prefix }}-osx.tar.gz asset_content_type: application/gzip - if: contains(github.ref, 'OverworldShuffle') && contains(github.event.head_commit.message, 'Version bump') # branch/tag name and commit message + if: contains(github.event.head_commit.message, "Merge 'OverworldShuffleDev' into OverworldShuffle") # branch/tag name and commit message - # upload windows archive asset (MASTER) + # upload windows archive asset (OverworldShuffle) #TODO: Make sure we're firing on the proper branches - - name: 🔼Upload Windows Archive Asset (MASTER) + - name: 🔼Upload Windows Archive Asset (OverworldShuffle) id: upload-windows-asset uses: actions/upload-release-asset@v1.0.2 env: @@ -411,4 +402,14 @@ jobs: asset_path: ${{ steps.parentDir.outputs.parentDir }}/deploy/windows/${{ steps.identify-windows-asset.outputs.asset_windows }} asset_name: ${{ steps.debug_info.outputs.asset_prefix }}-windows.zip asset_content_type: application/zip - if: contains(github.ref, 'OverworldShuffle') && contains(github.event.head_commit.message, 'Version bump') # branch/tag name and commit message + if: contains(github.event.head_commit.message, "Merge 'OverworldShuffleDev' into OverworldShuffle") # branch/tag name and commit message + + - name: 🖳Tag Baserom + uses: ./.github/actions/tag-repo + env: + FINE_PAT: ${{ secrets.ALTTPER_TAGGER }} + with: + repository: ${{ github.repository_owner }}/z3randomizer + ref-name: heads/OWMain + github-tag: ${{ github.event.release.tag_name }} + if: contains(github.event.head_commit.message, "Merge 'OverworldShuffleDev' into OverworldShuffle") diff --git a/BaseClasses.py b/BaseClasses.py index c9c90090..6abb3d12 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -642,7 +642,7 @@ class CollectionState(object): queue = deque(old_state.blocked_connections[player].items()) old_state.traverse_world(queue, rrp, bc, player) - if old_state.world.key_logic_algorithm[player] == 'default': + if old_state.world.key_logic_algorithm[player] == 'dangerous': unresolved_events = [x for y in old_state.reachable_regions[player] for x in y.locations if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement) and x not in old_state.locations_checked and x.can_reach(old_state)] @@ -670,7 +670,7 @@ class CollectionState(object): queue = deque(self.blocked_connections[player].items()) self.traverse_world(queue, rrp, bc, player) - if self.world.key_logic_algorithm[player] == 'default': + if self.world.key_logic_algorithm[player] == 'dangerous': unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement) and x not in self.locations_checked and x.can_reach(self)] @@ -2651,7 +2651,7 @@ class Location(object): self.recursion_count = 0 self.staleness_count = 0 self.locked = False - self.real = not prize + self.real = True self.always_allow = lambda item, state: False self.access_rule = lambda state: True self.verbose_rule = None @@ -2789,7 +2789,7 @@ class Item(object): def explore_region(region): explored_regions.append(region.name) for ent in region.entrances: - if ent.parent_region is not None: + if ent.parent_region is not None and ent.spot_type != 'OWG': if ent.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]: return ent elif ent.parent_region.name not in explored_regions: @@ -2989,6 +2989,8 @@ class Spoiler(object): 'shuffleganon': self.world.shuffle_ganon, 'shufflelinks': self.world.shufflelinks, 'shuffletavern': self.world.shuffletavern, + 'skullwoods': self.world.skullwoods, + 'linked_drops': self.world.linked_drops, 'take_any': self.world.take_any, 'overworld_map': self.world.overworld_map, 'door_shuffle': self.world.doorShuffle, @@ -3242,6 +3244,9 @@ class Spoiler(object): if self.metadata['shuffle'][player] != 'vanilla': outfile.write('Shuffle Link\'s House:'.ljust(line_width) + '%s\n' % yn(self.metadata['shufflelinks'][player])) outfile.write('Shuffle Back of Tavern:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffletavern'][player])) + outfile.write('Skull Woods Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['skullwoods'][player]) + if self.metadata['linked_drops'] != "unset": + outfile.write('Linked Drops Override:'.ljust(line_width) + '%s\n' % self.metadata['linked_drops'][player]) outfile.write('Shuffle GT/Ganon:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffleganon'][player])) outfile.write('Pyramid Hole Pre-opened:'.ljust(line_width) + '%s\n' % self.metadata['open_pyramid'][player]) outfile.write('Overworld Map:'.ljust(line_width) + '%s\n' % self.metadata['overworld_map'][player]) @@ -3641,7 +3646,11 @@ keyshuffle_mode = {'none': 0, 'off': 0, 'wild': 1, 'on': 1, 'universal': 2} # byte 14: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo) overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2} trap_door_mode = {'vanilla': 0, 'optional': 1, 'boss': 2, 'oneway': 3} -key_logic_algo = {'default': 0, 'partial': 1, 'strict': 2} +key_logic_algo = {'dangerous': 0, 'partial': 1, 'strict': 2} + +# byte 15: SSDD ???? (skullwoods, linked_drops, 4 free bytes) +skullwoods_mode = {'original': 0, 'restricted': 1, 'loose': 2, 'followlinked': 3} +linked_drops_mode = {'unset': 0, 'linked': 1, 'independent': 2} # sfx_shuffle and other adjust items does not affect settings code @@ -3698,6 +3707,8 @@ class Settings(object): ((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 5 | trap_door_mode[w.trap_door_mode[p]] << 3 | key_logic_algo[w.key_logic_algorithm[p]]), + + (skullwoods_mode[w.skullwoods[p]] << 6 | linked_drops_mode[w.linked_drops[p]] << 4), ]) return base64.b64encode(code, "+-".encode()).decode() @@ -3705,7 +3716,7 @@ class Settings(object): def adjust_args_from_code(code, player, args): settings, p = base64.b64decode(code.encode(), "+-".encode()), player - if len(settings) < 14: + if len(settings) < 15: raise Exception('Provided code is incompatible with this version') if settings[10] != settings_version: raise Exception('Provided code is incompatible with this version') @@ -3788,6 +3799,10 @@ class Settings(object): args.trap_door_mode[p] = r(trap_door_mode)[(settings[14] & 0x18) >> 3] args.key_logic_algorithm[p] = r(key_logic_algo)[settings[14] & 0x07] + if len(settings) > 15: + args.skullwoods[p] = r(skullwoods_mode)[(settings[15] & 0xc0) >> 6] + args.linked_drops[p] = r(linked_drops_mode)[(settings[15] & 0x30) >> 4] + class KeyRuleType(FastEnum): WorstCase = 0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e43a83b..d36646be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.5.0.2 +- \~Merged in DR v1.4.5~ + - ER - SW Shuffle Options + - ER - Linked Drops Options +- Many GFX fixes +- Fixes to MSU/music +- No longer delete smith with prize shuffle +- Fixed screen scroll issue in Zora's Domain +- Fixed HUD when drawing A items on DR menu + ## 0.5.0.1 - Fixed HP refill interrupting boss cutscenes - Fixed incorrect compass counts in Prize Shuffle diff --git a/CLI.py b/CLI.py index dfe2a680..70fae6e2 100644 --- a/CLI.py +++ b/CLI.py @@ -139,6 +139,7 @@ def parse_cli(argv, no_defaults=False): 'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_max_difference', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern', + 'skullwoods', 'linked_drops', 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', @@ -203,6 +204,8 @@ def parse_settings(): "shuffle": "vanilla", "shufflelinks": False, "shuffletavern": True, + 'skullwoods': 'original', + 'linked_drops': 'unset', "overworld_map": "default", "take_any": "none", "pseudoboots": False, diff --git a/DoorShuffle.py b/DoorShuffle.py index 074e71d4..b5a1e1dd 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -2088,7 +2088,7 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_ if actual_chest_keys == 0: dungeon.small_keys = [] else: - dungeon.small_keys = [ItemFactory(dungeon_keys[dungeon_name], player)] * actual_chest_keys + dungeon.small_keys = [ItemFactory(dungeon_keys[dungeon_name], player) for _ in range(actual_chest_keys)] for name, small_list in small_map.items(): used_doors.update(flatten_pair_list(small_list)) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index d5b0dafd..2e539bf1 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1850,10 +1850,8 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s sector.equations = calc_sector_equations(sector) if sector.is_entrance_sector() and not sector.destination_entrance: need_switch = True - for region in sector.get_start_regions(): - if region.crystal_switch: - need_switch = False - break + if sector.c_switch: # this relies on the fact that Mire Fishbone SE cannot be a portal + need_switch = False any_benefit = False for eq in sector.equations: if len(eq.benefit) > 0: @@ -4035,7 +4033,7 @@ def calc_door_equation(door, sector, look_for_entrance, sewers_flag=None): eq = DoorEquation(door) eq.benefit[hook_from_door(door)].append(door) eq.required = True - eq.c_switch = door.crystal == CrystalBarrier.Either + eq.c_switch = sector.c_switch # Big change - not true for mire fishbone, need to verify for others # exceptions for long entrances ??? # if door.name in ['PoD Dark Alley']: eq.entrance_flag = True diff --git a/Fill.py b/Fill.py index 8a917070..d439e0c5 100644 --- a/Fill.py +++ b/Fill.py @@ -95,7 +95,7 @@ def fill_dungeons_restrictive(world, shuffled_locations): continue break else: - raise FillError(f'Unable to place dungeon prizes {", ".join(list(map(lambda d: d.hint_text, prize_locs)))}') + raise FillError(f'Unable to place dungeon prizes: {", ".join(list(map(lambda d: d.name, prizes)))}') random.shuffle(shuffled_locations) fill(all_state_base, others, shuffled_locations) diff --git a/InitialSram.py b/InitialSram.py index f3f6a5d8..001c9f9d 100644 --- a/InitialSram.py +++ b/InitialSram.py @@ -69,6 +69,7 @@ class InitialSram: starting_arrow_cap_upgrades = 30 starting_bombs = 0 starting_arrows = 0 + starting_magic = 0 startingstate = CollectionState(world) @@ -108,10 +109,10 @@ class InitialSram: if startingstate.has('Magic Upgrade (1/4)', player): equip[0x37B] = 2 - equip[0x36E] = 0x80 + starting_magic = 0x80 elif startingstate.has('Magic Upgrade (1/2)', player): equip[0x37B] = 1 - equip[0x36E] = 0x80 + starting_magic = 0x80 if startingstate.has('Return Old Man', player): self._initial_sram_bytes[0x410] |= 0x01 @@ -176,6 +177,7 @@ class InitialSram: arrow_caps = {'Arrow Upgrade (+5)': 5, 'Arrow Upgrade (+10)': 10} bombs = {'Single Bomb': 1, 'Bombs (3)': 3, 'Bombs (10)': 10} arrows = {'Single Arrow': 1, 'Arrows (10)': 10} + magic = {'Big Magic': 0x80, 'Small Magic': 0x10} if item.name in set_table: equip[set_table[item.name][0]] = set_table[item.name][1] @@ -212,13 +214,19 @@ class InitialSram: if item.name != 'Piece of Heart' or equip[0x36B] == 0: equip[0x36C] = min(equip[0x36C] + 0x08, 0xA0) equip[0x36D] = min(equip[0x36D] + 0x08, 0xA0) + elif item.name in magic: + starting_magic += magic[item.name] else: raise RuntimeError(f'Unsupported item in starting equipment: {item.name}') + equip[0x36E] = min(starting_magic, 0x80) equip[0x370] = min(starting_bomb_cap_upgrades, 50) equip[0x371] = min(starting_arrow_cap_upgrades, 70) equip[0x343] = min(starting_bombs, equip[0x370]) equip[0x377] = min(starting_arrows, equip[0x371]) + + if not startingstate.has('Magic Mirror', player) and world.doorShuffle[player] != 'vanilla': + equip[0x353] = 1 # Assertion and copy equip to initial_sram_bytes assert equip[:0x340] == [0] * 0x340 diff --git a/ItemList.py b/ItemList.py index a3cf5438..17b522eb 100644 --- a/ItemList.py +++ b/ItemList.py @@ -11,7 +11,7 @@ from Tables import bonk_prize_lookup from Items import ItemFactory from source.dungeon.EnemyList import add_drop_contents -from source.overworld.EntranceShuffle2 import connect_entrance +from source.overworld.EntranceShuffle2 import exit_ids, door_addresses from source.item.FillUtil import trash_items, pot_items import source.classes.constants as CONST @@ -546,6 +546,27 @@ def set_up_take_anys(world, player, skip_adjustments=False): world.initialize_regions() +def connect_entrance(world, entrancename, exitname, player): + entrance = world.get_entrance(entrancename, player) + # check if we got an entrance or a region to connect to + try: + region = world.get_region(exitname, player) + exit = None + except RuntimeError: + exit = world.get_entrance(exitname, player) + region = exit.parent_region + + # if this was already connected somewhere, remove the backreference + if entrance.connected_region is not None: + entrance.connected_region.entrances.remove(entrance) + + target = exit_ids[exit.name][0] if exit is not None else exit_ids.get(region.name, None) + addresses = door_addresses[entrance.name][0] + + entrance.connect(region, addresses, target) + world.spoiler.set_entrance(entrance.name, exit.name if exit is not None else region.name, 'entrance', player) + + def create_dynamic_shop_locations(world, player): for shop in world.shops[player]: if shop.region.player == player: @@ -1462,6 +1483,7 @@ def make_customizer_pool(world, player): bow_found = next((i for i in pool if i in {'Bow', 'Progressive Bow'}), None) if not bow_found: missing_items.append('Progressive Bow') + missing_items = [i for i in missing_items if all(i != start.name or player != start.player for start in world.precollected_items)] if missing_items: logging.getLogger('').warning(f'The following items are not in the custom item pool {", ".join(missing_items)}') diff --git a/Items.py b/Items.py index 89e07a41..c81b80d7 100644 --- a/Items.py +++ b/Items.py @@ -20,151 +20,151 @@ def ItemFactory(items, player): # Format: Name: (Advancement, Priority, Type, ItemCode, BasePrice, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text) -item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'), - 'Progressive Bow': (True, False, None, 0x64, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), - 'Progressive Bow (Alt)': (True, False, None, 0x65, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), +item_table = {'Bow': (True, False, None, 0x0B, 200, 'Bow!\nJoin the archer class!', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'), + 'Progressive Bow': (True, False, None, 0x64, 150, 'Bow!\nJoin the archer class!', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), + 'Progressive Bow (Alt)': (True, False, None, 0x65, 150, 'Bow!\nJoin the archer class!', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), 'Book of Mudora': (True, False, None, 0x1D, 150, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the Book'), 'Hammer': (True, False, None, 0x09, 250, 'Stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the Hammer'), - 'Hookshot': (True, False, None, 0x0A, 250, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'), - 'Magic Mirror': (True, False, None, 0x1A, 250, 'Isn\'t your\nreflection so\npretty?', 'the face reflector', 'the narcissistic kid', 'your face for sale', 'trades looking-glass', 'narcissistic boy is happy again', 'the Mirror'), - 'Ocarina': (True, False, None, 0x14, 250, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the Flute'), - 'Ocarina (Activated)': (True, False, None, 0x4A, 250, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the Flute'), - 'Pegasus Boots': (True, False, None, 0x4B, 250, 'Gotta go fast!', 'and the sprint shoes', 'the running-man kid', 'sprint shoe for sale', 'shrooms for speed', 'gotta-go-fast boy runs again', 'the Boots'), - 'Power Glove': (True, False, None, 0x1B, 100, 'Now you can\nlift weak\nstuff!', 'and the grey mittens', 'body-building kid', 'lift glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'the Glove'), - 'Cape': (True, False, None, 0x19, 50, 'Wear this to\nbecome\ninvisible!', 'the camouflage cape', 'red riding-hood kid', 'red hood for sale', 'hood from a hood', 'dapper boy hides again', 'the Cape'), - 'Mushroom': (True, False, None, 0x29, 50, 'I\'m a fun guy!\n\nI\'m a funghi!', 'and the legal drugs', 'the drug-dealing kid', 'legal drugs for sale', 'shroom swap', 'shroom boy sells drugs again', 'the Mushroom'), - 'Shovel': (True, False, None, 0x13, 50, 'Can\n You\n Dig it?', 'and the spade', 'archaeologist kid', 'dirt spade for sale', 'can you dig it', 'shovel boy digs again', 'the Shovel'), - 'Lamp': (True, False, None, 0x12, 150, 'Baby, baby,\nbaby.\nLight my way!', 'and the flashlight', 'light-shining kid', 'flashlight for sale', 'fungus for illumination', 'illuminated boy can see again', 'the Lamp'), - 'Magic Powder': (True, False, None, 0x0D, 50, 'You can turn\nanti-faeries\ninto faeries', 'and the magic sack', 'the sack-holding kid', 'magic sack for sale', 'the witch and assistant', 'magic boy plays marbles again', 'the Powder'), - 'Moon Pearl': (True, False, None, 0x1F, 200, ' Bunny Link\n be\n gone!', 'and the jaw breaker', 'fortune-telling kid', 'lunar orb for sale', 'shrooms for moon rock', 'moon boy plays ball again', 'the Moon Pearl'), - 'Cane of Somaria': (True, False, None, 0x15, 250, 'I make blocks\nto hold down\nswitches!', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'cane boy makes blocks again', 'the Red Cane'), - 'Fire Rod': (True, False, None, 0x07, 250, 'I\'m the hot\nrod. I make\nthings burn!', 'and the flamethrower', 'fire-starting kid', 'rage rod for sale', 'fungus for rage-rod', 'firestarter boy burns again', 'the Fire Rod'), - 'Flippers': (True, False, None, 0x1E, 250, 'Fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the Flippers'), - 'Ice Rod': (True, False, None, 0x08, 250, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the Ice Rod'), - 'Titans Mitts': (True, False, None, 0x1C, 200, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the Mitts'), - 'Bombos': (True, False, None, 0x0F, 100, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'), - 'Ether': (True, False, None, 0x10, 100, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'), - 'Quake': (True, False, None, 0x11, 100, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'), - 'Bottle': (True, False, None, 0x16, 50, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a bottle'), - 'Bottle (Red Potion)': (True, False, None, 0x2B, 70, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a bottle'), - 'Bottle (Green Potion)': (True, False, None, 0x2C, 60, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a bottle'), - 'Bottle (Blue Potion)': (True, False, None, 0x2D, 80, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a bottle'), - 'Bottle (Fairy)': (True, False, None, 0x3D, 70, 'Save me and I will revive you', 'and the captive', 'the tingle kid', 'hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a bottle'), - 'Bottle (Bee)': (True, False, None, 0x3C, 50, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bottle'), - 'Bottle (Good Bee)': (True, False, None, 0x48, 60, 'I will sting your foes a whole lot!', 'and the sparkle sting', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has beetor again', 'a bottle'), - 'Master Sword': (True, False, 'Sword', 0x50, 100, 'I beat barries and pigs alike', 'and the master sword', 'sword-wielding kid', 'glow sword for sale', 'fungus for blue slasher', 'sword boy fights again', 'the Master Sword'), - 'Tempered Sword': (True, False, 'Sword', 0x02, 150, 'I stole the\nblacksmith\'s\njob!', 'the tempered sword', 'sword-wielding kid', 'flame sword for sale', 'fungus for red slasher', 'sword boy fights again', 'the Tempered Sword'), - 'Fighter Sword': (True, False, 'Sword', 0x49, 50, 'A pathetic\nsword rests\nhere!', 'the tiny sword', 'sword-wielding kid', 'tiny sword for sale', 'fungus for tiny slasher', 'sword boy fights again', 'the Fighter Sword'), - 'Sword and Shield': (True, False, 'Sword', 0x00, 'An uncle\nsword rests\nhere!', 'the sword and shield', 'sword and shield-wielding kid', 'training set for sale', 'fungus for training set', 'sword and shield boy fights again', 'the small sword and shield'), - 'Golden Sword': (True, False, 'Sword', 0x03, 200, 'The butter\nsword rests\nhere!', 'and the butter sword', 'sword-wielding kid', 'butter for sale', 'cap churned to butter', 'sword boy fights again', 'the Golden Sword'), - 'Progressive Sword': (True, False, 'Sword', 0x5E, 150, 'A better copy\nof your sword\nfor your time', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again', 'a Sword'), - 'Progressive Glove': (True, False, None, 0x61, 150, 'A way to lift\nheavier things', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a Glove'), - 'Silver Arrows': (True, False, None, 0x58, 100, 'Do you fancy\nsilver tipped\narrows?', 'and the ganonsbane', 'ganon-killing kid', 'ganon doom for sale', 'fungus for pork', 'archer boy shines again', 'the Silver Arrows'), - 'Green Pendant': (True, False, 'Prize', 0x37, 250, 'A pendant that\nsome old guy\nwill never see', 'and the green pendant', 'pendant kid', 'pendant for sale', 'fungus for pendant', 'pendant boy dabs again', 'a Prize'), - 'Blue Pendant': (True, False, 'Prize', 0x39, 150, 'A pendant that you\'ll never get', 'and the pendant', 'pendant kid', 'pendant for sale', 'fungus for pendant', 'pendant boy dabs again', 'a Prize'), - 'Red Pendant': (True, False, 'Prize', 0x38, 150, 'A pendant that you\'ll never get', 'and the pendant', 'pendant kid', 'pendant for sale', 'fungus for pendant', 'pendant boy dabs again', 'a Prize'), - 'Triforce': (True, False, None, 0x6A, 777, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'), - 'Power Star': (True, False, None, 0x6B, 100, 'A small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'), - 'Triforce Piece': (True, False, None, 0x6C, 100, 'A small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce piece'), - 'Crystal 1': (True, False, 'Prize', 0xB1, 200, 'A pretty nice,\nshiny crystal', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), - 'Crystal 2': (True, False, 'Prize', 0xB4, 200, 'A pretty nice,\nshiny crystal', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), - 'Crystal 3': (True, False, 'Prize', 0xB6, 200, 'A pretty nice,\nshiny crystal', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), - 'Crystal 4': (True, False, 'Prize', 0xB5, 200, 'A pretty nice,\nshiny crystal', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), - 'Crystal 5': (True, False, 'Prize', 0xB2, 250, 'A crystal that\nunlocks a bomb', 'and the red crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), - 'Crystal 6': (True, False, 'Prize', 0xB0, 250, 'A crystal that\nunlocks a bomb', 'and the red crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), - 'Crystal 7': (True, False, 'Prize', 0xB3, 200, 'A pretty nice,\nshiny crystal', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), - 'Single Arrow': (False, False, None, 0x43, 3, 'A lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'), - 'Arrows (10)': (False, False, None, 0x44, 30, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'ten arrows'), - 'Arrow Upgrade (+10)': (False, False, None, 0x54, 100, 'Increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), - 'Arrow Upgrade (+5)': (False, False, None, 0x53, 100, 'Increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), - 'Single Bomb': (False, False, None, 0x27, 5, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'), - 'Arrows (5)': (False, False, None, 0xD5, 15, 'This will give\nyou five shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'five arrows'), - 'Small Magic': (False, False, None, 0x45, 5, 'A bit of magic', 'and the bit of magic', 'bit-o-magic kid', 'magic bit for sale', 'fungus for magic', 'magic boy conjures again', 'a bit of magic'), - 'Big Magic': (False, False, None, 0xD4, 40, 'A lot of magic', 'and lots of magic', 'lot-o-magic kid', 'magic refill for sale', 'fungus for magic', 'magic boy conjures again', 'a magic refill'), + 'Hookshot': (True, False, None, 0x0A, 250, 'Hookshot!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'), + 'Magic Mirror': (True, False, None, 0x1A, 250, 'Mirror!\nTake some time to\nreflect on this moment!', 'the face reflector', 'the narcissistic kid', 'your face for sale', 'trades looking-glass', 'narcissistic boy is happy again', 'the Mirror'), + 'Ocarina': (True, False, None, 0x14, 250, 'Ocarina!\nA Flute by another name', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the Flute'), + 'Ocarina (Activated)': (True, False, None, 0x4A, 250, 'Ocarina!\nA Flute by another name', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the Flute'), + 'Pegasus Boots': (True, False, None, 0x4B, 250, 'Pegasus Boots!\nGotta go fast!', 'and the sprint shoes', 'the running-man kid', 'sprint shoe for sale', 'shrooms for speed', 'gotta-go-fast boy runs again', 'the Boots'), + 'Power Glove': (True, False, None, 0x1B, 100, 'Glove!\nNow you can\nlift weak stuff!', 'and the grey mittens', 'body-building kid', 'lift glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'the Glove'), + 'Cape': (True, False, None, 0x19, 50, 'Cape!\nInvisbility cloak activate!', 'the camouflage cape', 'red riding-hood kid', 'red hood for sale', 'hood from a hood', 'dapper boy hides again', 'the Cape'), + 'Mushroom': (True, False, None, 0x29, 50, 'Mushroom!\nDon\'t eat it. Find a witch.', 'and the legal drugs', 'the drug-dealing kid', 'legal drugs for sale', 'shroom swap', 'shroom boy sells drugs again', 'the Mushroom'), + 'Shovel': (True, False, None, 0x13, 50, 'Shovel!\nCan you dig it?', 'and the spade', 'archaeologist kid', 'dirt spade for sale', 'can you dig it', 'shovel boy digs again', 'the Shovel'), + 'Lamp': (True, False, None, 0x12, 150, 'Lamp!\nYou can see in the dark,\nand light torches.', 'and the flashlight', 'light-shining kid', 'flashlight for sale', 'fungus for illumination', 'illuminated boy can see again', 'the Lamp'), + 'Magic Powder': (True, False, None, 0x0D, 50, 'Powder!\nSprinkle it on a dancing pickle!', 'and the magic sack', 'the sack-holding kid', 'magic sack for sale', 'the witch and assistant', 'magic boy plays marbles again', 'the Powder'), + 'Moon Pearl': (True, False, None, 0x1F, 200, 'Moon Pearl!\nRabbit Be Gone!', 'and the jaw breaker', 'fortune-telling kid', 'lunar orb for sale', 'shrooms for moon rock', 'moon boy plays ball again', 'the Moon Pearl'), + 'Cane of Somaria': (True, False, None, 0x15, 250, 'Cane of Somaria!\nMake blocks, throw blocks', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'cane boy makes blocks again', 'the Red Cane'), + 'Fire Rod': (True, False, None, 0x07, 250, 'Fire Rod!\nI\'m burning for you!', 'and the flamethrower', 'fire-starting kid', 'rage rod for sale', 'fungus for rage-rod', 'firestarter boy burns again', 'the Fire Rod'), + 'Flippers': (True, False, None, 0x1E, 250, 'Flippers!\nFancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the Flippers'), + 'Ice Rod': (True, False, None, 0x08, 250, 'Ice Rod!\nActivate Freeze Ray!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the Ice Rod'), + 'Titans Mitts': (True, False, None, 0x1C, 200, 'Mitts!\nLift ALL the rocks!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the Mitts'), + 'Bombos': (True, False, None, 0x0F, 100, 'Bombos!\nExplosions! Fire!\nBurn it all!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'), + 'Ether': (True, False, None, 0x10, 100, 'Ether!\nLet\'s cool things down!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'), + 'Quake': (True, False, None, 0x11, 100, 'Quake!\nLet\'s shake the ground!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'), + 'Bottle': (True, False, None, 0x16, 50, 'Bottle!\nStore all manner of things!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a bottle'), + 'Bottle (Red Potion)': (True, False, None, 0x2B, 70, 'Red Potion!\nFill your hearts!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a bottle'), + 'Bottle (Green Potion)': (True, False, None, 0x2C, 60, 'Green Potion!\nRestore your magic!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a bottle'), + 'Bottle (Blue Potion)': (True, False, None, 0x2D, 80, 'Blue Potion!\nHeal and restore!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a bottle'), + 'Bottle (Fairy)': (True, False, None, 0x3D, 70, 'Fairy Bottle!\nI\'ll save your life!', 'and the captive', 'the tingle kid', 'hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a bottle'), + 'Bottle (Bee)': (True, False, None, 0x3C, 50, 'Bee Bottle!\nI\'ll sting your enemies a bit!', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bottle'), + 'Bottle (Good Bee)': (True, False, None, 0x48, 60, 'Good Bee Bottle!\nI\'ll sting your enemies\na whole lot!', 'and the sparkle sting', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has beetor again', 'a bottle'), + 'Master Sword': (True, False, 'Sword', 0x50, 100, 'Master Sword!\nEvil\'s bane!', 'and the master sword', 'sword-wielding kid', 'glow sword for sale', 'fungus for blue slasher', 'sword boy fights again', 'the Master Sword'), + 'Tempered Sword': (True, False, 'Sword', 0x02, 150, 'Tempered Sword!\nMore slashy!', 'the tempered sword', 'sword-wielding kid', 'flame sword for sale', 'fungus for red slasher', 'sword boy fights again', 'the Tempered Sword'), + 'Fighter Sword': (True, False, 'Sword', 0x49, 50, 'Fighter Sword!\nStarter level slashy!', 'the tiny sword', 'sword-wielding kid', 'tiny sword for sale', 'fungus for tiny slasher', 'sword boy fights again', 'the Fighter Sword'), + 'Sword and Shield': (True, False, 'Sword', 0x00, 'Sword and Shield!\nUncle sword ahoy!', 'the sword and shield', 'sword and shield-wielding kid', 'training set for sale', 'fungus for training set', 'sword and shield boy fights again', 'the small sword and shield'), + 'Golden Sword': (True, False, 'Sword', 0x03, 200, 'Golden Sword!\nBest slashy!', 'and the butter sword', 'sword-wielding kid', 'butter for sale', 'cap churned to butter', 'sword boy fights again', 'the Golden Sword'), + 'Progressive Sword': (True, False, 'Sword', 0x5E, 150, 'Sword!\nA better sword for your time!', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again', 'a Sword'), + 'Progressive Glove': (True, False, None, 0x61, 150, 'Glove!\nLift more than you can now!', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a Glove'), + 'Silver Arrows': (True, False, None, 0x58, 100, 'Silver Arrows!\nDefeat Ganon faster!', 'and the ganonsbane', 'ganon-killing kid', 'ganon doom for sale', 'fungus for pork', 'archer boy shines again', 'the Silver Arrows'), + 'Green Pendant': (True, False, 'Prize', 0x37, 250, 'Green Pendant! Too\nbad Sahasrahla\nwill never see it!', 'and the green pendant', 'pendant kid', 'pendant for sale', 'fungus for pendant', 'pendant boy dabs again', 'a Prize'), + 'Blue Pendant': (True, False, 'Prize', 0x39, 150, 'Blue Pendant!\nBut how?!', 'and the pendant', 'pendant kid', 'pendant for sale', 'fungus for pendant', 'pendant boy dabs again', 'a Prize'), + 'Red Pendant': (True, False, 'Prize', 0x38, 150, 'Red Pendant!\nBut how?!', 'and the pendant', 'pendant kid', 'pendant for sale', 'fungus for pendant', 'pendant boy dabs again', 'a Prize'), + 'Triforce': (True, False, None, 0x6A, 777, 'Triforce!\nGrab me to win!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'), + 'Power Star': (True, False, None, 0x6B, 100, 'Power Star!\nA small step\ntowards victory!', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'), + 'Triforce Piece': (True, False, None, 0x6C, 100, 'Triforce Piece!\nA small step\ntowards victory!', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce piece'), + 'Crystal 1': (True, False, 'Prize', 0xB1, 200, 'Crystal 1! It\'s very shiny!', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 2': (True, False, 'Prize', 0xB4, 200, 'Crystal 2! It\'s very shiny!', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 3': (True, False, 'Prize', 0xB6, 200, 'Crystal 3! It\'s very shiny!', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 4': (True, False, 'Prize', 0xB5, 200, 'Crystal 4! It\'s very shiny!', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 5': (True, False, 'Prize', 0xB2, 250, 'Crystal 5! It unlocks a bomb!', 'and the red crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 6': (True, False, 'Prize', 0xB0, 250, 'Crystal 6! It unlocks a bomb!', 'and the red crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 7': (True, False, 'Prize', 0xB3, 200, 'Crystal 7! It\'s very shiny!', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Single Arrow': (False, False, None, 0x43, 3, 'Single Arrow!\nDestiny awaits!', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'), + 'Arrows (10)': (False, False, None, 0x44, 30, '10 Arrows!\nAn archer\'s bailout!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'ten arrows'), + 'Arrow Upgrade (+10)': (False, False, None, 0x54, 100, '10 Arrow Upgrade!\nGain 10 more arrow capacity!', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), + 'Arrow Upgrade (+5)': (False, False, None, 0x53, 100, '5 Arrow Upgrade!\nGain 5 more arrow capacity!', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), + 'Single Bomb': (False, False, None, 0x27, 5, '1 Bomb!\nBoom boom pow!', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'), + 'Arrows (5)': (False, False, None, 0xD5, 15, '5 Arrows!\nAn archer\'s bailout!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'five arrows'), + 'Small Magic': (False, False, None, 0x45, 5, 'Magic Decanter!\nJust a little one', 'and the bit of magic', 'bit-o-magic kid', 'magic bit for sale', 'fungus for magic', 'magic boy conjures again', 'a bit of magic'), + 'Big Magic': (False, False, None, 0xD4, 40, 'Magic Decanter!\nFull refill', 'and lots of magic', 'lot-o-magic kid', 'magic refill for sale', 'fungus for magic', 'magic boy conjures again', 'a magic refill'), 'Chicken': (False, False, None, 0xD3, 5, 'Cucco of Legend', 'and the legendary cucco', 'chicken kid', 'fried chicken for sale', 'fungus for chicken', 'cucco boy clucks again', 'a cucco'), - 'Bombs (3)': (False, False, None, 0x28, 15, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'), - 'Bombs (10)': (False, False, None, 0x31, 50, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'), - 'Bomb Upgrade (+10)': (False, False, None, 0x52, 100, 'Increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), - 'Bomb Upgrade (+5)': (False, False, None, 0x51, 100, 'Increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), - 'Blue Mail': (False, True, None, 0x22, 50, 'Now you\'re a\nblue elf!', 'and the banana hat', 'the protected kid', 'banana hat for sale', 'the clothing store', 'tailor boy banana hatted again', 'the Blue Mail'), - 'Red Mail': (False, True, None, 0x23, 100, 'Now you\'re a\nred elf!', 'and the eggplant hat', 'well-protected kid', 'purple hat for sale', 'the nice clothing store', 'tailor boy fears nothing again', 'the Red Mail'), - 'Progressive Armor': (False, True, None, 0x60, 50, 'Time for a\nchange of\nclothes?', 'and the unknown hat', 'the protected kid', 'new hat for sale', 'the clothing store', 'tailor boy has threads again', 'some armor'), - 'Blue Boomerang': (True, False, None, 0x0C, 50, 'No matter what\nyou do, blue\nreturns to you', 'and the bluemarang', 'the bat-throwing kid', 'bent stick for sale', 'fungus for puma-stick', 'throwing boy plays fetch again', 'the Blue Boomerang'), - 'Red Boomerang': (True, False, None, 0x2A, 50, 'No matter what\nyou do, red\nreturns to you', 'and the badmarang', 'the bat-throwing kid', 'air foil for sale', 'fungus for return-stick', 'magical boy plays fetch again', 'the Red Boomerang'), - 'Blue Shield': (False, True, None, 0x04, 50, 'Now you can\ndefend against\npebbles!', 'and the stone blocker', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a Blue Shield'), - 'Red Shield': (False, True, None, 0x05, 500, 'Now you can\ndefend against\nfireballs!', 'and the shot blocker', 'shield-wielding kid', 'fire shield for sale', 'fungus for fire shield', 'shield boy defends again', 'a Red Shield'), - 'Mirror Shield': (True, False, None, 0x06, 200, 'Now you can\ndefend against\nlasers!', 'and the laser blocker', 'shield-wielding kid', 'face shield for sale', 'fungus for face shield', 'shield boy defends again', 'the Mirror Shield'), - 'Progressive Shield': (True, False, None, 0x5F, 50, 'Have a better\nblocker in\nfront of you', 'and the new shield', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a shield'), - 'Bug Catching Net': (True, False, None, 0x21, 50, 'Let\'s catch\nsome bees and\nfaeries!', 'and the bee catcher', 'the bug-catching kid', 'stick web for sale', 'fungus for butterflies', 'wrong boy catches bees again', 'the Bug Net'), - 'Cane of Byrna': (True, False, None, 0x18, 50, 'Use this to\nbecome\ninvincible!', 'and the bad cane', 'the spark-making kid', 'spark stick for sale', 'spark-stick for trade', 'cane boy encircles again', 'the Blue Cane'), - 'Boss Heart Container': (False, True, None, 0x3E, 40, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'), - 'Sanctuary Heart Container': (False, True, None, 0x3F, 50, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'), - 'Piece of Heart': (False, False, None, 0x17, 10, 'Just a little\npiece of love!', 'and the broken heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart piece'), - 'Rupee (1)': (False, False, None, 0x34, 0, 'Just pocket\nchange. Move\nright along.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'destitute boy has snack again', 'a green rupee'), - 'Rupees (5)': (False, False, None, 0x35, 2, 'Just pocket\nchange. Move\nright along.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'destitute boy has snack again', 'a blue rupee'), - 'Rupees (20)': (False, False, None, 0x36, 10, 'Just couch\ncash. Move\nright along.', 'and the couch cash', 'the piggy-bank kid', 'life lesson for sale', 'the witch buying drugs', 'destitute boy has lunch again', 'a red rupee'), - 'Rupees (50)': (False, False, None, 0x41, 25, 'A rupee pile!\nOkay?', 'and the rupee pile', 'the well-off kid', 'life lesson for sale', 'buying okay drugs', 'destitute boy has dinner again', 'fifty rupees'), - 'Rupees (100)': (False, False, None, 0x40, 50, 'A rupee stash!\nHell yeah!', 'and the rupee stash', 'the kind-of-rich kid', 'life lesson for sale', 'buying good drugs', 'affluent boy goes drinking again', 'one hundred rupees'), - 'Rupees (300)': (False, False, None, 0x46, 150, 'A rupee hoard!\nHell yeah!', 'and the rupee hoard', 'the really-rich kid', 'life lesson for sale', 'buying the best drugs', 'fat-cat boy is rich again', 'three hundred rupees'), - 'Rupoor': (False, False, None, 0x59, 0, 'A debt collector', 'and the toll-booth', 'the toll-booth kid', 'double loss for sale', 'witch stole your rupees', 'affluent boy steals rupees', 'a rupoor'), - 'Red Clock': (False, True, None, 0x5B, 0, 'A waste of time', 'the ruby clock', 'the ruby-time kid', 'red time for sale', 'for ruby time', 'moment boy travels time again', 'a red clock'), - 'Blue Clock': (False, True, None, 0x5C, 50, 'A bit of time', 'the sapphire clock', 'sapphire-time kid', 'blue time for sale', 'for sapphire time', 'moment boy time travels again', 'a blue clock'), - 'Green Clock': (False, True, None, 0x5D, 200, 'A lot of time', 'the emerald clock', 'the emerald-time kid', 'green time for sale', 'for emerald time', 'moment boy adjusts time again', 'a red clock'), + 'Bombs (3)': (False, False, None, 0x28, 15, '3 Bombs!\nExplosions!\nIn a handy 3-pack', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'), + 'Bombs (10)': (False, False, None, 0x31, 50, '10 Bombs!\nExplosions!\nIn a thrifty 10-pack', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'), + 'Bomb Upgrade (+10)': (False, False, None, 0x52, 100, '10 Bomb Upgrade!\nGain 10 more bomb capacity!', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), + 'Bomb Upgrade (+5)': (False, False, None, 0x51, 100, '5 Bomb Upgrade!\nGain 5 more bomb capacity!', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), + 'Blue Mail': (False, True, None, 0x22, 50, 'Blue Mail!\nLess damage activate!', 'and the banana hat', 'the protected kid', 'banana hat for sale', 'the clothing store', 'tailor boy banana hatted again', 'the Blue Mail'), + 'Red Mail': (False, True, None, 0x23, 100, 'Red Mail!\nEven less damage!', 'and the eggplant hat', 'well-protected kid', 'purple hat for sale', 'the nice clothing store', 'tailor boy fears nothing again', 'the Red Mail'), + 'Progressive Armor': (False, True, None, 0x60, 50, 'Upgrade Mail!\nNot the kind you read!', 'and the unknown hat', 'the protected kid', 'new hat for sale', 'the clothing store', 'tailor boy has threads again', 'some armor'), + 'Blue Boomerang': (True, False, None, 0x0C, 50, 'Blue Boomerang!\nAlways comes back!', 'and the bluemarang', 'the bat-throwing kid', 'bent stick for sale', 'fungus for puma-stick', 'throwing boy plays fetch again', 'the Blue Boomerang'), + 'Red Boomerang': (True, False, None, 0x2A, 50, 'Red Boomerang!\nAlways comes back!', 'and the badmarang', 'the bat-throwing kid', 'air foil for sale', 'fungus for return-stick', 'magical boy plays fetch again', 'the Red Boomerang'), + 'Blue Shield': (False, True, None, 0x04, 50, 'Blue Shield!\nBlock rocks!', 'and the stone blocker', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a blue shield'), + 'Red Shield': (False, True, None, 0x05, 500, 'Red Shield!\nBlock fireballs!', 'and the shot blocker', 'shield-wielding kid', 'fire shield for sale', 'fungus for fire shield', 'shield boy defends again', 'the Red Shield'), + 'Mirror Shield': (True, False, None, 0x06, 200, 'Mirror Shield!\nBlock lasers!', 'and the laser blocker', 'shield-wielding kid', 'face shield for sale', 'fungus for face shield', 'shield boy defends again', 'the Mirror Shield'), + 'Progressive Shield': (True, False, None, 0x5F, 50, 'Shield!\nA better shield for your time!', 'and the new shield', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a shield'), + 'Bug Catching Net': (True, False, None, 0x21, 50, 'Bug Net!\nCatch all manner\nof things!', 'and the bee catcher', 'the bug-catching kid', 'stick web for sale', 'fungus for butterflies', 'wrong boy catches bees again', 'the Bug Net'), + 'Cane of Byrna': (True, False, None, 0x18, 50, 'Cane of Byrna!\nSwirly protection!', 'and the bad cane', 'the spark-making kid', 'spark stick for sale', 'spark-stick for trade', 'cane boy encircles again', 'the Blue Cane'), + 'Boss Heart Container': (False, True, None, 0x3E, 40, 'Heart Container!\nHealth Increased!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'), + 'Sanctuary Heart Container': (False, True, None, 0x3F, 50, 'Heart Container!\nHealth Increased!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'), + 'Piece of Heart': (False, False, None, 0x17, 10, 'Heart Piece!\nOne step closer\nto more health!', 'and the broken heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart piece'), + 'Rupee (1)': (False, False, None, 0x34, 0, 'Rupees!\nJust pocket\nchange.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'destitute boy has snack again', 'a green rupee'), + 'Rupees (5)': (False, False, None, 0x35, 2, 'Rupees!\nJust pocket\nchange.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'destitute boy has snack again', 'a blue rupee'), + 'Rupees (20)': (False, False, None, 0x36, 10, 'Rupees!\nJust couch\ncash.', 'and the couch cash', 'the piggy-bank kid', 'life lesson for sale', 'the witch buying drugs', 'destitute boy has lunch again', 'a red rupee'), + 'Rupees (50)': (False, False, None, 0x41, 25, 'Rupees!\nA big pile!', 'and the rupee pile', 'the well-off kid', 'life lesson for sale', 'buying okay drugs', 'destitute boy has dinner again', 'fifty rupees'), + 'Rupees (100)': (False, False, None, 0x40, 50, 'Rupees!\nA big stash!', 'and the rupee stash', 'the kind-of-rich kid', 'life lesson for sale', 'buying good drugs', 'affluent boy goes drinking again', 'one hundred rupees'), + 'Rupees (300)': (False, False, None, 0x46, 150, 'Rupees!\nA big hoard!', 'and the rupee hoard', 'the really-rich kid', 'life lesson for sale', 'buying the best drugs', 'fat-cat boy is rich again', 'three hundred rupees'), + 'Rupoor': (False, False, None, 0x59, 0, 'Rupoor!\nI\'ll take your rupees!', 'and the toll-booth', 'the toll-booth kid', 'double loss for sale', 'witch stole your rupees', 'affluent boy steals rupees', 'a rupoor'), + 'Red Clock': (False, True, None, 0x5B, 0, 'Red Clock!\nA waste of time.', 'the ruby clock', 'the ruby-time kid', 'red time for sale', 'for ruby time', 'moment boy travels time again', 'a red clock'), + 'Blue Clock': (False, True, None, 0x5C, 50, 'Blue Clock!\nA bit of time!', 'the sapphire clock', 'sapphire-time kid', 'blue time for sale', 'for sapphire time', 'moment boy time travels again', 'a blue clock'), + 'Green Clock': (False, True, None, 0x5D, 200, 'Green Clock!\nTons of time!', 'the emerald clock', 'the emerald-time kid', 'green time for sale', 'for emerald time', 'moment boy adjusts time again', 'a red clock'), 'Single RNG': (False, True, None, 0x62, 300, 'Something you don\'t yet have', None, None, None, None, 'unknown boy somethings again', 'a new mystery'), 'Multi RNG': (False, True, None, 0x63, 100, 'Something you may already have', None, None, None, None, 'unknown boy somethings again', 'a total mystery'), - 'Magic Upgrade (1/2)': (True, False, None, 0x4E, 50, 'Your magic\npower has been\ndoubled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Half Magic'), # can be required to beat mothula in an open seed in very very rare circumstance - 'Magic Upgrade (1/4)': (True, False, None, 0x4F, 100, 'Your magic\npower has been\nquadrupled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Quarter Magic'), # can be required to beat mothula in an open seed in very very rare circumstance - 'Small Key (Eastern Palace)': (False, False, 'SmallKey', 0xA2, 40, 'A small key to Armos Knights', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key to Eastern Palace'), - 'Big Key (Eastern Palace)': (False, False, 'BigKey', 0x9D, 60, 'A big key to Armos Knights', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a Big Key to Eastern Palace'), - 'Compass (Eastern Palace)': (False, True, 'Compass', 0x8D, 10, 'Now you can find the Armos Knights!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a Compass to Eastern Palace'), - 'Map (Eastern Palace)': (False, True, 'Map', 0x7D, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a Map to Eastern Palace'), - 'Small Key (Desert Palace)': (False, False, 'SmallKey', 0xA3, 40, 'A small key to the desert', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key to Desert Palace'), - 'Big Key (Desert Palace)': (False, False, 'BigKey', 0x9C, 60, 'A big key to the desert', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a Big Key to Desert Palace'), - 'Compass (Desert Palace)': (False, True, 'Compass', 0x8C, 10, 'Now you can find Lanmolas!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a Compass to Desert Palace'), - 'Map (Desert Palace)': (False, True, 'Map', 0x7C, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a Map to Desert Palace'), - 'Small Key (Tower of Hera)': (False, False, 'SmallKey', 0xAA, 40, 'A small key to Hera', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key to Tower of Hera'), - 'Big Key (Tower of Hera)': (False, False, 'BigKey', 0x95, 60, 'A big key to Hera', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a Big Key to Tower of Hera'), - 'Compass (Tower of Hera)': (False, True, 'Compass', 0x85, 10, 'Now you can find Moldorm!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a Compass to Tower of Hera'), - 'Map (Tower of Hera)': (False, True, 'Map', 0x75, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a Map to Tower of Hera'), - 'Small Key (Escape)': (False, False, 'SmallKey', 0xA0, 40, 'A small key to the castle', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key to Hyrule Castle'), - 'Big Key (Escape)': (False, False, 'BigKey', 0x9F, 60, 'A big key to the castle', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a Big Key to Hyrule Castle'), - 'Compass (Escape)': (False, True, 'Compass', 0x8F, 10, 'Now you can find no boss!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a Compass to Hyrule Castle'), - 'Map (Escape)': (False, True, 'Map', 0x7F, 10, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a Map to Hyrule Castle'), - 'Small Key (Agahnims Tower)': (False, False, 'SmallKey', 0xA4, 40, 'A small key to Agahnim', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key to Castle Tower'), - 'Big Key (Agahnims Tower)': (False, False, 'BigKey', 0x9B, 60, 'A big key to Agahnim', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a Big Key to Castle Tower'), - 'Compass (Agahnims Tower)': (False, True, 'Compass', 0x8B, 10, 'Now you can find Aga1!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a Compass to Castle Tower'), - 'Map (Agahnims Tower)': (False, True, 'Map', 0x7B, 10, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a Map to Castle Tower'), - 'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 40, 'A small key to darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key to Palace of Darkness'), - 'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 60, 'A big key to darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a Big Key to Palace of Darkness'), - 'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 10, 'Now you can find Helmasaur King!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a Compass to Palace of Darkness'), - 'Map (Palace of Darkness)': (False, True, 'Map', 0x79, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a Map to Palace of Darkness'), - 'Small Key (Thieves Town)': (False, False, 'SmallKey', 0xAB, 40, 'A small key to thievery', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key to Thieves\' Town'), - 'Big Key (Thieves Town)': (False, False, 'BigKey', 0x94, 60, 'A big key to thievery', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a Big Key to Thieves\' Town'), - 'Compass (Thieves Town)': (False, True, 'Compass', 0x84, 10, 'Now you can find Blind!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a Compass to Thieves\' Town'), - 'Map (Thieves Town)': (False, True, 'Map', 0x74, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a Map to Thieves\' Town'), - 'Small Key (Skull Woods)': (False, False, 'SmallKey', 0xA8, 40, 'A small key to the woods', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key to Skull Woods'), - 'Big Key (Skull Woods)': (False, False, 'BigKey', 0x97, 60, 'A big key to the woods', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a Big Key to Skull Woods'), - 'Compass (Skull Woods)': (False, True, 'Compass', 0x87, 10, 'Now you can find Mothula!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a Compass to Skull Woods'), - 'Map (Skull Woods)': (False, True, 'Map', 0x77, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a Map to Skull Woods'), - 'Small Key (Swamp Palace)': (False, False, 'SmallKey', 0xA5, 40, 'A small key to the swamp', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key to Swamp Palace'), - 'Big Key (Swamp Palace)': (False, False, 'BigKey', 0x9A, 60, 'A big key to the swamp', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a Big Key to Swamp Palace'), - 'Compass (Swamp Palace)': (False, True, 'Compass', 0x8A, 10, 'Now you can find Arrghus!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a Compass to Swamp Palace'), - 'Map (Swamp Palace)': (False, True, 'Map', 0x7A, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a Map to Swamp Palace'), - 'Small Key (Ice Palace)': (False, False, 'SmallKey', 0xA9, 40, 'A small key to the iceberg', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key to Ice Palace'), - 'Big Key (Ice Palace)': (False, False, 'BigKey', 0x96, 60, 'A big key to the iceberg', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a Big Key to Ice Palace'), - 'Compass (Ice Palace)': (False, True, 'Compass', 0x86, 10, 'Now you can find Kholdstare!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a Compass to Ice Palace'), - 'Map (Ice Palace)': (False, True, 'Map', 0x76, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a Map to Ice Palace'), - 'Small Key (Misery Mire)': (False, False, 'SmallKey', 0xA7, 40, 'A small key to the mire', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key to Misery Mire'), - 'Big Key (Misery Mire)': (False, False, 'BigKey', 0x98, 60, 'A big key to the mire', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a Big Key to Misery Mire'), - 'Compass (Misery Mire)': (False, True, 'Compass', 0x88, 10, 'Now you can find Vitreous!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a Compass to Misery Mire'), - 'Map (Misery Mire)': (False, True, 'Map', 0x78, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a Map to Misery Mire'), - 'Small Key (Turtle Rock)': (False, False, 'SmallKey', 0xAC, 40, 'A small key to the pipe maze', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key to Turtle Rock'), - 'Big Key (Turtle Rock)': (False, False, 'BigKey', 0x93, 60, 'A big key to the pipe maze', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a Big Key to Turtle Rock'), - 'Compass (Turtle Rock)': (False, True, 'Compass', 0x83, 10, 'Now you can find Trinexx!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a Compass to Turtle Rock'), - 'Map (Turtle Rock)': (False, True, 'Map', 0x73, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a Map to Turtle Rock'), - 'Small Key (Ganons Tower)': (False, False, 'SmallKey', 0xAD, 40, 'A small key to the evil tower', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key to Ganon\'s Tower'), - 'Big Key (Ganons Tower)': (False, False, 'BigKey', 0x92, 60, 'A big key to the evil tower', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a Big Key to Ganon\'s Tower'), - 'Compass (Ganons Tower)': (False, True, 'Compass', 0x82, 10, 'Now you can find Agahnim!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a Compass to Ganon\'s Tower'), + 'Magic Upgrade (1/2)': (True, False, None, 0x4E, 50, 'Half Magic!\nDouble your magic power!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Half Magic'), # can be required to beat mothula in an open seed in very very rare circumstance + 'Magic Upgrade (1/4)': (True, False, None, 0x4F, 100, 'Quarter Magic!\nMagic is basically free now!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Quarter Magic'), # can be required to beat mothula in an open seed in very very rare circumstance + 'Small Key (Eastern Palace)': (False, False, 'SmallKey', 0xA2, 40, 'A small key for\nEastern Palace', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Eastern Palace'), + 'Big Key (Eastern Palace)': (False, False, 'BigKey', 0x9D, 60, 'A big key for\nEastern Palace', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Eastern Palace'), + 'Compass (Eastern Palace)': (False, True, 'Compass', 0x8D, 10, 'A compass for\nEastern Palace', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Eastern Palace'), + 'Map (Eastern Palace)': (False, True, 'Map', 0x7D, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Eastern Palace'), + 'Small Key (Desert Palace)': (False, False, 'SmallKey', 0xA3, 40, 'A small key for\nDesert Palace', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Desert Palace'), + 'Big Key (Desert Palace)': (False, False, 'BigKey', 0x9C, 60, 'A big key for\nDesert Palace', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Desert Palace'), + 'Compass (Desert Palace)': (False, True, 'Compass', 0x8C, 10, 'A compass for\nDesert Palace', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Desert Palace'), + 'Map (Desert Palace)': (False, True, 'Map', 0x7C, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Desert Palace'), + 'Small Key (Tower of Hera)': (False, False, 'SmallKey', 0xAA, 40, 'A small key for\nTower of Hera', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Tower of Hera'), + 'Big Key (Tower of Hera)': (False, False, 'BigKey', 0x95, 60, 'A big key for\nTower of Hera', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Tower of Hera'), + 'Compass (Tower of Hera)': (False, True, 'Compass', 0x85, 10, 'A compass for\nTower of Hera', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Tower of Hera'), + 'Map (Tower of Hera)': (False, True, 'Map', 0x75, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Tower of Hera'), + 'Small Key (Escape)': (False, False, 'SmallKey', 0xA0, 40, 'A small key for\nHyrule Castle', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Hyrule Castle'), + 'Big Key (Escape)': (False, False, 'BigKey', 0x9F, 60, 'A big key for\nHyrule Castle', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Hyrule Castle'), + 'Compass (Escape)': (False, True, 'Compass', 0x8F, 10, 'A compass for\nHyrule Castle', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Hyrule Castle'), + 'Map (Escape)': (False, True, 'Map', 0x7F, 10, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Hyrule Castle'), + 'Small Key (Agahnims Tower)': (False, False, 'SmallKey', 0xA4, 40, 'A small key for\nAgahnim\'s Tower', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Castle Tower'), + 'Big Key (Agahnims Tower)': (False, False, 'BigKey', 0x9B, 60, 'A big key for\nAgahnim\'s Tower', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'), + 'Compass (Agahnims Tower)': (False, True, 'Compass', 0x8B, 10, 'A compass for\nAgahnim\'s Tower', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Castle Tower'), + 'Map (Agahnims Tower)': (False, True, 'Map', 0x7B, 10, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Castle Tower'), + 'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 40, 'A small key for\nDark Palace', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'), + 'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 60, 'A big key for\nDark Palace', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'), + 'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 10, 'A compass for\nDark Palace', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'), + 'Map (Palace of Darkness)': (False, True, 'Map', 0x79, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Palace of Darkness'), + 'Small Key (Thieves Town)': (False, False, 'SmallKey', 0xAB, 40, 'A small key for\nThieves Town', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Thieves\' Town'), + 'Big Key (Thieves Town)': (False, False, 'BigKey', 0x94, 60, 'A big key for\nThieves Town', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Thieves\' Town'), + 'Compass (Thieves Town)': (False, True, 'Compass', 0x84, 10, 'A compass for\nThieves Town', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Thieves\' Town'), + 'Map (Thieves Town)': (False, True, 'Map', 0x74, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Thieves\' Town'), + 'Small Key (Skull Woods)': (False, False, 'SmallKey', 0xA8, 40, 'A small key for\nSkull Woods', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Skull Woods'), + 'Big Key (Skull Woods)': (False, False, 'BigKey', 0x97, 60, 'A big key for\nSkull Woods', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Skull Woods'), + 'Compass (Skull Woods)': (False, True, 'Compass', 0x87, 10, 'A compass for\nSkull Woods', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Skull Woods'), + 'Map (Skull Woods)': (False, True, 'Map', 0x77, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Skull Woods'), + 'Small Key (Swamp Palace)': (False, False, 'SmallKey', 0xA5, 40, 'A small key for\nSwamp Palace', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Swamp Palace'), + 'Big Key (Swamp Palace)': (False, False, 'BigKey', 0x9A, 60, 'A big key for\nSwamp Palace', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Swamp Palace'), + 'Compass (Swamp Palace)': (False, True, 'Compass', 0x8A, 10, 'A compass for\nSwamp Palace', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Swamp Palace'), + 'Map (Swamp Palace)': (False, True, 'Map', 0x7A, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Swamp Palace'), + 'Small Key (Ice Palace)': (False, False, 'SmallKey', 0xA9, 40, 'A small key for\nIce Palace', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Ice Palace'), + 'Big Key (Ice Palace)': (False, False, 'BigKey', 0x96, 60, 'A big key for\nIce Palace', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Ice Palace'), + 'Compass (Ice Palace)': (False, True, 'Compass', 0x86, 10, 'A compass for\nIce Palace', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Ice Palace'), + 'Map (Ice Palace)': (False, True, 'Map', 0x76, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Ice Palace'), + 'Small Key (Misery Mire)': (False, False, 'SmallKey', 0xA7, 40, 'A small key for\nMisery Mire', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Misery Mire'), + 'Big Key (Misery Mire)': (False, False, 'BigKey', 0x98, 60, 'A big key for\nMisery Mire', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Misery Mire'), + 'Compass (Misery Mire)': (False, True, 'Compass', 0x88, 10, 'A compass for\nMisery Mire', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Misery Mire'), + 'Map (Misery Mire)': (False, True, 'Map', 0x78, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Misery Mire'), + 'Small Key (Turtle Rock)': (False, False, 'SmallKey', 0xAC, 40, 'A small key for\nTurtle Rock', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Turtle Rock'), + 'Big Key (Turtle Rock)': (False, False, 'BigKey', 0x93, 60, 'A big key for\nTurtle Rock', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Turtle Rock'), + 'Compass (Turtle Rock)': (False, True, 'Compass', 0x83, 10, 'A compass for\nTurtle Rock', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Turtle Rock'), + 'Map (Turtle Rock)': (False, True, 'Map', 0x73, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Turtle Rock'), + 'Small Key (Ganons Tower)': (False, False, 'SmallKey', 0xAD, 40, 'A small key for\nGanon\'s Tower', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Ganon\'s Tower'), + 'Big Key (Ganons Tower)': (False, False, 'BigKey', 0x92, 60, 'A big key for\nGanon\'s Tower', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Ganon\'s Tower'), + 'Compass (Ganons Tower)': (False, True, 'Compass', 0x82, 10, 'A compass for\nGanon\'s Tower', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Ganon\'s Tower'), 'Map (Ganons Tower)': (False, True, 'Map', 0x72, 10, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a Map to Ganon\'s Tower'), 'Small Key (Universal)': (False, True, None, 0xAF, 100, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key'), 'Nothing': (False, False, None, 0x5A, 1, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again', 'nothing'), diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 5f5ba909..a3c008b1 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -2137,11 +2137,11 @@ def validate_key_placement(key_layout, world, player): if key_layout.prize_relevant: found_prize = any(x for x in counter.important_locations if x.prize) if not found_prize and dungeon_table[key_layout.sector.name].prize: - prize_loc = dungeon_table[key_layout.sector.name].prize.location + prize_dungeon = [d for d in world.dungeons if d.name == key_layout.sector.name][0] if key_layout.prize_relevant == 'BigBomb': - found_prize = prize_loc.item.name not in ['Crystal 5', 'Crystal 6'] + found_prize = prize_dungeon.prize.name not in ['Crystal 5', 'Crystal 6'] elif key_layout.prize_relevant == 'GT': - found_prize = 'Crystal' not in prize_loc.item.name or world.crystals_needed_for_gt[player] < 7 + found_prize = 'Crystal' not in prize_dungeon.prize.name or world.crystals_needed_for_gt[player] < 7 else: found_prize = False can_progress = (not counter.big_key_opened and big_found and any(d.bigKey for d in counter.child_doors)) or \ diff --git a/Main.py b/Main.py index e2fcc7d1..7ea58a47 100644 --- a/Main.py +++ b/Main.py @@ -40,7 +40,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.4.1.12' +version_number = '1.4.5' version_branch = '-u' __version__ = f'{version_number}{version_branch}' @@ -161,7 +161,7 @@ def main(args, seed=None, fish=None): if world.spoiler_mode != 'none' and not args.jsonout: logger.info(world.fish.translate("cli", "cli", "create.meta")) world.spoiler.meta_to_file(output_path(f'{outfilebase}_Spoiler.txt')) - if args.mystery and not (args.suppress_meta or args.create_spoiler): + if args.mystery and not (args.suppress_meta or args.spoiler != 'none'): world.spoiler.mystery_meta_to_file(output_path(f'{outfilebase}_meta.txt')) for player in range(1, world.players + 1): @@ -362,7 +362,7 @@ def main(args, seed=None, fish=None): with open(output_path('%s_multidata' % outfilebase), 'wb') as f: f.write(multidata) - if args.mystery and not (args.suppress_meta or args.create_spoiler): + if args.mystery and not (args.suppress_meta or args.spoiler not in ['full']): world.spoiler.hashes_to_file(output_path(f'{outfilebase}_meta.txt')) elif world.spoiler_mode != 'none' and not args.jsonout: world.spoiler.hashes_to_file(output_path(f'{outfilebase}_Spoiler.txt')) @@ -495,6 +495,8 @@ def init_world(args, fish): world.standardize_palettes = args.standardize_palettes.copy() world.shufflelinks = args.shufflelinks.copy() world.shuffletavern = args.shuffletavern.copy() + world.skullwoods = args.skullwoods.copy() + world.linked_drops = args.linked_drops.copy() world.pseudoboots = args.pseudoboots.copy() world.overworld_map = args.overworld_map.copy() world.take_any = args.take_any.copy() @@ -522,7 +524,18 @@ def set_starting_inventory(world, args): if inv_list: for inv_item in inv_list: name = inv_item.strip() - name = name if name != 'Ocarina' or world.flute_mode[player] != 'active' else 'Ocarina (Activated)' + if inv_item == 'RandomWeapon': + name = random.choice(['Progressive Bow', 'Hammer', 'Progressive Sword', 'Cane of Somaria', 'Cane of Byrna', 'Fire Rod']) + extra = [] + if name in ['Cane of Somaria', 'Cane of Byrna', 'Fire Rod']: + extra.append('Big Magic') + if name == 'Progressive Bow': + extra.extend(['Arrows (10)'] * 3) + for e in extra: + item = ItemFactory(e, p) + if item: + world.push_precollected(item) + name = name if name != 'Ocarina' or world.flute_mode[p] != 'active' else 'Ocarina (Activated)' item = ItemFactory(name, p) if item: world.push_precollected(item) diff --git a/MultiClient.py b/MultiClient.py index d28d401f..0d238392 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -344,7 +344,7 @@ location_table_boss = {'Eastern Palace - Prize': 0x2000, 'Desert Palace - Prize': 0x1000, 'Tower of Hera - Prize': 0x0020, 'Palace of Darkness - Prize': 0x0200, - 'Thieves Town - Prize': 0x0010, + 'Thieves\' Town - Prize': 0x0010, 'Skull Woods - Prize': 0x0080, 'Swamp Palace - Prize': 0x0400, 'Ice Palace - Prize': 0x0040, @@ -1097,7 +1097,7 @@ async def game_watcher(ctx : Context): async def main(): parser = argparse.ArgumentParser() - parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.') + parser.add_argument('--snes', default='localhost:23074', help='Address of the QUsb2snes/SNI server.') parser.add_argument('--connect', default=None, help='Address of the multiworld host.') parser.add_argument('--password', default=None, help='Password of the multiworld host.') parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index c7e41fcd..c22cc8b4 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -8,7 +8,7 @@ from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitType from OverworldGlitchRules import create_owg_connections from Utils import bidict -version_number = '0.5.0.1' +version_number = '0.5.0.2' # branch indicator is intentionally different across branches version_branch = '' @@ -1151,7 +1151,7 @@ def define_tile_groups(world, do_grouped, player): merge_groups([[0x13, 0x14, 0x1b]]) # sanctuary and grave connector - if world.shuffle[player] in ['dungeonssimple', 'dungeonsfull', 'simple', 'restricted', 'full', 'lite']: + if world.shuffle[player] in ['dungeonssimple', 'dungeonsfull', 'simple', 'restricted', 'full', 'lite', 'district']: merge_groups([[0x13, 0x14]]) # cross-screen connector @@ -1499,7 +1499,7 @@ def build_accessible_region_list(world, start_region, player, build_copy_world=F elif exit.connected_region.name not in explored_regions \ and (exit.connected_region.type == region.type or exit.name in OWExitTypes['OWEdge'] or (cross_world and exit.name in (OWExitTypes['Portal'] + OWExitTypes['Mirror']))) \ - and (not region_rules or exit.access_rule(blank_state)) and (not ignore_ledges or exit.name not in OWExitTypes['Ledge', 'OWG']): + and (not region_rules or exit.access_rule(blank_state)) and (not ignore_ledges or exit.name not in (OWExitTypes['Ledge'] + OWExitTypes['OWG'])): explore_region(exit.connected_region.name, exit.connected_region) if build_copy_world: diff --git a/Plando.py b/Plando.py index 91780d5d..cc97562f 100755 --- a/Plando.py +++ b/Plando.py @@ -87,7 +87,7 @@ def main(args): outfilebase = 'Plando_%s_%s' % (os.path.splitext(os.path.basename(args.plando))[0], world.seed) rom.write_to_file('%s.sfc' % outfilebase) - if args.create_spoiler: + if args.spoiler != 'none': world.spoiler.to_file('%s_Spoiler.txt' % outfilebase) logger.info('Done. Enjoy.') @@ -178,7 +178,7 @@ def prefill_world(world, plando, text_patches): world.fix_trock_exit = {1: trfstr.strip().lower() == 'true'} elif line.startswith('!fix_gtower_exit'): _, gtfstr = line.split(':', 1) - world.fix_gtower_exit = gtfstr.strip().lower() == 'true' + world.fix_gtower_exit = {1: gtfstr.strip().lower() == 'true'} elif line.startswith('!fix_pod_exit'): _, podestr = line.split(':', 1) world.fix_palaceofdarkness_exit = {1: podestr.strip().lower() == 'true'} @@ -223,7 +223,7 @@ def prefill_world(world, plando, text_patches): def start(): parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument('--create_spoiler', help='Output a Spoiler File', action='store_true') + parser.add_argument('--spoiler', default='none', help='Output a Spoiler File') parser.add_argument('--ignore_unsolvable', help='Do not abort if seed is deemed unsolvable.', action='store_true') parser.add_argument('--rom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.') parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.') diff --git a/Regions.py b/Regions.py index 5d01b959..6dd82146 100644 --- a/Regions.py +++ b/Regions.py @@ -1240,11 +1240,12 @@ def adjust_locations(world, player): index += 1 setup_enemy_locations(world, player) # disable forced prize locations - if world.prizeshuffle[player] != 'none': - for l in [name for name, data in location_table.items() if data[2]]: - location = world.get_location_unsafe(l, player) - if location: - location.prize = False + prize_on_boss = world.prizeshuffle[player] == 'none' + for l in [name for name, data in location_table.items() if data[2]]: + location = world.get_location_unsafe(l, player) + if location: + location.prize = prize_on_boss + location.real = not prize_on_boss # unreal events: for l in ['Ganon', 'Zelda Pickup', 'Zelda Drop Off'] + list(location_events): location = world.get_location_unsafe(l, player) diff --git a/Rom.py b/Rom.py index 271390b4..03ee61cb 100644 --- a/Rom.py +++ b/Rom.py @@ -43,7 +43,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '87be9d9bd56b6ad8e4b9697ecfc31841' +RANDOMIZERBASEHASH = 'c0c4b8166fbe9b637a0aaa82e3f4cb8d' class JsonRom(object): @@ -492,6 +492,9 @@ def patch_rom(world, rom, player, team, is_mystery=False): if world.mapshuffle[player]: rom.write_byte(0x155C9, random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle + if world.doorShuffle[player] != 'vanilla': + rom.write_bytes(snes_to_pc(0x0293FE), [0x80, 0x15]) # skip pre-Aga music change + # fix for swamp drains if necessary swamp1location = world.get_location('Swamp Palace - Trench 1 Pot Key', player) if not swamp1location.pot.indicator: @@ -664,7 +667,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): write_int16(rom, 0x15DB5 + 2 * offset, 0x0640) elif room_id == 0x00d6 and world.fix_trock_exit[player]: write_int16(rom, 0x15DB5 + 2 * offset, 0x0134) - elif room_id == 0x000c and world.fix_gtower_exit: # fix ganons tower exit point + elif room_id == 0x000c and world.fix_gtower_exit[player]: # fix ganons tower exit point write_int16(rom, 0x15DB5 + 2 * offset, 0x00A4) else: write_int16(rom, 0x15DB5 + 2 * offset, link_y) @@ -889,7 +892,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): rom.write_byte(cr_pc+0x1e, 0xEE) # slash rom.write_byte(cr_pc+0x1f, thousands_bot) # modify stat config - stat_address = 0x23978C + stat_address = 0x23983E stat_pc = snes_to_pc(stat_address) rom.write_byte(stat_pc, 0xa9) # change to pos 21 (from b1) rom.write_byte(stat_pc+2, 0xc0) # change to 12 bits (from a0) @@ -2704,6 +2707,8 @@ def patch_shuffled_dark_sanc(world, rom, player): dark_sanc = world.get_region('Dark Sanctuary Hint', player) dark_sanc_entrance = str([i for i in dark_sanc.entrances if i.parent_region.name != 'Menu'][0].name) room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = door_addresses[dark_sanc_entrance][1] + if dark_sanc_entrance == 'Tavern North': + link_y -= 0x10 # rom code assumes south-facing doors and adds $10 to the y-coordinate door_index = door_addresses[str(dark_sanc_entrance)][0] rom.initial_sram.pre_set_overworld_flag(ow_area, door_addresses[dark_sanc_entrance][2]) @@ -2719,6 +2724,8 @@ def patch_shuffled_bomb_shop(world, rom, player): bomb_shop = world.get_region('Big Bomb Shop', player) bomb_shop_entrance = str([i for i in bomb_shop.entrances if i.parent_region.name != 'Menu'][0].name) room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = door_addresses[bomb_shop_entrance][1] + if bomb_shop_entrance == 'Tavern North': + link_y -= 0x10 # rom code assumes south-facing doors and adds $10 to the y-coordinate door_index = door_addresses[str(bomb_shop_entrance)][0] rom.initial_sram.pre_set_overworld_flag(ow_area, door_addresses[bomb_shop_entrance][2]) diff --git a/Rules.py b/Rules.py index 549e3d30..61c918fa 100644 --- a/Rules.py +++ b/Rules.py @@ -557,11 +557,12 @@ def global_rules(world, player): set_rule(world.get_entrance('Mire Post-Gap Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Falling Bridge Hook Path', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Falling Bridge Hook Only Path', player), lambda state: state.has('Hookshot', player)) - set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or - (state.has('Fire Rod', player) and (state.can_use_bombs(player) or state.can_extend_magic(player, 9))) or # 9 fr shots or 8 with some bombs - (state.has('Ice Rod', player) and state.can_use_bombs(player)) or # freeze popo and throw, bomb to finish - state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player)) # need to defeat wizzrobes, bombs don't work ... - # byrna could work with sufficient magic + # Note: new enemy logic doesn't account for Fire Rod + Bombs or Ice Rod + Bombs yet + # set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or + # (state.has('Fire Rod', player) and (state.can_use_bombs(player) or state.can_extend_magic(player, 9))) or # 9 fr shots or 8 with some bombs + # (state.has('Ice Rod', player) and state.can_use_bombs(player)) or # freeze popo and throw, bomb to finish + # state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player)) # need to defeat wizzrobes, bombs don't work ... + # byrna could work with sufficient magic set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) loc = world.get_location('Misery Mire - Spikes Pot Key', player) if loc.pot: @@ -641,10 +642,6 @@ def global_rules(world, player): set_rule(world.get_entrance('GT Ice Armos NE', player), lambda state: world.get_region('GT Ice Armos', player).dungeon.bosses['bottom'].can_defeat(state)) set_rule(world.get_entrance('GT Ice Armos WS', player), lambda state: world.get_region('GT Ice Armos', player).dungeon.bosses['bottom'].can_defeat(state)) - set_rule(world.get_entrance('GT Mimics 1 NW', player), lambda state: state.can_shoot_arrows(player)) - set_rule(world.get_entrance('GT Mimics 1 ES', player), lambda state: state.can_shoot_arrows(player)) - set_rule(world.get_entrance('GT Mimics 2 WS', player), lambda state: state.can_shoot_arrows(player)) - set_rule(world.get_entrance('GT Mimics 2 NE', player), lambda state: state.can_shoot_arrows(player)) # consider access to refill room - interior doors would need a change set_rule(world.get_entrance('GT Cannonball Bridge SE', player), lambda state: state.has_Boots(player)) set_rule(world.get_entrance('GT Lanmolas 2 ES', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state)) @@ -1531,6 +1528,10 @@ std_kill_rooms = { (['GT Petting Zoo SE'], [], 0x7d, [4, 5, 6, 7, 8, 10]), 'GT DMs Room': # Four red stalfos (['GT DMs Room SW'], [], 0x7b, [2, 3, 4, 5, 8, 9, 10]), + 'GT Mimics 1': # two red mimics + (['GT Mimics 1 NW', 'GT Mimics 1 ES'], [], 0x006b, [5, 6, 7, 8, 9]), + 'GT Mimics 2': # two red mimics + (['GT Mimics 2 NE', 'GT Mimics 2 WS'], [], 0x006b, [10, 11, 12, 13]), 'GT Gauntlet 1': # Stalfos/zazaks (['GT Gauntlet 1 WN'], [], 0x5d, [3, 4, 5, 6]), 'GT Gauntlet 2': # Red stalfos @@ -2017,7 +2018,7 @@ def add_key_logic_rules(world, player): eval_func = eval_small_key_door if world.key_logic_algorithm[player] == 'strict' and world.keyshuffle[player] == 'wild': eval_func = eval_small_key_door_strict - elif world.key_logic_algorithm[player] != 'default': + elif world.key_logic_algorithm[player] != 'dangerous': eval_func = eval_small_key_door_partial for d_name, d_logic in key_logic.items(): for door_name, rule in d_logic.door_rules.items(): diff --git a/TestSuite.py b/TestSuite.py index a1b18ab3..ca734581 100644 --- a/TestSuite.py +++ b/TestSuite.py @@ -29,7 +29,7 @@ def main(args=None): ['Std ', ' --mode standard'], ['Inv ', ' --mode inverted']]: - basecommand = f"python3.8 DungeonRandomizer.py --door_shuffle {args.dr} --intensity {args.tense} --suppress_rom --suppress_spoiler" + basecommand = f"python DungeonRandomizer.py --door_shuffle {args.dr} --intensity {args.tense} --suppress_rom --spoiler none" def gen_seed(): taskcommand = basecommand + " " + command + mode[1] diff --git a/TestSuiteRandom.pyw b/TestSuiteRandom.pyw index 965ca1ee..8348709d 100644 --- a/TestSuiteRandom.pyw +++ b/TestSuiteRandom.pyw @@ -25,7 +25,7 @@ def main(args=None): alive = 0 - basecommand = f'py Mystery.py --suppress_rom --suppress_meta --create_spoiler --outputpath L:/_Work/Zelda/ROMs/Bug/Automate/{datetime.now().strftime("%y%m%d")} --weights L:/_Work/Zelda/ROMs/Bug/Automate/_test.yml' + basecommand = f'py Mystery.py --suppress_rom --suppress_meta --spoiler full --outputpath L:/_Work/Zelda/ROMs/Bug/Automate/{datetime.now().strftime("%y%m%d")} --weights L:/_Work/Zelda/ROMs/Bug/Automate/_test.yml' def gen_seed(): return subprocess.run(basecommand, capture_output=True, shell=True, text=True) diff --git a/Text.py b/Text.py index 08756f8a..7518c54d 100644 --- a/Text.py +++ b/Text.py @@ -1668,50 +1668,50 @@ class TextTable(object): text['potion_shop_no_empty_bottles'] = CompressedTextMapper.convert("Whoa, bucko!\nNo empty bottles.") text['item_get_lamp'] = CompressedTextMapper.convert("Lamp! You can see in the dark, and light torches.") text['item_get_boomerang'] = CompressedTextMapper.convert("Boomerang! Press START to select it.") - text['item_get_bow'] = CompressedTextMapper.convert("You're in bow mode now!") - text['item_get_shovel'] = CompressedTextMapper.convert("This is my new mop. My friend George, he gave me this mop. It's a pretty good mop. It's not as good as my old mop. I miss my old mop. But it's still a good mop.") - text['item_get_magic_cape'] = CompressedTextMapper.convert("Finally! we get to play Invisble Man!") - text['item_get_powder'] = CompressedTextMapper.convert("It's the powder. Let's cause some mischief!") - text['item_get_flippers'] = CompressedTextMapper.convert("Splish! Splash! Let's go take a bath!") - text['item_get_power_gloves'] = CompressedTextMapper.convert("Feel the power! You can now lift light rocks! Rock on!") + text['item_get_bow'] = CompressedTextMapper.convert("Bow! Join the archer class!") + text['item_get_shovel'] = CompressedTextMapper.convert("Shovel! Can you dig it?") + text['item_get_magic_cape'] = CompressedTextMapper.convert("Cape! Invisbility cloak activate!") + text['item_get_powder'] = CompressedTextMapper.convert("Powder! Sprinkle it on a dancing pickle!") + text['item_get_flippers'] = CompressedTextMapper.convert("Flippers! Time to swim!") + text['item_get_power_gloves'] = CompressedTextMapper.convert("Gloves! Lift up those rocks!") text['item_get_pendant_courage'] = CompressedTextMapper.convert("We have the Pendant of Courage! How brave!") text['item_get_pendant_power'] = CompressedTextMapper.convert("We have the Pendant of Power! How robust!") text['item_get_pendant_wisdom'] = CompressedTextMapper.convert("We have the Pendant of Wisdom! How astute!") - text['item_get_mushroom'] = CompressedTextMapper.convert("A Mushroom! Don't eat it. Find a witch.") - text['item_get_book'] = CompressedTextMapper.convert("It book! U R now litterit!") - text['item_get_moonpearl'] = CompressedTextMapper.convert("I found a shiny marble! No more hops!") + text['item_get_mushroom'] = CompressedTextMapper.convert("Mushroom! Don't eat it. Find a witch.") + text['item_get_book'] = CompressedTextMapper.convert("Book! Are you well read?") + text['item_get_moonpearl'] = CompressedTextMapper.convert("Moon Pearl! Rabbit Be Gone!") text['item_get_compass'] = CompressedTextMapper.convert("A compass! I can now find the boss.") # 60 text['item_get_map'] = CompressedTextMapper.convert("Yo! You found a MAP! Press X to see it.") - text['item_get_ice_rod'] = CompressedTextMapper.convert("It's the Ice Rod! Freeze Ray time.") - text['item_get_fire_rod'] = CompressedTextMapper.convert("A Rod that shoots fire? Let's burn all the things!") - text['item_get_ether'] = CompressedTextMapper.convert("We can chill out with this!") - text['item_get_bombos'] = CompressedTextMapper.convert("Let's set everything on fire, and melt things!") - text['item_get_quake'] = CompressedTextMapper.convert("Time to make the earth shake, rattle, and roll!") + text['item_get_ice_rod'] = CompressedTextMapper.convert("Ice Rod! Time to chill out!") + text['item_get_fire_rod'] = CompressedTextMapper.convert("Fire Rod! I'm burning for you!") + text['item_get_ether'] = CompressedTextMapper.convert("Ether! Let's cool things down!") + text['item_get_bombos'] = CompressedTextMapper.convert("Bombos! Explosions, fire, burn it all!") + text['item_get_quake'] = CompressedTextMapper.convert("Quake! Let's shake the ground!") text['item_get_hammer'] = CompressedTextMapper.convert("STOP!\n\nHammer Time!") # 66 - text['item_get_ocarina'] = CompressedTextMapper.convert("Finally! We can play the Song of Time!") - text['item_get_cane_of_somaria'] = CompressedTextMapper.convert("Make blocks!\nThrow blocks!\nsplode Blocks!") - text['item_get_hookshot'] = CompressedTextMapper.convert("BOING!!!\nBOING!!!\nSay no more…") + text['item_get_ocarina'] = CompressedTextMapper.convert("Ocarina! A Flute by another name") + text['item_get_cane_of_somaria'] = CompressedTextMapper.convert("Somaria! Make blocks, throw blocks") + text['item_get_hookshot'] = CompressedTextMapper.convert("Hookshot! Grab all the things!") text['item_get_bombs'] = CompressedTextMapper.convert("BOMBS! Use A to pick 'em up, throw 'em, get hurt!") - text['item_get_bottle'] = CompressedTextMapper.convert("It's a terrarium. I hope we find a lizard!") + text['item_get_bottle'] = CompressedTextMapper.convert("Bottle! Store all manner of things") text['item_get_big_key'] = CompressedTextMapper.convert("Yo! You got a Big Key!") - text['item_get_titans_mitts'] = CompressedTextMapper.convert("So, like, you can now lift anything.\nANYTHING!") - text['item_get_magic_mirror'] = CompressedTextMapper.convert("We could stare at this all day or, you know, beat Ganon…") + text['item_get_titans_mitts'] = CompressedTextMapper.convert("Mitts! Lift ALL the rocks!") + text['item_get_magic_mirror'] = CompressedTextMapper.convert("Mirror! Take some time to reflect on this moment!") text['item_get_fake_mastersword'] = CompressedTextMapper.convert("It's the Master Sword! …or not…\n\n FOOL!") # 70 text['post_item_get_mastersword'] = CompressedTextMapper.convert("{NOBORDER}\n{SPEED6}\n@, you got the sword!\n{CHANGEMUSIC}\nNow let's go beat up Agahnim!") - text['item_get_red_potion'] = CompressedTextMapper.convert("Red goo to go! Nice!") - text['item_get_green_potion'] = CompressedTextMapper.convert("Green goo to go! Nice!") - text['item_get_blue_potion'] = CompressedTextMapper.convert("Blue goo to go! Nice!") - text['item_get_bug_net'] = CompressedTextMapper.convert("Surprise Net! Let's catch stuff!") - text['item_get_blue_mail'] = CompressedTextMapper.convert("Blue threads? Less damage activated!") - text['item_get_red_mail'] = CompressedTextMapper.convert("You feel the power of the eggplant on your head.") - text['item_get_temperedsword'] = CompressedTextMapper.convert("Nice… I now have a craving for Cheetos.") - text['item_get_mirror_shield'] = CompressedTextMapper.convert("Pit would be proud!") - text['item_get_cane_of_byrna'] = CompressedTextMapper.convert("It's the Blue Cane. You can now protect yourself with lag!") + text['item_get_red_potion'] = CompressedTextMapper.convert("Red Potion! Heal yourself") + text['item_get_green_potion'] = CompressedTextMapper.convert("Green Potion! Magic refill!") + text['item_get_blue_potion'] = CompressedTextMapper.convert("Blue Potion! Heal and restore!") + text['item_get_bug_net'] = CompressedTextMapper.convert("Bug Net! Let's catch stuff!") + text['item_get_blue_mail'] = CompressedTextMapper.convert("Blue Mail! Less damage activated!") + text['item_get_red_mail'] = CompressedTextMapper.convert("Red Mail! Even less damage!") + text['item_get_temperedsword'] = CompressedTextMapper.convert("Tempered Sword! Even more slashy!") + text['item_get_mirror_shield'] = CompressedTextMapper.convert("Mirror Shield! Time to reflect") + text['item_get_cane_of_byrna'] = CompressedTextMapper.convert("Byrna! Swirly protection!") text['missing_big_key'] = CompressedTextMapper.convert("Something is missing…\nThe Big Key?") text['missing_magic'] = CompressedTextMapper.convert("Something is missing…\nMagic meter?") - text['item_get_pegasus_boots'] = CompressedTextMapper.convert("Finally, it's bonking time!\nHold A to dash") + text['item_get_pegasus_boots'] = CompressedTextMapper.convert("Pegasus Boots! Finally, it's bonking time!\nHold A to dash") text['talking_tree_info_start'] = CompressedTextMapper.convert("Whoa! I can talk again!") text['talking_tree_info_1'] = CompressedTextMapper.convert("Yank on the pitchfork in the center of town, ya heard it here.") text['talking_tree_info_2'] = CompressedTextMapper.convert("Ganon is such a dingus, no one likes him, ya heard it here.") diff --git a/data/base2current.bps b/data/base2current.bps index 8890dd42..8ef0542e 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/mystery_example.yml b/mystery_example.yml index 45d82ecd..79f9815d 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -47,8 +47,8 @@ boss: 0 oneway: 0 key_logic_algorithm: - default: 1 - partial: 0 + dangerous: 0 + partial: 1 strict: 0 decoupledoors: off door_self_loops: diff --git a/mystery_testsuite.yml b/mystery_testsuite.yml index 08110a27..955f6cd6 100644 --- a/mystery_testsuite.yml +++ b/mystery_testsuite.yml @@ -25,7 +25,7 @@ trap_door_mode: boss: 1 oneway: 1 key_logic_algorithm: - default: 1 + dangerous: 1 partial: 0 strict: 0 decoupledoors: @@ -71,6 +71,15 @@ shufflelinks: shuffletavern: on: 1 off: 1 +skullwoods: + original: 1 + restricted: 1 + loose: 1 + followlinked: 1 +linked_drops: + unset: 1 + linked: 1 + independent: 1 world_state: standard: 1 open: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index eef2cccc..7d2de9da 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -254,7 +254,7 @@ }, "key_logic_algorithm": { "choices": [ - "default", + "dangerous", "partial", "strict" ] @@ -516,6 +516,21 @@ "action": "store_true", "type": "bool" }, + "skullwoods": { + "choices": [ + "original", + "restricted", + "loose", + "followlinked" + ] + }, + "linked_drops": { + "choices": [ + "unset", + "linked", + "independent" + ] + }, "overworld_map": { "choices": [ "default", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 80f479e8..0ddb3c48 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -305,7 +305,7 @@ ], "key_logic_algorithm": [ "Key Logic Algorithm (default: %(default)s)", - "default: Balance between safety and randomization", + "dangerous: Key usage must follow logic for safety", "partial: Partial protection when using certain minor glitches", "strict: Ensure small keys are available" ], @@ -431,6 +431,20 @@ "shuffletavern": [ "Include the back of the tavern in the entrance shuffle pool. (default: %(default)s)" ], + "skullwoods": [ + "Select how to shuffle skull woods (default: %(default)s)", + "Original: Skull Woods is shuffled amongst itself", + "Restricted: Drops are vanilla. Entrances stay in skull woods", + "Loose: Drops are vanilla. Entrances go in the main pool", + "Followlinked: If drops are linked, then pinball/left side will be vanilla", + " with other drops paired with an entrance. Otherwise, all go into the main pool" + ], + "linked_drops": [ + "Select how drops are treated in entrance shuffle. (default: %(default)s)", + "Unset: The shuffle mode determines the setting.", + "Linked: Dropdowns will be linked with entrance caves", + "Independent: Dropdowns will not be linked" + ], "overworld_map": [ "Control if and how the overworld map indicators show the locations of dungeons (default: %(default)s)" ], diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index a9541a0b..68fba490 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -92,7 +92,7 @@ "randomizer.dungeon.trap_door_mode.oneway": "Remove All Annoying Traps", "randomizer.dungeon.key_logic_algorithm": "Key Logic Algorithm", - "randomizer.dungeon.key_logic_algorithm.default": "Default", + "randomizer.dungeon.key_logic_algorithm.dangerous": "Dangerous", "randomizer.dungeon.key_logic_algorithm.partial": "Partial Protection", "randomizer.dungeon.key_logic_algorithm.strict": "Strict", @@ -195,6 +195,17 @@ "randomizer.entrance.entranceshuffle.dungeonsfull": "Dungeons + Full", "randomizer.entrance.entranceshuffle.dungeonssimple": "Dungeons + Simple", + "randomizer.entrance.skullwoods": "Skull Woods Shuffle", + "randomizer.entrance.skullwoods.original": "Original", + "randomizer.entrance.skullwoods.restricted": "Vanilla Drops, Entrances Restricted", + "randomizer.entrance.skullwoods.loose": "Vanilla Drops, Entrances use Shuffle", + "randomizer.entrance.skullwoods.followlinked": "Follow Linked Drops Setting", + + "randomizer.entrance.linked_drops": "Linked Drops Override", + "randomizer.entrance.linked_drops.unset": "Determined by Shuffle", + "randomizer.entrance.linked_drops.linked": "Always Linked", + "randomizer.entrance.linked_drops.independent": "Independent", + "randomizer.gameoptions.nobgm": "Disable Music & MSU-1", "randomizer.gameoptions.quickswap": "L/R Quickswapping", "randomizer.gameoptions.reduce_flashing": "Reduce Flashing", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index 268fee35..89addb90 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -14,11 +14,11 @@ }, "key_logic_algorithm": { "type": "selectbox", - "default": "default", + "default": "partial", "options": [ - "default", "partial", - "strict" + "strict", + "dangerous" ], "config": { "padx": [20,0], diff --git a/resources/app/gui/randomize/entrando/widgets.json b/resources/app/gui/randomize/entrando/widgets.json index 809dbac5..61b8609f 100644 --- a/resources/app/gui/randomize/entrando/widgets.json +++ b/resources/app/gui/randomize/entrando/widgets.json @@ -35,6 +35,26 @@ "padx": [20,0] } }, + "skullwoods": { + "type": "selectbox", + "options": [ + "original", + "restricted", + "loose", + "followlinked" + ], + "config": { + "width": 30 + } + }, + "linked_drops": { + "type": "selectbox", + "options": [ + "unset", + "linked", + "independent" + ] + }, "openpyramid": { "type": "selectbox", "options": [ diff --git a/resources/ci/common/common.py b/resources/ci/common/common.py index 5329c768..17b37434 100644 --- a/resources/ci/common/common.py +++ b/resources/ci/common/common.py @@ -19,7 +19,7 @@ global FILESIZE_CHECK # windows: 2022, 2019 # macos: 14, 13, 12, 11 DEFAULT_EVENT = "event" -DEFAULT_REPO_SLUG = "miketrethewey/ALttPDoorRandomizer" +DEFAULT_REPO_SLUG = "codemann8/ALttPDoorRandomizer" FILENAME_CHECKS = [ "DungeonRandomizer", "Gui", diff --git a/resources/ci/common/prepare_release.py b/resources/ci/common/prepare_release.py index 8bab9de4..c4b4bce3 100644 --- a/resources/ci/common/prepare_release.py +++ b/resources/ci/common/prepare_release.py @@ -71,7 +71,7 @@ if len(BUILD_FILENAMES) > 0: # clean the git slate git_clean() - # mv dirs from source code + # mv dirs from source code dirs = [ os.path.join(".",".git"), os.path.join(".",".github"), @@ -98,8 +98,8 @@ if len(BUILD_FILENAMES) > 0: if "linux" in env["OS_NAME"] or "ubuntu" in env["OS_NAME"] or "mac" in env["OS_NAME"] or "osx" in env["OS_NAME"]: os.chmod(os.path.join(".",BUILD_FILENAME),0o755) - # .zip if windows - # .tar.gz otherwise + # .zip if windows + # .tar.gz otherwise if len(BUILD_FILENAMES) > 1: # ZIP_FILENAME = os.path.join("..","deploy",env["REPO_NAME"]) ZIP_FILENAME = os.path.join("..","deploy","ALttPOverworldRandomizer") @@ -112,7 +112,7 @@ if len(BUILD_FILENAMES) > 0: make_archive(ZIP_FILENAME,"gztar") ZIP_FILENAME += ".tar.gz" - # mv dirs back + # mv dirs back for dir in dirs: if os.path.isdir(os.path.join("..","build",dir)): move( diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 33fd8f87..51c852d7 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -160,6 +160,8 @@ class CustomSettings(object): args.bombbag[p] = get_setting(settings['bombbag'], args.bombbag[p]) args.shufflelinks[p] = get_setting(settings['shufflelinks'], args.shufflelinks[p]) args.shuffletavern[p] = get_setting(settings['shuffletavern'], args.shuffletavern[p]) + args.skullwoods[p] = get_setting(settings['skullwoods'], args.skullwoods[p]) + args.linked_drops[p] = get_setting(settings['linked_drops'], args.linked_drops[p]) args.restrict_boss_items[p] = get_setting(settings['restrict_boss_items'], args.restrict_boss_items[p]) args.overworld_map[p] = get_setting(settings['overworld_map'], args.overworld_map[p]) args.pseudoboots[p] = get_setting(settings['pseudoboots'], args.pseudoboots[p]) @@ -335,6 +337,8 @@ class CustomSettings(object): settings_dict[p]['bombbag'] = world.bombbag[p] settings_dict[p]['shufflelinks'] = world.shufflelinks[p] settings_dict[p]['shuffletavern'] = world.shuffletavern[p] + settings_dict[p]['skullwoods'] = world.skullwoods[p] + settings_dict[p]['linked_drops'] = world.linked_drops[p] settings_dict[p]['overworld_map'] = world.overworld_map[p] settings_dict[p]['pseudoboots'] = world.pseudoboots[p] settings_dict[p]['triforce_goal'] = world.treasure_hunt_count[p] diff --git a/source/classes/constants.py b/source/classes/constants.py index 4d170746..14739fb8 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -103,6 +103,8 @@ SETTINGSTOPROCESS = { "shuffleganon": "shuffleganon", "shufflelinks": "shufflelinks", "shuffletavern": "shuffletavern", + "skullwoods": "skullwoods", + "linked_drops": "linked_drops", "openpyramid": "openpyramid", "overworld_map": "overworld_map", }, diff --git a/source/enemizer/EnemyLogic.py b/source/enemizer/EnemyLogic.py index 67f6eb38..df8a21c5 100644 --- a/source/enemizer/EnemyLogic.py +++ b/source/enemizer/EnemyLogic.py @@ -201,8 +201,7 @@ def find_possible_rules(vln_list, used_resources, world, player): flag = flag if flag < 0 else (hits + used_resources['Magic'] * 7 / 8) optional_clears[vln_sub_list].append((byrna_rule(world, player, flag), resources)) elif damage_type == 'FireRod' and hits + used_resources['Magic'] <= 160: - flag = min(vln[damage_type] for vln in vln_list.values()) - flag = flag if flag < 0 else (hits + used_resources['Magic']) + flag = hits + used_resources['Magic'] optional_clears[vln_sub_list].append((fire_rod_rule(world, player, flag), resources)) elif damage_type == 'IceRod' and hits + used_resources['Magic'] <= 160: flag = min(vln[damage_type] for vln in vln_list.values()) diff --git a/source/enemizer/enemy_deny.yaml b/source/enemizer/enemy_deny.yaml index 65a96ab1..04be92fa 100644 --- a/source/enemizer/enemy_deny.yaml +++ b/source/enemizer/enemy_deny.yaml @@ -34,8 +34,9 @@ UwGeneralDeny: - [ 0x001e, 4, [ "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "BigSpike", "Bumper" ] ] #"Ice Palace - Blob Ambush - Red Bari 4" - [ 0x001e, 5, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ice Palace - Blob Ambush - Zol 1" - [ 0x001e, 6, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ice Palace - Blob Ambush - Zol 2" - - [ 0x001f, 0, [ "RollerHorizontalRight" ] ] #"Ice Palace - Big Key View - Pengator 1" + - [0x001f, 0, ["RollerHorizontalRight", "RollerHorizontalLeft"]] #"Ice Palace - Big Key View - Pengator 1" - [ 0x001f, 3, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots + - [0x001f, 4, ["RollerVerticalDown", "RollerVerticalUp"]] # too large, hits the door - [ 0x0021, 3, [ "RollerVerticalDown", "RollerVerticalUp" ] ] #"Sewers - Dark U - Rat 2" - [ 0x0021, 4, [ "RollerVerticalDown", "RollerVerticalUp" ] ] #"Sewers - Dark U - Rat 3" - [ 0x0024, 6, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots @@ -54,10 +55,12 @@ UwGeneralDeny: - [0x0028, 2, ["Raven", "Poe", "GreenZirro", "BlueZirro", "Swamola", "Zora"]] - [0x0028, 3, ["Raven", "Poe", "GreenZirro", "BlueZirro", "Swamola", "Zora"]] - [0x0028, 4, ["Raven", "Poe", "GreenZirro", "BlueZirro", "Swamola", "Zora", "RollerVerticalUp"]] #"Swamp Palace - Entrance Ledge - Spike Trap" - - [ 0x002a, 2, [ "SparkCW", "SparkCCW", "RollerHorizontalRight", "RollerHorizontalLeft", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper"]] #"Palace of Darkness - Arena Main - Hardhat Beetle 1" - - [ 0x002a, 3, [ "Statue", "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "FirebarCW", "FirebarCCW", "SpikeBlock", "Bumper" ] ] #"Palace of Darkness - Arena Main - Hardhat Beetle 2" + - [0x002a, 1, ["RollerVerticalUp", "RollerVerticalDown"]] + - [0x002a, 2, [ "SparkCW", "SparkCCW", "RollerHorizontalRight", "RollerHorizontalLeft", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper", "Chainchomp"]] #"Palace of Darkness - Arena Main - Hardhat Beetle 1" + - [0x002a, 3, ["Statue", "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "FirebarCW", "FirebarCCW", "SpikeBlock", "Bumper", "Chainchomp"]] #"Palace of Darkness - Arena Main - Hardhat Beetle 2" - [ 0x002a, 4, [ "Statue", "Beamos", "AntiFairyCircle", "BigSpike", "Bumper", "RollerHorizontalRight", "RollerHorizontalLeft"]] - [ 0x002a, 6, [ "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Palace of Darkness - Arena Main - Hardhat Beetle 5" + - [0x002a, 7, ["RollerVerticalUp", "RollerVerticalDown", "Chainchomp"]] - [ 0x002b, 5, [ "RollerHorizontalRight" ] ] #"Palace of Darkness - Fairies - Red Bari 2" - [ 0x002e, 0, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "BigSpike", "FirebarCW", "FirebarCCW" ] ] #"Ice Palace - Penguin Chest - Pengator 1" - [ 0x002e, 1, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "BigSpike", "FirebarCW", "FirebarCCW" ] ] #"Ice Palace - Penguin Chest - Pengator 2" @@ -80,6 +83,7 @@ UwGeneralDeny: - [0x0039, 5, ["RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "Bumper"]] #"Skull Woods - Play Pen - Hardhat Beetle" - [ 0x0039, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "FirebarCW", "FirebarCCW" ] ] #"Skull Woods - Play Pen - Spike Trap 2" - [ 0x003b, 1, [ "Bumper" ]] + - [ 0x003b, 4, ["RollerVerticalUp", "RollerVerticalDown"]] - [ 0x003c, 0, ["BigSpike"]] - [ 0x003c, 1, [ "SparkCW", "SparkCCW", "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Hookshot Cave - Blue Bari 1" - [ 0x003c, 2, [ "SparkCW", "SparkCCW", "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Hookshot Cave - Blue Bari 2" @@ -118,7 +122,7 @@ UwGeneralDeny: - [ 0x0049, 5, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Skull Woods - Bari Pits - Gibdo 2" - [ 0x0049, 7, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Skull Woods - Bari Pits - Gibdo 4" - [ 0x0049, 8, [ "Beamos", "AntiFairyCircle", "Bumper" ] ] #"Skull Woods - Bari Pits - Gibdo 5" - - [ 0x004b, 0, [ "Beamos", "AntiFairyCircle", "Bumper" ] ] #"Palace of Darkness - Mimics 1 - Red Eyegore" + - [0x004b, 0, ["Beamos", "AntiFairyCircle", "Bumper", "BigSpike"]] #"Palace of Darkness - Mimics 1 - Red Eyegore" - [ 0x004b, 1, [ "RollerHorizontalRight" ] ] #"Palace of Darkness - Warp Hint - Antifairy 1" - [ 0x004b, 5, [ "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Palace of Darkness - Jelly Hall - Blue Bari 1" - [ 0x004b, 6, [ "AntiFairyCircle", "BigSpike" ] ] #"Palace of Darkness - Jelly Hall - Blue Bari 2" @@ -129,6 +133,7 @@ UwGeneralDeny: - [ 0x0050, 0, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Hyrule Castle - North West Passage - Green Guard" - [ 0x0050, 1, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Hyrule Castle - North West Passage - Green Knife Guard 1" - [ 0x0050, 2, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Hyrule Castle - North West Passage - Green Knife Guard 2" + - [0x0051, 2, ["Zoro"]] # Zoro clips off and doesn't return - [ 0x0052, 0, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "Bumper" ] ] #"Hyrule Castle - North East Passage - Green Guard" - [ 0x0052, 1, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "Bumper" ] ] #"Hyrule Castle - North East Passage - Green Knife Guard 1" - [ 0x0052, 2, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "Bumper" ] ] #"Hyrule Castle - North East Passage - Green Knife Guard 2" @@ -151,7 +156,7 @@ UwGeneralDeny: - [ 0x0057, 13, [ "RollerVerticalDown", "Beamos", "AntiFairyCircle", "Bumper", "Statue", "BigSpike"]] #"Skull Woods - Big Key Room - Blue Bari 1" - [ 0x0057, 14, [ "RollerVerticalDown", "Beamos", "AntiFairyCircle", "Bumper", "Statue", "BigSpike"]] #"Skull Woods - Big Key Room - Blue Bari 2" - [ 0x0058, 0, ["Statue"]] - - [ 0x0058, 1, ["Statue"]] + - [0x0058, 1, ["Statue", "RollerHorizontalRight", "RollerHorizontalLeft"]] - [ 0x0058, 2, ["Statue"]] - [ 0x0058, 3, ["Statue"]] - [ 0x0058, 4, ["Statue"]] @@ -205,7 +210,7 @@ UwGeneralDeny: - [ 0x007b, 7, [ "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ganon's Tower - DMs Room - Hardhat Beetle" - [ 0x007c, 1, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ganon's Tower - Randomizer Room - Fire Bar (Counterclockwise)" - [ 0x007c, 2, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ganon's Tower - Randomizer Room - Spike Trap" - - [ 0x007c, 3, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper", "Statue"]] #"Ganon's Tower - Randomizer Room - Fire Bar (Clockwise)" + - [ 0x007c, 3, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper", "Statue", "SpikeBlock"]] #"Ganon's Tower - Randomizer Room - Fire Bar (Clockwise)" - [ 0x007c, 4, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ganon's Tower - Randomizer Room - Hardhat Beetle" - [ 0x007d, 0, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ganon's Tower - The Zoo - Fire Snake 1" - [ 0x007d, 1, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ganon's Tower - The Zoo - Fire Snake 2" @@ -267,6 +272,7 @@ UwGeneralDeny: - [ 0x009c, 3, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ganon's Tower - Invisible Floor Maze - Hardhat Beetle 3" - [ 0x009c, 4, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ganon's Tower - Invisible Floor Maze - Hardhat Beetle 4" - [ 0x009c, 5, [ "RollerVerticalUp", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ganon's Tower - Invisible Floor Maze - Hardhat Beetle 5" + - [0x009d, 2, ["AntiFairyCircle"]] - [ 0x009d, 3, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ganon's Tower - Compass Room - Gibdo 2" - [ 0x009d, 6, [ "RollerHorizontalLeft", "RollerHorizontalRight" ] ] #"Ganon's Tower - Compass Room - Blue Bari 1" - [ 0x009d, 7, [ "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ganon's Tower - Compass Room - Blue Bari 2" @@ -288,7 +294,7 @@ UwGeneralDeny: - [ 0x00ab, 7, [ "RollerVerticalUp", "RollerHorizontalLeft" ] ] #"Thieves' Town - Spike Dodge - Spike Trap 6" - [ 0x00ae, 0, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ice Palace - Ice T - Blue Bari 1" - [ 0x00ae, 1, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ice Palace - Ice T - Blue Bari 2" - - [ 0x00af, 0, [ "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ice Palace - Ice Clock - Fire Bar (Clockwise)" + - [0x00af, 0, ["RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper", "Lynel"]] #"Ice Palace - Ice Clock - Fire Bar (Clockwise)" - [0x00b0, 7, [ "StalfosKnight", "Blob", "Stal", "Wizzrobe"]] # blocked, but Geldmen are probably okay - [0x00b0, 8, [ "StalfosKnight", "Blob", "Stal", "Wizzrobe"]] # blocked, but Geldmen are probably okay - [ 0x00b1, 2, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Misery Mire - Hourglass - Spike Trap 1" @@ -332,6 +338,7 @@ UwGeneralDeny: - [ 0x00cb, 9, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] - [ 0x00cb, 10, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] - [ 0x00cb, 11, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots + - [0x00cc, 6, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "Bumper"]] # prevent access around - [ 0x00cc, 8, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #Prevents Pot access (Beamos okay?) - [ 0x00cc, 12, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #Prevents Pot access (Beamos okay?) - [ 0x00ce, 0, [ "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "AntiFairyCircle", "Antifairy", "BigSpike", "FirebarCCW", "Bumper", "Chainchomp"]] #"Ice Palace - Over Boss - top - Red Bari 1" @@ -389,7 +396,7 @@ UwGeneralDeny: - [ 0x00e7, 5, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalRight" ] ] #"Death Mountain Descent Cave Right - Keese 6" - [ 0x00e7, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalRight" ] ] #"Death Mountain Descent Cave Right - Keese 7" - [ 0x00e8, 0, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "SpikeBlock", "Bumper" ] ] #"Super Bunny Exit - Hardhat Beetle 1" - - [ 0x00e8, 1, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Super Bunny Exit - Hardhat Beetle 2" + - [ 0x00e8, 2, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Super Bunny Exit - Hardhat Beetle 2" - [ 0x00ee, 0, [ "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Sprial Cave Top - Mini Moldorm 1" - [ 0x00ee, 1, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Sprial Cave Top - Mini Moldorm 2" - [ 0x00ee, 2, [ "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Sprial Cave Top - Mini Moldorm 3" @@ -402,7 +409,7 @@ UwGeneralDeny: - [ 0x00f1, 3, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight" ] ] #"Old Man Maze - Keese 4" - [ 0x00f1, 4, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight" ] ] #"Old Man Maze - Keese 5" - [ 0x00f1, 5, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight" ] ] #"Old Man Maze - Keese 6" - - [ 0x00fd, 0, [ "Bumper" ] ] + - [0x00fd, 0, ["Bumper", "AntiFairyCircle"]] - [ 0x0107, 1, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle"]] - [ 0x0107, 2, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle"]] - [0x010b, 6, ["RollerHorizontalRight"]] @@ -449,6 +456,8 @@ OwGeneralDeny: - [0x5e, 18, ["Gibo"]] # kiki eating Gibo - [0x5e, 19, ["Gibo"]] # kiki eating Gibo - [0x5e, 20, ["Gibo"]] # kiki eating Gibo + - [0x62, 1, ["RollerVerticalUp", "RollerVerticalDown"]] # hard to avoid roller around hammer pegs + - [0x62, 3, ["RollerVerticalUp", "RollerVerticalDown"]] # hard to avoid roller around hammer pegs - [0x77, 1, ["Bumper"]] # soft-lock potential near ladder - [0x7f, 1, ["Bumper"]] # soft-lock potential near ladder UwEnemyDrop: diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 37ea8404..52bbf9d5 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -26,6 +26,7 @@ class EntrancePool(object): self.decoupled_exits = [] self.original_entrances = set() self.original_exits = set() + self.same_world_restricted = {} self.world = world self.player = player @@ -59,7 +60,6 @@ def link_entrances_new(world, player): avail_pool.entrances = set(i_drop_map.keys()).union(i_entrance_map.keys()).union(i_single_ent_map.keys()) avail_pool.exits = set(i_entrance_map.values()).union(i_drop_map.values()).union(i_single_ent_map.values()) avail_pool.inverted = world.mode[player] == 'inverted' - avail_pool.assumed_loose_caves = world.shuffle[player] == 'district' inverted_substitution(avail_pool, avail_pool.entrances, True, True) inverted_substitution(avail_pool, avail_pool.exits, False, True) avail_pool.original_entrances.update(avail_pool.entrances) @@ -99,8 +99,13 @@ def link_entrances_new(world, player): if mode not in modes: raise RuntimeError(f'Shuffle mode {mode} is not yet supported') mode_cfg = copy.deepcopy(modes[mode]) + + if world.linked_drops[player] != 'unset': + mode_cfg['keep_drops_together'] = 'on' if world.linked_drops[player] == 'linked' else 'off' + avail_pool.swapped = mode_cfg['undefined'] == 'swap' avail_pool.keep_drops_together = mode_cfg['keep_drops_together'] == 'on' if 'keep_drops_together' in mode_cfg else True + avail_pool.assumed_loose_caves = not avail_pool.keep_drops_together and world.shuffle[player] == 'district' avail_pool.coupled = mode_cfg['decoupled'] != 'on' if 'decoupled' in mode_cfg else True if avail_pool.is_standard(): do_standard_connections(avail_pool) @@ -108,11 +113,7 @@ def link_entrances_new(world, player): for pool_name, pool in pool_list.items(): special_shuffle = pool['special'] if 'special' in pool else None if special_shuffle == 'drops': - holes, targets = find_entrances_and_targets_drops(avail_pool, pool['entrances']) - if avail_pool.swapped: - connect_swapped(holes, targets, avail_pool) - else: - connect_random(holes, targets, avail_pool) + handle_skull_woods_drops(avail_pool, pool['entrances'], mode_cfg) elif special_shuffle == 'normal_drops': cross_world = mode_cfg['cross_world'] == 'on' if 'cross_world' in mode_cfg else False do_holes_and_linked_drops(set(avail_pool.entrances), set(avail_pool.exits), avail_pool, cross_world) @@ -143,20 +144,7 @@ def link_entrances_new(world, player): entrances, exits = find_entrances_and_exits(avail_pool, entrances+drops) do_main_shuffle(entrances, exits, avail_pool, mode_cfg) elif special_shuffle == 'skull': - entrances, exits = find_entrances_and_exits(avail_pool, pool['entrances']) - rem_ent = None - if avail_pool.world.shuffle[avail_pool.player] in ['dungeonssimple', 'simple', 'restricted'] \ - and not avail_pool.world.is_tile_swapped(0x00, avail_pool.player): - rem_ent = random.choice(['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']) - entrances.remove(rem_ent) - exits.remove('Skull Woods First Section Exit') - connect_random(entrances, exits, avail_pool, True) - entrances, exits = [rem_ent], ['Skull Woods First Section Exit'] - if avail_pool.swapped: - connect_swapped(entrances, exits, avail_pool, True) - else: - connect_random(entrances, exits, avail_pool, True) - avail_pool.skull_handled = True + handle_skull_woods_entrances(avail_pool, pool['entrances']) else: entrances, exits = find_entrances_and_exits(avail_pool, pool['entrances']) do_main_shuffle(entrances, exits, avail_pool, mode_cfg) @@ -253,12 +241,21 @@ def do_main_shuffle(entrances, exits, avail, mode_def): # mandatory exits rem_entrances, rem_exits = set(), set() if not cross_world: + determine_dungeon_restrictions(avail) mand_exits = figure_out_must_exits_same_world(entrances, exits, avail) must_exit_lw, must_exit_dw, lw_entrances, dw_entrances, multi_exit_caves = mand_exits + + def do_world_mandatory(world_entrances, world_must_exits, restriction): + nonlocal multi_exit_caves + candidates = filter_restricted_caves(multi_exit_caves, restriction, avail) + other_candidates = [x for x in multi_exit_caves if x not in candidates] # remember those not passed in + do_mandatory_connections(avail, world_entrances, candidates, world_must_exits) + multi_exit_caves = (other_candidates + candidates) if other_candidates else candidates # rebuild list from the candidates and those not passed + if not avail.inverted: - do_mandatory_connections(avail, lw_entrances, multi_exit_caves, must_exit_lw) + do_world_mandatory(lw_entrances, must_exit_lw, 'LightWorld') else: - do_mandatory_connections(avail, dw_entrances, multi_exit_caves, must_exit_dw) + do_world_mandatory(dw_entrances, must_exit_dw, 'DarkWorld') new_mec = [] for cave_option in multi_exit_caves: @@ -272,9 +269,9 @@ def do_main_shuffle(entrances, exits, avail, mode_def): multi_exit_caves = new_mec if not avail.inverted: - do_mandatory_connections(avail, dw_entrances, multi_exit_caves, must_exit_dw) + do_world_mandatory(dw_entrances, must_exit_dw, 'DarkWorld') else: - do_mandatory_connections(avail, lw_entrances, multi_exit_caves, must_exit_lw) + do_world_mandatory(lw_entrances, must_exit_lw, 'LightWorld') rem_entrances.update(lw_entrances) rem_entrances.update(dw_entrances) else: @@ -367,6 +364,10 @@ def do_main_shuffle(entrances, exits, avail, mode_def): if bonk_fairy_exception(x): lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x) do_same_world_connectors(lw_entrances, dw_entrances, multi_exit_caves, avail) + if avail.world.doorShuffle[avail.player] != 'vanilla': + determine_dungeon_restrictions(avail) + possibles = figure_out_possible_exits(rem_exits) + do_same_world_possible_connectors(lw_entrances, dw_entrances, possibles, avail) unused_entrances.update(lw_entrances) unused_entrances.update(dw_entrances) else: @@ -519,12 +520,16 @@ def do_holes_and_linked_drops(entrances, exits, avail, cross_world): if not avail.keep_drops_together: targets = [avail.one_way_map[x] for x in holes_to_shuffle] - connect_random(holes_to_shuffle, targets, avail) + if avail.swapped: + connect_swapped(holes_to_shuffle, targets, avail) + else: + connect_random(holes_to_shuffle, targets, avail) remove_from_list(entrances, holes_to_shuffle) remove_from_list(exits, targets) return # we're done here hole_entrances, hole_targets = [], [] + leftover_hole_entrances, leftover_hole_targets = [], [] for hole in drop_map: if hole in avail.original_entrances and hole in linked_drop_map: linked_entrance = linked_drop_map[hole] @@ -534,39 +539,44 @@ def do_holes_and_linked_drops(entrances, exits, avail, cross_world): target_drop = avail.one_way_map[hole] if target_exit in exits and target_drop in exits: hole_targets.append((target_exit, target_drop)) + else: + if hole in avail.original_entrances and hole in entrances: + leftover_hole_entrances.append(hole) + if drop_map[hole] in exits: + leftover_hole_targets.append(drop_map[hole]) random.shuffle(hole_entrances) - if not cross_world and 'Sanctuary Grave' in holes_to_shuffle: - hc = avail.world.get_entrance('Hyrule Castle Exit (South)', avail.player) - is_hc_in_dw = avail.world.mode[avail.player] == 'inverted' - if hc.connected_region: - is_hc_in_dw = hc.connected_region.type == RegionType.DarkWorld - chosen_entrance = None - if is_hc_in_dw: - if avail.swapped: - chosen_entrance = next(e for e in hole_entrances if e[0] in DW_Entrances and e[0] != 'Sanctuary') + if not cross_world: + if 'Sanctuary Grave' in holes_to_shuffle: + hc = avail.world.get_entrance('Hyrule Castle Exit (South)', avail.player) + is_hc_in_opp_world = avail.inverted + if hc.connected_region: + opp_world = RegionType.LightWorld if avail.inverted else RegionType.DarkWorld + is_hc_in_opp_world = hc.connected_region.type == opp_world + start_world_entrances = DW_Entrances if avail.inverted else LW_Entrances + opp_world_entrances = LW_Entrances if avail.inverted else DW_Entrances + chosen_entrance = None + if is_hc_in_opp_world: + if avail.swapped: + chosen_entrance = next(e for e in hole_entrances if e[0] in opp_world_entrances and e[0] != 'Sanctuary') + if not chosen_entrance: + chosen_entrance = next((e for e in hole_entrances if e[0] in opp_world_entrances), None) if not chosen_entrance: - chosen_entrance = next(e for e in hole_entrances if e[0] in DW_Entrances) - if not chosen_entrance: - if avail.swapped: - chosen_entrance = next(e for e in hole_entrances if e[0] in LW_Entrances and e[0] != 'Sanctuary') - if not chosen_entrance: - chosen_entrance = next(e for e in hole_entrances if e[0] in LW_Entrances) - if chosen_entrance: - hole_entrances.remove(chosen_entrance) - sanc_interior = next(target for target in hole_targets if target[0] == 'Sanctuary Exit') - hole_targets.remove(sanc_interior) - connect_two_way(chosen_entrance[0], sanc_interior[0], avail) # two-way exit - connect_entrance(chosen_entrance[1], sanc_interior[1], avail) # hole - remove_from_list(entrances, [chosen_entrance[0], chosen_entrance[1]]) - remove_from_list(exits, [sanc_interior[0], sanc_interior[1]]) - if avail.swapped and drop_map[chosen_entrance[1]] != sanc_interior[1]: - swap_ent, swap_ext = connect_swap(chosen_entrance[0], sanc_interior[0], avail) - swap_drop, swap_tgt = connect_swap(chosen_entrance[1], sanc_interior[1], avail) - hole_entrances.remove((swap_ent, swap_drop)) - hole_targets.remove((swap_ext, swap_tgt)) - remove_from_list(entrances, [swap_ent, swap_drop]) - remove_from_list(exits, [swap_ext, swap_tgt]) + if avail.swapped: + chosen_entrance = next(e for e in hole_entrances if e[0] in start_world_entrances and e[0] != 'Sanctuary') + if not chosen_entrance: + chosen_entrance = next(e for e in hole_entrances if e[0] in start_world_entrances) + + if chosen_entrance: + connect_hole_via_interior(chosen_entrance, 'Sanctuary Exit', hole_entrances, hole_targets, entrances, exits, avail) + + sw_world_entrances = DW_Entrances if not avail.world.is_tile_swapped(0x00, avail.player) else LW_Entrances + if 'Skull Woods First Section Hole (North)' in holes_to_shuffle: + chosen_entrance = next(e for e in hole_entrances if e[0] in sw_world_entrances) + connect_hole_via_interior(chosen_entrance, 'Skull Woods First Section Exit', hole_entrances, hole_targets, entrances, exits, avail) + if 'Skull Woods Second Section Hole' in holes_to_shuffle: + chosen_entrance = next(e for e in hole_entrances if e[0] in sw_world_entrances) + connect_hole_via_interior(chosen_entrance, 'Skull Woods Second Section Exit (East)', hole_entrances, hole_targets, entrances, exits, avail) random.shuffle(hole_targets) while len(hole_entrances): @@ -588,6 +598,31 @@ def do_holes_and_linked_drops(entrances, exits, avail, cross_world): remove_from_list(entrances, [swap_ent, swap_drop]) remove_from_list(exits, [swap_ext, swap_tgt]) + if leftover_hole_entrances and leftover_hole_targets: + remove_from_list(entrances, leftover_hole_entrances) + remove_from_list(exits, leftover_hole_targets) + if avail.swapped: + connect_swapped(leftover_hole_entrances, leftover_hole_targets, avail) + else: + connect_random(leftover_hole_entrances, leftover_hole_targets, avail) + + +def connect_hole_via_interior(chosen_entrance, interior, hole_entrances, hole_targets, entrances, exits, avail): + hole_entrances.remove(chosen_entrance) + interior = next(target for target in hole_targets if target[0] == interior) + hole_targets.remove(interior) + connect_two_way(chosen_entrance[0], interior[0], avail) + connect_entrance(chosen_entrance[1], interior[1], avail) + remove_from_list(entrances, [chosen_entrance[0], chosen_entrance[1]]) + remove_from_list(exits, [interior[0], interior[1]]) + if avail.swapped and drop_map[chosen_entrance[1]] != interior[1]: + swap_ent, swap_ext = connect_swap(chosen_entrance[0], interior[0], avail) + swap_drop, swap_tgt = connect_swap(chosen_entrance[1], interior[1], avail) + hole_entrances.remove((swap_ent, swap_drop)) + hole_targets.remove((swap_ext, swap_tgt)) + remove_from_list(entrances, [swap_ent, swap_drop]) + remove_from_list(exits, [swap_ext, swap_tgt]) + def do_dark_sanc(entrances, exits, avail): if avail.world.is_dark_chapel_start(avail.player): @@ -867,14 +902,15 @@ def get_accessible_entrances(start_region, avail, assumed_inventory=[], cross_wo return found_entrances -def figure_out_connectors(exits, avail): +def figure_out_connectors(exits, avail, cross_world=True): multi_exit_caves = [] cave_list = list(Connector_List) - if avail.assumed_loose_caves: - sw_list = ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'] - random.shuffle(sw_list) - cave_list.extend([sw_list]) - cave_list.extend([[entrance_map[e]] for e in linked_drop_map.values() if 'Inverted ' not in e]) + if avail.assumed_loose_caves or (not avail.skull_handled and (cross_world or not avail.world.is_tile_swapped(0x00, avail.player))): + skull_connector = [x for x in ['Skull Woods Second Section Exit (West)', 'Skull Woods Second Section Exit (East)'] if x in exits] + cave_list.extend(skull_connector) + if avail.assumed_loose_caves or not avail.keep_drops_together: + cave_list.extend([[entrance_map[e]] for e in linked_drop_map.values() if 'Inverted ' not in e and 'Skull Woods ' not in e]) + for item in cave_list: if all(x in exits for x in item): remove_from_list(exits, item) @@ -1055,12 +1091,49 @@ def must_exits_helper(avail): return must_exit_lw, must_exit_dw +def figure_out_possible_exits(exits): + possible_multi_exit_caves = [] + for item in doors_possible_connectors: + if item in exits: + remove_from_list(exits, item) + possible_multi_exit_caves.append(item) + return possible_multi_exit_caves + + +def determine_dungeon_restrictions(avail): + check_for_hc = (avail.is_standard() or avail.world.doorShuffle[avail.player] != 'vanilla') + for check in dungeon_restriction_checks: + dungeon_exits, drop_regions = check + if check_for_hc and any('Hyrule Castle' in x for x in dungeon_exits): + avail.same_world_restricted.update({x: 'LightWorld' for x in dungeon_exits}) + else: + restriction = None + for x in dungeon_exits: + ent = avail.world.get_entrance(x, avail.player) + if ent.connected_region: + if ent.connected_region.type == RegionType.LightWorld: + restriction = 'LightWorld' + elif ent.connected_region.type == RegionType.DarkWorld: + restriction = 'DarkWorld' + # Holes only restrict + for x in drop_regions: + region = avail.world.get_region(x, avail.player) + ent = next((ent for ent in region.entrances if ent.parent_region and ent.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]), None) + if ent: + if ent.parent_region.type == RegionType.LightWorld and not avail.inverted: + restriction = 'LightWorld' + elif ent.parent_region.type == RegionType.DarkWorld and avail.inverted: + restriction = 'DarkWorld' + if restriction: + avail.same_world_restricted.update({x: restriction for x in dungeon_exits}) + + def figure_out_must_exits_same_world(entrances, exits, avail): lw_entrances, dw_entrances = [], [] for x in entrances: lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x) - multi_exit_caves = figure_out_connectors(exits, avail) + multi_exit_caves = figure_out_connectors(exits, avail, False) must_exit_lw, must_exit_dw = must_exits_helper(avail) must_exit_lw = must_exit_filter(avail, must_exit_lw, lw_entrances) @@ -1069,8 +1142,26 @@ def figure_out_must_exits_same_world(entrances, exits, avail): return must_exit_lw, must_exit_dw, lw_entrances, dw_entrances, multi_exit_caves +def filter_restricted_caves(multi_exit_caves, restriction, avail): + candidates = [] + for cave in multi_exit_caves: + if all(x not in avail.same_world_restricted or avail.same_world_restricted[x] == restriction for x in cave): + candidates.append(cave) + return candidates + + +def flatten(list_to_flatten): + ret = [] + for item in list_to_flatten: + if isinstance(item, tuple): + ret.extend(item) + else: + ret.append(item) + return ret + + def figure_out_must_exits_cross_world(entrances, exits, avail): - multi_exit_caves = figure_out_connectors(exits, avail) + multi_exit_caves = figure_out_connectors(exits, avail, True) must_exit_lw, must_exit_dw = must_exits_helper(avail) must_exit = must_exit_filter(avail, must_exit_lw + must_exit_dw, entrances) @@ -1083,7 +1174,7 @@ def do_same_world_connectors(lw_entrances, dw_entrances, caves, avail): random.shuffle(dw_entrances) random.shuffle(caves) while caves: - # connect highest exit count caves first, prevent issue where we have 2 or 3 exits across worlds left to fill + # connect highest-exit-count caves first, prevent issue where we have 2 or 3 exits across worlds left to fill cave_candidate = (None, 0) for i, cave in enumerate(caves): if isinstance(cave, str): @@ -1092,12 +1183,19 @@ def do_same_world_connectors(lw_entrances, dw_entrances, caves, avail): cave_candidate = (i, len(cave)) cave = caves.pop(cave_candidate[0]) - target = lw_entrances if random.randint(0, 1) == 0 else dw_entrances if isinstance(cave, str): cave = (cave,) + target, restriction = None, None + if any(x in avail.same_world_restricted for x in cave): + restriction = next(avail.same_world_restricted[x] for x in cave if x in avail.same_world_restricted) + target = lw_entrances if restriction == 'LightWorld' else dw_entrances + if target is None: + target = lw_entrances if random.randint(0, 1) == 0 else dw_entrances # check if we can still fit the cave into our target group if len(target) < len(cave): + if restriction: + raise Exception('Not enough entrances for restricted cave, algorithm needs revision (main)') # need to use other set target = lw_entrances if target is dw_entrances else dw_entrances @@ -1111,6 +1209,18 @@ def do_same_world_connectors(lw_entrances, dw_entrances, caves, avail): connect_two_way(target.pop(), ext, avail) +def do_same_world_possible_connectors(lw_entrances, dw_entrances, possibles, avail): + random.shuffle(possibles) + while possibles: + possible = possibles.pop() + target = None + if possible in avail.same_world_restricted: + target = lw_entrances if avail.same_world_restricted[possible] == 'LightWorld' else dw_entrances + if target is None: + target = lw_entrances if random.randint(0, 1) == 0 else dw_entrances + connect_two_way(target.pop(), possible, avail) + determine_dungeon_restrictions(avail) + def do_cross_world_connectors(entrances, caves, avail): random.shuffle(entrances) random.shuffle(caves) @@ -1158,6 +1268,44 @@ def do_cross_world_connectors(entrances, caves, avail): break +def handle_skull_woods_drops(avail, pool, mode_cfg): + skull_woods = avail.world.skullwoods[avail.player] + if skull_woods in ['restricted', 'loose']: + for drop in pool: + target = drop_map[drop] + connect_entrance(drop, target, avail) + elif skull_woods == 'original': + holes, targets = find_entrances_and_targets_drops(avail, pool) + if avail.swapped: + connect_swapped(holes, targets, avail) + else: + connect_random(holes, targets, avail) + elif skull_woods == 'followlinked': + keep_together = mode_cfg['keep_drops_together'] == 'on' if 'keep_drops_together' in mode_cfg else True + if keep_together: + for drop in ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)']: + target = drop_map[drop] + connect_entrance(drop, target, avail) + + +def handle_skull_woods_entrances(avail, pool): + skull_woods = avail.world.skullwoods[avail.player] + if skull_woods in ['restricted', 'original']: + entrances, exits = find_entrances_and_exits(avail, pool) + if avail.world.shuffle[avail.player] in ['dungeonssimple', 'simple', 'restricted'] \ + and not avail.world.is_tile_swapped(0x00, avail.player): + rem_ent = random.choice(['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']) + entrances.remove(rem_ent) + exits.remove('Skull Woods First Section Exit') + connect_random(entrances, exits, avail, True) + entrances, exits = [rem_ent], ['Skull Woods First Section Exit'] + if avail.swapped: + connect_swapped(entrances, exits, avail, True) + else: + connect_random(entrances, exits, avail, True) + avail.skull_handled = True + + def do_fixed_shuffle(avail, entrance_list): max_size = 0 options = {} @@ -1214,7 +1362,6 @@ def do_same_world_shuffle(avail, pool_def): multi_exit = pool_def['connectors'] # complete_entrance_set = set() lw_entrances, dw_entrances, multi_exits_caves, other_exits = [], [], [], [] - hyrule_forced = None single_entrances, single_exits = find_entrances_and_exits(avail, single_exit) other_exits.extend(single_exits) @@ -1224,11 +1371,8 @@ def do_same_world_shuffle(avail, pool_def): for option in multi_exit: multi_entrances, multi_exits = find_entrances_and_exits(avail, option) # complete_entrance_set.update(multi_entrances) - if avail.is_sanc_forced_in_hc() and any(x in multi_entrances for x in ['Hyrule Castle Entrance (South)', - 'Hyrule Castle Entrance (East)', - 'Hyrule Castle Entrance (West)']): - hyrule_forced = [multi_exits] - multi_exits_caves.append(multi_exits) + if multi_exits: + multi_exits_caves.append(multi_exits) for x in multi_entrances: (dw_entrances, lw_entrances)[x in LW_Entrances].append(x) @@ -1236,13 +1380,16 @@ def do_same_world_shuffle(avail, pool_def): must_exit_lw = must_exit_filter(avail, must_exit_lw, lw_entrances) must_exit_dw = must_exit_filter(avail, must_exit_dw, dw_entrances) - do_mandatory_connections(avail, lw_entrances, multi_exits_caves, must_exit_lw) - if hyrule_forced and hyrule_forced[0] in multi_exits_caves: - remove_from_list(multi_exits_caves, hyrule_forced) - do_mandatory_connections(avail, dw_entrances, multi_exits_caves, must_exit_dw) - multi_exits_caves.append(hyrule_forced[0]) - else: - do_mandatory_connections(avail, dw_entrances, multi_exits_caves, must_exit_dw) + def do_world_mandatory(world_entrances, world_must_exits, restriction): + nonlocal multi_exits_caves + candidates = filter_restricted_caves(multi_exits_caves, restriction, avail) + other_candidates = [x for x in multi_exits_caves if x not in candidates] # remember those not passed in + do_mandatory_connections(avail, world_entrances, candidates, world_must_exits) + multi_exits_caves = (other_candidates + candidates) if other_candidates else candidates # rebuild list from the candidates and those not passed + + determine_dungeon_restrictions(avail) + do_world_mandatory(lw_entrances, must_exit_lw, 'LightWorld') + do_world_mandatory(dw_entrances, must_exit_dw, 'DarkWorld') # connect caves random.shuffle(lw_entrances) @@ -1250,19 +1397,20 @@ def do_same_world_shuffle(avail, pool_def): random.shuffle(multi_exits_caves) while multi_exits_caves: cave_candidate = (None, 0) - if hyrule_forced and hyrule_forced[0] in multi_exits_caves: - multi_exits_caves.remove(hyrule_forced[0]) - cave = hyrule_forced[0] - hyrule_forced = None - target = lw_entrances - else: - for i, cave in enumerate(multi_exits_caves): - if len(cave) > cave_candidate[1]: - cave_candidate = (i, len(cave)) - cave = multi_exits_caves.pop(cave_candidate[0]) - target = lw_entrances if random.randint(0, 1) == 0 else dw_entrances + for i, cave in enumerate(multi_exits_caves): + if len(cave) > cave_candidate[1]: + cave_candidate = (i, len(cave)) + cave = multi_exits_caves.pop(cave_candidate[0]) + target, restriction = None, None + if any(x in avail.same_world_restricted for x in cave): + restriction = next(avail.same_world_restricted[x] for x in cave if x in avail.same_world_restricted) + target = lw_entrances if restriction == 'LightWorld' else dw_entrances + if target is None: + target = lw_entrances if random.randint(0, 1) == 0 else dw_entrances if len(target) < len(cave): # swap because we ran out of entrances in that world + if restriction: + raise Exception('Not enough entrances for restricted cave, algorithm needs revision (dungeonsfull)') target = lw_entrances if target is dw_entrances else dw_entrances for ext in cave: @@ -1670,7 +1818,7 @@ def connect_swap(entrance, exit, avail): swap_entrance = next(e for e, x in avail.combine_map.items() if x == exit) if swap_entrance in ['Pyramid Entrance', 'Pyramid Hole'] and avail.world.is_tile_swapped(0x1b, avail.player): swap_entrance = 'Inverted ' + swap_entrance - if entrance in entrance_map: + if swap_exit in entrance_map.values(): connect_two_way(swap_entrance, swap_exit, avail) else: connect_entrance(swap_entrance, swap_exit, avail) @@ -1816,6 +1964,12 @@ modes = { 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] }, + 'skull_layout': { + 'special': 'vanilla', + 'condition': '', + 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)'] + }, 'single_entrance_dungeon': { 'entrances': ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace', 'Ganons Tower'] @@ -1849,13 +2003,15 @@ modes = { 'sanc_flag': 'light_world', # always light world flag 'entrances': ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Agahnims Tower', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace', - 'Ganons Tower'], + 'Ganons Tower', 'Desert Palace Entrance (North)', 'Dark Death Mountain Ledge (East)'], 'connectors': [['Hyrule Castle Entrance (South)', 'Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)'], ['Desert Palace Entrance (South)', 'Desert Palace Entrance (East)', - 'Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'], + 'Desert Palace Entrance (West)'], ['Turtle Rock', 'Turtle Rock Isolated Ledge Entrance', - 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)']] + 'Dark Death Mountain Ledge (West)'], + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', + 'Skull Woods First Section Door']] }, } }, @@ -1878,7 +2034,8 @@ modes = { 'special': 'normal_drops', 'entrances': ['Hyrule Castle Secret Entrance Drop', 'Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', - 'Sanctuary Grave', 'Pyramid Hole'] + 'Sanctuary Grave', 'Pyramid Hole', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] }, 'fixed_non_items': { 'special': 'vanilla', @@ -1990,7 +2147,8 @@ modes = { 'special': 'normal_drops', 'entrances': ['Hyrule Castle Secret Entrance Drop', 'Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', - 'Sanctuary Grave', 'Pyramid Hole'] + 'Sanctuary Grave', 'Pyramid Hole', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] }, 'fixed_non_items': { 'special': 'vanilla', @@ -2081,6 +2239,12 @@ modes = { 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] }, + 'skull_layout': { + 'special': 'vanilla', + 'condition': '', + 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)'] + }, 'single_entrance_dungeon': { 'entrances': ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace', 'Ganons Tower'] @@ -2156,6 +2320,12 @@ modes = { 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] }, + 'skull_layout': { + 'special': 'vanilla', + 'condition': '', + 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)'] + }, 'single_entrance_dungeon': { 'entrances': ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace', 'Ganons Tower'] @@ -2408,11 +2578,14 @@ linked_drop_map = { 'Lumberjack Tree Tree': 'Lumberjack Tree Cave', 'Sanctuary Grave': 'Sanctuary', 'Pyramid Hole': 'Pyramid Entrance', - 'Inverted Pyramid Hole': 'Inverted Pyramid Entrance' + 'Inverted Pyramid Hole': 'Inverted Pyramid Entrance', + + 'Skull Woods First Section Hole (North)': 'Skull Woods First Section Door', + 'Skull Woods Second Section Hole': 'Skull Woods Second Section Door (East)', } sw_linked_drop_map = { - 'Skull Woods Second Section Hole': 'Skull Woods Second Section Door (West)', + 'Skull Woods Second Section Hole': 'Skull Woods Second Section Door (East)', 'Skull Woods First Section Hole (North)': 'Skull Woods First Section Door', 'Skull Woods First Section Hole (West)': 'Skull Woods First Section Door', 'Skull Woods First Section Hole (East)': 'Skull Woods First Section Door' @@ -2490,7 +2663,6 @@ entrance_map = { 'Paradox Cave (Top)': 'Paradox Cave Exit (Top)' } - single_entrance_map = { 'Mimic Cave': 'Mimic Cave', 'Dark Death Mountain Fairy': 'Dark Death Mountain Healer Fairy', 'Dark Death Mountain Shop': 'Dark Death Mountain Shop', 'Spike Cave': 'Spike Cave', @@ -2601,6 +2773,19 @@ Dungeon_Exit_Set = { 'Turtle Rock Exit (Front)', 'Turtle Rock Isolated Ledge Exit', 'Turtle Rock Ledge Exit (West)' } +dungeon_restriction_checks = [ + (['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Sanctuary Exit'], ['Sewer Drop']), + (['Desert Palace Exit (South)', 'Desert Palace Exit (East)', 'Desert Palace Exit (West)', 'Desert Palace Exit (North)'], []), + (['Turtle Rock Exit (Front)', 'Turtle Rock Isolated Ledge Exit', 'Turtle Rock Ledge Exit (West)', 'Turtle Rock Ledge Exit (East)'], []), + (['Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)', 'Skull Woods Final Section Exit'], + ['Skull Pinball', 'Skull Left Drop', 'Skull Pot Circle', 'Skull Back Drop']) + ] + +doors_possible_connectors = [ + 'Sanctuary Exit', 'Desert Palace Exit (North)', 'Skull Woods First Section Exit', + 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)', 'Skull Woods Final Section Exit' +] + # Entrances that cannot be used to access a must_exit entrance - symmetrical to allow reverse lookups Must_Exit_Invalid_Connections = defaultdict(set) diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 77f6dd2e..e7da45df 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -173,6 +173,8 @@ def roll_settings(weights): ret.shuffleganon = get_choice_bool('shuffleganon') ret.shufflelinks = get_choice_bool('shufflelinks') ret.shuffletavern = get_choice_bool('shuffletavern') + ret.skullwoods = get_choice('skullwoods') + ret.linked_drops = get_choice('linked_drops') ret.crystals_gt = get_choice('tower_open') ret.crystals_ganon = get_choice('ganon_open') diff --git a/test/NewTestSuite.py b/test/NewTestSuite.py index cb4739e0..c5beba4f 100644 --- a/test/NewTestSuite.py +++ b/test/NewTestSuite.py @@ -136,7 +136,7 @@ if __name__ == "__main__": error[2] = error[2].split("\n") results["errors"].append(error) - with open("new-test-suite-success.txt", "w") as stream: + with open(os.path.join(LOGPATH, "new-test-suite-success.txt"), 'w') as stream: stream.write(str.join("\n", successes)) results["success"] = successes diff --git a/test/suite/default_key_logic.yaml b/test/suite/default_key_logic.yaml index e9f3e94b..39b5e43c 100644 --- a/test/suite/default_key_logic.yaml +++ b/test/suite/default_key_logic.yaml @@ -6,7 +6,7 @@ meta: players: 1 settings: 1: - key_logic_algorithm: default + key_logic_algorithm: dangerous keysanity: True crystals_needed_for_gt: 0 # to skip trash fill placements: