From 0faeeeb45fb7d76e095af0d5ea61985980fd9b95 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 19 Apr 2024 14:29:26 -0600 Subject: [PATCH 1/6] fix(enemizer): bonk fairies and standard tutorial guard fixed --- source/enemizer/Enemizer.py | 2 +- source/enemizer/OwEnemyList.py | 1 + source/rom/DataTables.py | 6 ++++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/source/enemizer/Enemizer.py b/source/enemizer/Enemizer.py index 65009fdd..021b7f03 100644 --- a/source/enemizer/Enemizer.py +++ b/source/enemizer/Enemizer.py @@ -260,7 +260,7 @@ def get_randomize_able_sprites_ow(area_id, data_tables): req = data_tables.sprite_requirements[key] if isinstance(req, dict): continue - if not req.static and req.can_randomize: + if not req.static and req.can_randomize and not sprite.static: sprite_table[idx] = sprite return sprite_table diff --git a/source/enemizer/OwEnemyList.py b/source/enemizer/OwEnemyList.py index ffbf79d4..3ca0f717 100644 --- a/source/enemizer/OwEnemyList.py +++ b/source/enemizer/OwEnemyList.py @@ -27,6 +27,7 @@ def init_vanilla_sprites_ow(): create_sprite(0x21b, EnemySprite.GreenKnifeGuard, 0x20, 0x1A, '', 0x09CB51) create_sprite(0x21b, EnemySprite.TutorialGuard, 0x2D, 0x25, '', 0x09CB54) create_sprite(0x21b, EnemySprite.TutorialGuard, 0x20, 0x29, '', 0x09CB57) + create_sprite(0x21b, EnemySprite.TutorialGuard, 0x3C, 0x2A, '', 0x09CB5B) create_sprite(0x21d, EnemySprite.Apple, 0x0B, 0x06, '', 0x09CB5B) create_sprite(0x22b, EnemySprite.TutorialGuard, 0x09, 0x1E, '', 0x09CB5F) create_sprite(0x22b, EnemySprite.TutorialGuard, 0x0B, 0x1E, '', 0x09CB62) diff --git a/source/rom/DataTables.py b/source/rom/DataTables.py index 9713ef80..72a1156d 100644 --- a/source/rom/DataTables.py +++ b/source/rom/DataTables.py @@ -134,9 +134,11 @@ class DataTables: # calculate how big this table is going to be? # bytes = sum(1+len(x)*3 for x in self.ow_enemy_table.values() if len(x) > 0)+1 # ending_byte = 0x09CB3B + bytes - max_per_state = {0: 0x40, 1: 0x90, 2: 0x8D} # dropped max on state 2 to steal space for a couple extra sprites (Murahdahla) + max_per_state = {0: 0x40, 1: 0x90, 2: 0x8B} # dropped max on state 2 to steal space for a couple extra sprites (Murahdahla, extra tutorial guard) + pointer_address = snes_to_pc(0x09C881) - data_pointer = snes_to_pc(0x09CB3B) # was originally 0x09CB41 - stealing space for a couple extra sprites (Murahdahla) + # currently borrowed 10 bytes, used 9 (2xMurah + TutorialGuard) + data_pointer = snes_to_pc(0x09CB38) # was originally 0x09CB41 - stealing space for a couple extra sprites (Murahdahla, extra tutorial guard) empty_pointer = pc_to_snes(data_pointer) & 0xFFFF rom.write_byte(data_pointer, 0xff) cached_dark_world = {} From 8461503c850a5a6552e0566c87967b1e84260bbc Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 22 Apr 2024 08:33:59 -0600 Subject: [PATCH 2/6] fix(customizer): Fixed an issue with customizing prize packs --- Rom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index d5106162..95327080 100644 --- a/Rom.py +++ b/Rom.py @@ -938,7 +938,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): for player, drop_config in drops.items(): for pack_num in range(1, 8): if f'Pack {pack_num}' in drop_config: - for prize, idx in enumerate(drop_config[f'Pack {pack_num}']): + for idx, prize in enumerate(drop_config[f'Pack {pack_num}']): chosen = random.choice(uniform_prizes) if prize == 'Random' else possible_prizes[prize] prizes[(pack_num-1)*8 + idx] = chosen for tree_pull_tier in range(1, 4): From 20e60c8a1dcb2a2d62361a6056db9d4464ab6e9c Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 22 Apr 2024 08:35:17 -0600 Subject: [PATCH 3/6] feat(spoiler): introducing custom spoiler levels --- BaseClasses.py | 191 ++++++++++-------- CLI.py | 5 +- Main.py | 16 +- Mystery.py | 4 +- Utils.py | 4 +- resources/app/cli/args.json | 16 +- resources/app/cli/lang/en.json | 10 +- resources/app/gui/lang/en.json | 7 +- .../gui/randomize/generation/checkboxes.json | 11 +- source/classes/constants.py | 2 +- source/gui/bottom.py | 1 - 11 files changed, 156 insertions(+), 111 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 64b33852..d2ddee2d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -21,7 +21,7 @@ from source.dungeon.RoomObject import RoomObject class World(object): def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, - timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints): + timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints, spoiler_mode): self.players = players self.teams = 1 self.shuffle = shuffle.copy() @@ -68,6 +68,7 @@ class World(object): self.hints = hints.copy() self.dynamic_regions = [] self.dynamic_locations = [] + self.spoiler_mode = spoiler_mode self.spoiler = Spoiler(self) self.lamps_needed_for_dark_rooms = 1 self.doors = [] @@ -89,6 +90,7 @@ class World(object): self.data_tables = {} self.damage_table = {} + for player in range(1, players + 1): def set_player_attr(attr, val): self.__dict__.setdefault(attr, {})[player] = val @@ -2497,6 +2499,17 @@ class Spoiler(object): self.metadata = {} self.shops = [] self.bosses = OrderedDict() + if world.spoiler_mode == 'settings': + self.settings = {'settings'} + elif world.spoiler_mode == 'semi': + self.settings = {'settings', 'entrances', 'requirements', 'prizes'} + elif world.spoiler_mode == 'full': + self.settings = {'settings', 'entrances', 'requirements', 'prizes', 'shops', 'doors', 'items', 'bosses', 'misc'} + elif world.spoiler_mode == 'debug': + self.settings = {'settings', 'entrances', 'requirements', 'prizes', 'shops', 'doors', 'items', 'misc', 'bosses', 'debug'} + else: + self.settings = {} + def set_entrance(self, entrance, exit, direction, player): if self.world.players == 1: @@ -2609,27 +2622,29 @@ class Spoiler(object): self.bottles[f'Waterfall Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][0] self.bottles[f'Pyramid Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][1] + def include_item(item): + return ('items' in self.settings and not item.crystal) or ('prizes' in self.settings and item.crystal) self.locations = OrderedDict() listed_locations = set() - lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld] + lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld and include_item(loc.item)] self.locations['Light World'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in lw_locations]) listed_locations.update(lw_locations) - dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld] + dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld and include_item(loc.item)] self.locations['Dark World'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in dw_locations]) listed_locations.update(dw_locations) - cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and not loc.skip] + cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and not loc.skip and include_item(loc.item)] self.locations['Caves'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in cave_locations]) listed_locations.update(cave_locations) for dungeon in self.world.dungeons: - dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and not loc.forced_item and not loc.skip] + dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and not loc.forced_item and not loc.skip and include_item(loc.item)] self.locations[str(dungeon)] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in dungeon_locations]) listed_locations.update(dungeon_locations) - other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and not loc.skip] + other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and not loc.skip and include_item(loc.item)] if other_locations: self.locations['Other Locations'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in other_locations]) listed_locations.update(other_locations) @@ -2721,65 +2736,68 @@ class Spoiler(object): outfile.write('ALttP Dungeon Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) if self.metadata['user_notes']: outfile.write('User Notes: %s\n' % self.metadata['user_notes']) - outfile.write('Filling Algorithm: %s\n' % self.world.algorithm) + if 'settings' in self.settings: + outfile.write('Filling Algorithm: %s\n' % self.world.algorithm) outfile.write('Players: %d\n' % self.world.players) outfile.write('Teams: %d\n' % self.world.teams) for player in range(1, self.world.players + 1): if self.world.players > 1: outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player))) - outfile.write(f'Settings Code: {self.metadata["code"][player]}\n') + if 'settings' in self.settings: + outfile.write(f'Settings Code: {self.metadata["code"][player]}\n') outfile.write('Logic: %s\n' % self.metadata['logic'][player]) - outfile.write('Mode: %s\n' % self.metadata['mode'][player]) - outfile.write('Swords: %s\n' % self.metadata['weapons'][player]) - outfile.write('Goal: %s\n' % self.metadata['goal'][player]) - if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']: - outfile.write('Triforce Pieces Required: %s\n' % self.metadata['triforcegoal'][player]) - outfile.write('Triforce Pieces Total: %s\n' % self.metadata['triforcepool'][player]) - outfile.write('Crystals required for GT: %s\n' % (str(self.world.crystals_gt_orig[player]))) - outfile.write('Crystals required for Ganon: %s\n' % (str(self.world.crystals_ganon_orig[player]))) - outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) - outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n") - outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) - outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) - outfile.write(f"Flute Mode: {self.metadata['flute_mode'][player]}\n") - outfile.write(f"Bow Mode: {self.metadata['bow_mode'][player]}\n") - outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n") - outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n") - outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][player])}\n") - outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) - if self.metadata['shuffle'][player] != 'vanilla': - outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'][player])}\n") - outfile.write(f"Back of Tavern Shuffled: {yn(self.metadata['shuffletavern'][player])}\n") - outfile.write(f"GT/Ganon Shuffled: {yn(self.metadata['shuffleganon'])}\n") - outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n") - outfile.write(f"Take Any Caves: {self.metadata['take_any'][player]}\n") - if self.metadata['goal'][player] != 'trinity': - outfile.write('Pyramid hole pre-opened: %s\n' % (self.metadata['open_pyramid'][player])) - outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) - if self.metadata['door_shuffle'][player] != 'vanilla': - outfile.write(f"Intensity: {self.metadata['intensity'][player]}\n") - outfile.write(f"Door Type Mode: {self.metadata['door_type_mode'][player]}\n") - outfile.write(f"Trap Door Mode: {self.metadata['trap_door_mode'][player]}\n") - outfile.write(f"Key Logic Algorithm: {self.metadata['key_logic'][player]}\n") - outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n") - outfile.write(f"Spiral Stairs can self-loop: {yn(self.metadata['door_self_loops'][player])}\n") - outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") - outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n") - outfile.write(f"Drop Shuffle: {self.metadata['dropshuffle'][player]}\n") - outfile.write(f"Pottery Mode: {self.metadata['pottery'][player]}\n") - outfile.write(f"Pot Shuffle (Legacy): {yn(self.metadata['potshuffle'][player])}\n") - outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) - outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) - outfile.write(f"Small Key shuffle: {self.metadata['keyshuffle'][player]}\n") - outfile.write('Big Key shuffle: %s\n' % ('Yes' if self.metadata['bigkeyshuffle'][player] else 'No')) - outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player]) - outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'][player]) - outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player]) - outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player]) - if self.metadata['enemy_shuffle'][player] != 'none': - outfile.write(f"Enemy logic: {self.metadata['any_enemy_logic'][player]}\n") - outfile.write(f"Hints: {yn(self.metadata['hints'][player])}\n") - outfile.write('Race: %s\n' % ('Yes' if self.world.settings.world_rep['meta']['race'] else 'No')) + if 'settings' in self.settings: + outfile.write('Mode: %s\n' % self.metadata['mode'][player]) + outfile.write('Swords: %s\n' % self.metadata['weapons'][player]) + outfile.write('Goal: %s\n' % self.metadata['goal'][player]) + if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']: + outfile.write('Triforce Pieces Required: %s\n' % self.metadata['triforcegoal'][player]) + outfile.write('Triforce Pieces Total: %s\n' % self.metadata['triforcepool'][player]) + outfile.write('Crystals required for GT: %s\n' % (str(self.world.crystals_gt_orig[player]))) + outfile.write('Crystals required for Ganon: %s\n' % (str(self.world.crystals_ganon_orig[player]))) + outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) + outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n") + outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) + outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) + outfile.write(f"Flute Mode: {self.metadata['flute_mode'][player]}\n") + outfile.write(f"Bow Mode: {self.metadata['bow_mode'][player]}\n") + outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n") + outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n") + outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][player])}\n") + outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) + if self.metadata['shuffle'][player] != 'vanilla': + outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'][player])}\n") + outfile.write(f"Back of Tavern Shuffled: {yn(self.metadata['shuffletavern'][player])}\n") + outfile.write(f"GT/Ganon Shuffled: {yn(self.metadata['shuffleganon'])}\n") + outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n") + outfile.write(f"Take Any Caves: {self.metadata['take_any'][player]}\n") + if self.metadata['goal'][player] != 'trinity': + outfile.write('Pyramid hole pre-opened: %s\n' % (self.metadata['open_pyramid'][player])) + outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) + if self.metadata['door_shuffle'][player] != 'vanilla': + outfile.write(f"Intensity: {self.metadata['intensity'][player]}\n") + outfile.write(f"Door Type Mode: {self.metadata['door_type_mode'][player]}\n") + outfile.write(f"Trap Door Mode: {self.metadata['trap_door_mode'][player]}\n") + outfile.write(f"Key Logic Algorithm: {self.metadata['key_logic'][player]}\n") + outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n") + outfile.write(f"Spiral Stairs can self-loop: {yn(self.metadata['door_self_loops'][player])}\n") + outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") + outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n") + outfile.write(f"Drop Shuffle: {self.metadata['dropshuffle'][player]}\n") + outfile.write(f"Pottery Mode: {self.metadata['pottery'][player]}\n") + outfile.write(f"Pot Shuffle (Legacy): {yn(self.metadata['potshuffle'][player])}\n") + outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) + outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) + outfile.write(f"Small Key shuffle: {self.metadata['keyshuffle'][player]}\n") + outfile.write('Big Key shuffle: %s\n' % ('Yes' if self.metadata['bigkeyshuffle'][player] else 'No')) + outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player]) + outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'][player]) + outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player]) + outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player]) + if self.metadata['enemy_shuffle'][player] != 'none': + outfile.write(f"Enemy logic: {self.metadata['any_enemy_logic'][player]}\n") + outfile.write(f"Hints: {yn(self.metadata['hints'][player])}\n") + outfile.write('Race: %s\n' % ('Yes' if self.world.settings.world_rep['meta']['race'] else 'No')) if self.startinventory: outfile.write('Starting Inventory:'.ljust(line_width)) @@ -2817,26 +2835,29 @@ class Spoiler(object): with open(filename, 'a') as outfile: line_width = 35 - outfile.write('\nRequirements:\n\n') - for dungeon, medallion in self.medallions.items(): - outfile.write(f'{dungeon}: {medallion} Medallion\n') - for player in range(1, self.world.players + 1): - player_name = '' if self.world.players == 1 else str(' (Player ' + str(player) + ')') - if self.world.crystals_gt_orig[player] == 'random': - outfile.write(str('Crystals Required for GT' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]))) - if self.world.crystals_ganon_orig[player] == 'random': - outfile.write(str('Crystals Required for Ganon' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) - outfile.write('\n\nBottle Refills:\n\n') - for fairy, bottle in self.bottles.items(): - outfile.write(f'{fairy}: {bottle}\n') + if 'requirements' in self.settings: + outfile.write('\nRequirements:\n\n') + for dungeon, medallion in self.medallions.items(): + outfile.write(f'{dungeon}: {medallion} Medallion\n') + for player in range(1, self.world.players + 1): + player_name = '' if self.world.players == 1 else str(' (Player ' + str(player) + ')') + if self.world.crystals_gt_orig[player] == 'random': + outfile.write(str('Crystals Required for GT' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]))) + if self.world.crystals_ganon_orig[player] == 'random': + outfile.write(str('Crystals Required for Ganon' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) - if self.entrances: + if 'misc' in self.settings: + outfile.write('\n\nBottle Refills:\n\n') + for fairy, bottle in self.bottles.items(): + outfile.write(f'{fairy}: {bottle}\n') + + if self.entrances and 'entrances' in self.settings: # entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly outfile.write('\nEntrances:\n\n') outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta", "entrances", entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta", "entrances", entry['exit'])) for entry in self.entrances.values()])) - if self.doors: + if self.doors and 'doors' in self.settings: outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( ['%s%s %s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', @@ -2845,33 +2866,37 @@ class Spoiler(object): self.world.fish.translate("meta", "doors", entry['exit']), '({0})'.format(entry['dname']) if self.world.doorShuffle[entry['player']] != 'basic' else '') for entry in self.doors.values()])) - if self.lobbies: + if self.lobbies and 'doors' in self.settings: outfile.write('\n\nDungeon Lobbies:\n\n') outfile.write('\n'.join( [f"{'Player {0}: '.format(entry['player']) if self.world.players > 1 else ''}{entry['lobby_name']}: {entry['door_name']}" for entry in self.lobbies.values()])) - if self.doorTypes: + if self.doorTypes and 'doors' in self.settings: # doorNames: For some reason these come in combined, somehow need to split on the thing to translate # doorTypes: Small Key, Bombable, Bonkable outfile.write('\n\nDoor Types:\n\n') outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', self.world.fish.translate("meta", "doors", entry['doorNames']), self.world.fish.translate("meta", "doorTypes", entry['type'])) for entry in self.doorTypes.values()])) + # locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name # items: Item names outfile.write('\n\nLocations:\n\n') - outfile.write('\n'.join(['%s: %s' % (self.world.fish.translate("meta", "locations", location), self.world.fish.translate("meta", "items", item)) for grouping in self.locations.values() for (location, item) in grouping.items()])) + outfile.write('\n'.join(['%s: %s' % (self.world.fish.translate("meta", "locations", location), self.world.fish.translate("meta", "items", item)) + for grouping in self.locations.values() for (location, item) in grouping.items()])) # locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name # items: Item names - outfile.write('\n\nShops:\n\n') - outfile.write('\n'.join("{} [{}]\n {}".format(self.world.fish.translate("meta", "locations", shop['location']), shop['type'], "\n ".join(self.world.fish.translate("meta", "items", item) for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops)) + if 'shops' in self.settings: + outfile.write('\n\nShops:\n\n') + outfile.write('\n'.join("{} [{}]\n {}".format(self.world.fish.translate("meta", "locations", shop['location']), shop['type'], "\n ".join(self.world.fish.translate("meta", "items", item) for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops)) - for player in range(1, self.world.players + 1): - if self.world.boss_shuffle[player] != 'none': - bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses - outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n') - outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']])) + if 'bosses' in self.settings: + for player in range(1, self.world.players + 1): + if self.world.boss_shuffle[player] != 'none': + bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses + outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n') + outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']])) def extras(self, filename): # todo: conditional on enemy shuffle mode diff --git a/CLI.py b/CLI.py index 1066b845..1c1e0a30 100644 --- a/CLI.py +++ b/CLI.py @@ -224,7 +224,7 @@ def parse_settings(): 'mixed_travel': 'prevent', 'standardize_palettes': 'standardize', 'aga_randomness': True, - + "triforce_pool": 0, "triforce_goal": 0, "triforce_pool_min": 0, @@ -253,10 +253,9 @@ def parse_settings(): 'msu_resume': False, 'collection_rate': False, - # Spoiler defaults to TRUE + 'spoiler': 'full', # Playthrough defaults to TRUE # ROM defaults to TRUE - "create_spoiler": True, "calc_playthrough": True, "create_rom": True, "bps": False, diff --git a/Main.py b/Main.py index 8e49e3f0..4bdeae9b 100644 --- a/Main.py +++ b/Main.py @@ -85,7 +85,7 @@ def main(args, seed=None, fish=None): raise RuntimeError(BabelFish().translate("cli","cli","hybridglitches.door.shuffle")) world = World(args.multi, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, - args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints) + args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints, args.spoiler) world.customizer = customized if customized else None logger = logging.getLogger('') if seed is None: @@ -202,7 +202,7 @@ def main(args, seed=None, fish=None): if item: world.push_precollected(item) - if args.create_spoiler and not args.jsonout: + 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: @@ -406,12 +406,12 @@ def main(args, seed=None, fish=None): if args.mystery and not args.suppress_meta: world.spoiler.hashes_to_file(output_path(f'{outfilebase}_meta.txt')) - elif args.create_spoiler and not args.jsonout: + elif world.spoiler_mode != 'none' and not args.jsonout: world.spoiler.hashes_to_file(output_path(f'{outfilebase}_Spoiler.txt')) - if args.create_spoiler and not args.jsonout: + if world.spoiler_mode != 'none' and not args.jsonout: logger.info(world.fish.translate("cli", "cli", "patching.spoiler")) world.spoiler.to_file(output_path(f'{outfilebase}_Spoiler.txt')) - if args.loglevel == 'debug': + if 'debug' in world.spoiler.settings: world.spoiler.extras(output_path(f'{outfilebase}_Spoiler.txt')) if not args.skip_playthrough: @@ -420,7 +420,7 @@ def main(args, seed=None, fish=None): if args.jsonout: print(json.dumps({**jsonout, 'spoiler': world.spoiler.to_json()})) - elif args.create_spoiler: + elif world.spoiler_mode != 'none': logger.info(world.fish.translate("cli","cli","patching.spoiler")) if args.jsonout: with open(output_path('%s_Spoiler.json' % outfilebase), 'w') as outfile: @@ -435,7 +435,7 @@ def main(args, seed=None, fish=None): logger.info("") logger.info(world.fish.translate("cli","cli","made.rom") % (YES if (args.create_rom) else NO)) logger.info(world.fish.translate("cli","cli","made.playthrough") % (YES if (args.calc_playthrough) else NO)) - logger.info(world.fish.translate("cli","cli","made.spoiler") % (YES if (not args.jsonout and args.create_spoiler) else NO)) + logger.info(world.fish.translate("cli","cli","made.spoiler") % (YES if (not args.jsonout and world.spoiler_mode != 'none') else NO)) logger.info(world.fish.translate("cli","cli","seed") + ": %s", world.seed) logger.info(world.fish.translate("cli","cli","total.time"), time.perf_counter() - start) @@ -449,7 +449,7 @@ def copy_world(world): # ToDo: Not good yet ret = World(world.players, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, - world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints) + world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode) ret.teams = world.teams ret.player_names = copy.deepcopy(world.player_names) ret.remote_items = world.remote_items.copy() diff --git a/Mystery.py b/Mystery.py index 170dd8bc..9d81b344 100644 --- a/Mystery.py +++ b/Mystery.py @@ -28,7 +28,7 @@ def main(): parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--names', default='') parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1)) - parser.add_argument('--create_spoiler', action='store_true') + parser.add_argument('--spoiler', default='none', choices=['none', 'settings', 'semi', 'full', 'debug']) parser.add_argument('--suppress_rom', action='store_true') parser.add_argument('--suppress_meta', action='store_true') parser.add_argument('--bps', action='store_true') @@ -63,7 +63,7 @@ def main(): erargs = parse_cli(['--multi', str(args.multi)]) erargs.seed = seed erargs.names = args.names - erargs.create_spoiler = args.create_spoiler + erargs.spoiler = args.spoiler erargs.suppress_rom = args.suppress_rom erargs.suppress_meta = args.suppress_meta erargs.bps = args.bps diff --git a/Utils.py b/Utils.py index 50ab1b84..6d52d5f0 100644 --- a/Utils.py +++ b/Utils.py @@ -342,11 +342,11 @@ def update_deprecated_args(args): # Don't do: Yes # Do: No if "suppress_spoiler" in argVars: - args.create_spoiler = not args.suppress_spoiler in truthy + args.spoiler = 'none' # Don't do: No # Do: Yes if "create_spoiler" in argVars: - args.suppress_spoiler = not args.create_spoiler in truthy + args.spoiler = 'full' # ROM defaults to TRUE # Don't do: Yes diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 6aedcad8..c2d31606 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -4,14 +4,14 @@ "action": "store_true", "type": "bool" }, - "create_spoiler": { - "action": "store_false", - "dest": "suppress_spoiler", - "type": "bool", - "help": "suppress" - }, - "suppress_spoiler": { - "action": "store_true" + "spoiler": { + "choices": [ + "none", + "settings", + "semi", + "full", + "debug" + ] }, "mystery": { "action": "store_true", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 1f0defb9..b57508fe 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -60,7 +60,15 @@ }, "help": { "lang": [ "App Language, if available, defaults to English" ], - "create_spoiler": [ "Output a Spoiler File" ], + "spoiler": [ + "Spoiler File Options. (default: %(default)s)", + "None: No Spoiler", + "Meta: Meta information only about game. Intended for mystery settings", + "Settings: Only settings information", + "Semi: ", + "Full: Full spoiler generated", + "Debug: Includes debug information" + ], "bps": [ "Output BPS patches instead of ROMs"], "logic": [ "Select Enforcement of Item Requirements. (default: %(default)s)", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index c14dee25..aa8be106 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -206,7 +206,12 @@ "randomizer.generation.bps": "Create BPS Patches", - "randomizer.generation.createspoiler": "Create Spoiler Log", + "randomizer.generation.spoiler": "Create Spoiler Log", + "randomizer.generation.spoiler.none": "None", + "randomizer.generation.spoiler.settings": "Settings Only", + "randomizer.generation.spoiler.semi": "Semi (Entrances and Prizes)", + "randomizer.generation.spoiler.full": "Full", + "randomizer.generation.spoiler.debug": "Debug", "randomizer.generation.createrom": "Create Patched ROM", "randomizer.generation.calcplaythrough": "Calculate Playthrough", "randomizer.generation.print_custom_yaml": "Print Customizer File", diff --git a/resources/app/gui/randomize/generation/checkboxes.json b/resources/app/gui/randomize/generation/checkboxes.json index 6e377027..3eeab78b 100644 --- a/resources/app/gui/randomize/generation/checkboxes.json +++ b/resources/app/gui/randomize/generation/checkboxes.json @@ -2,7 +2,16 @@ "checkboxes": { "createrom": { "type": "checkbox" }, "bps": { "type": "checkbox" }, - "createspoiler": { "type": "checkbox" }, + "spoiler": { + "type": "selectbox", + "options": [ + "none", + "settings", + "semi", + "full", + "debug" + ] + }, "calcplaythrough": { "type": "checkbox" }, "print_custom_yaml": { "type": "checkbox" } } diff --git a/source/classes/constants.py b/source/classes/constants.py index 15797e03..0950211e 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -134,7 +134,7 @@ SETTINGSTOPROCESS = { }, "generation": { "bps": "bps", - "createspoiler": "create_spoiler", + "spoiler": "spoiler", "createrom": "create_rom", "calcplaythrough": "calc_playthrough", "print_custom_yaml": "print_custom_yaml", diff --git a/source/gui/bottom.py b/source/gui/bottom.py index b9fdd9b2..5abb3de6 100644 --- a/source/gui/bottom.py +++ b/source/gui/bottom.py @@ -126,7 +126,6 @@ def bottom_frame(self, parent, args=None): made[k] = m.group(1) + m.group(2) + ' ' + m.group(4) successMsg += (made["rom"] % (YES if (guiargs.create_rom) else NO)) + "\n" successMsg += (made["playthrough"] % (YES if (guiargs.calc_playthrough) else NO)) + "\n" - successMsg += (made["spoiler"] % (YES if (not guiargs.jsonout and guiargs.create_spoiler) else NO)) + "\n" successMsg += (made["enemizer"] % (YES if needEnemizer else NO)) + "\n" # FIXME: English successMsg += ("Seed%s: %s" % ('s' if len(seeds) > 1 else "", ','.join(str(x) for x in seeds))) From fcc464ef2351e8737eeb620e82729e3b480a69c4 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 22 Apr 2024 11:49:03 -0600 Subject: [PATCH 4/6] fix(enemizer): messed up gfx on hyrule castle rain state --- source/enemizer/SpriteSheets.py | 6 +++++- source/rom/DataTables.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/source/enemizer/SpriteSheets.py b/source/enemizer/SpriteSheets.py index 2a935048..4496fb97 100644 --- a/source/enemizer/SpriteSheets.py +++ b/source/enemizer/SpriteSheets.py @@ -687,7 +687,11 @@ def setup_required_overworld_groups(sheets): sheets[4].add_sprite_to_sheet([None, None, None, None], {0xF, 0x9F}) # Waterfall of wishing (pre/post-Aga) sheets[3].add_sprite_to_sheet([None, None, None, 14], {0x14, 0xA4}) # Graveyard (pre/post-Aga) sheets[1].add_sprite_to_sheet([None, None, 76, 0x3F], {0x1B, 0xAB}) # Hyrule Castle (pre/post-Aga) - sheets[2].add_sprite_to_sheet([None, None, None, 0x3F], {}) # Hyrule Castle - rain state + ## group 0 set to 0x48 for tutortial guards + ## group 1 & 2 set for green knife guards (and probably normal green guard) + ## group 3 set for lightning gate + sheets[2].add_sprite_to_sheet([0x48, 0x49, 0x13, 0x3F], {}) # Hyrule Castle - rain state + # Smithy/Race/Kak (pre/post-Aga) sheets[6].add_sprite_to_sheet([0x4F, 0x49, 0x4A, 0x50], {0x18, 0x22, 0x28, 0xA8, 0xB2, 0xB8}) sheets[8].add_sprite_to_sheet([None, None, 18, None], {0x30, 0xC0}) # Desert (pre/post-Aga) diff --git a/source/rom/DataTables.py b/source/rom/DataTables.py index 72a1156d..752ec73e 100644 --- a/source/rom/DataTables.py +++ b/source/rom/DataTables.py @@ -94,6 +94,7 @@ class DataTables: # _00FA81 is LW normal # _00FAC1 is LW post-aga # _00FB01 is DW + # _00FA41 is rain state self.write_ow_sprite_data_to_rom(rom) for sprite, stats in self.enemy_stats.items(): # write health to rom From 292d49faed1cac09c0e483fd7a20f9da80249f13 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 23 Apr 2024 11:02:41 -0600 Subject: [PATCH 5/6] fix(enemizer): enenmy bans --- Main.py | 2 +- RELEASENOTES.md | 9 +++++++++ source/enemizer/enemy_deny.yaml | 3 +++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Main.py b/Main.py index 4bdeae9b..3a74ff8f 100644 --- a/Main.py +++ b/Main.py @@ -39,7 +39,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.10' +version_number = '1.4.1.11' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d37a2a0a..24398bb3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -141,6 +141,15 @@ These are now independent of retro mode and have three options: None, Random, an # Patch Notes +* 1.4.1.11u + * New Feature: Several spoiler levels added: None, Settings-only, Semi, Full, Debug + * Semi includes only entrances, prizes, and medallions (potential new spoiler mode being worked on, definition may change) + * Standard: Re-added tutorial guard near large rock + * Enemizer + * Fixed the overwriting of bonk fairies + * Fixed broken graphics on hyrule castle + * Enemy bans + * Customizer: Fixed bug with customizing prize packs * 1.4.1.10u * Vanilla key logic: Fix for vanilla layout Misery Mire which allows more complex key logic. Locations blocked by crystal switch access are only locked by 2 keys thanks to that being the minimum in Mire to reach one of two crystal switches. * Autotracking: Fix for chest turn counter with chest containing multiworld items (Thanks Hiimcody) diff --git a/source/enemizer/enemy_deny.yaml b/source/enemizer/enemy_deny.yaml index d6dc6564..46b1176c 100644 --- a/source/enemizer/enemy_deny.yaml +++ b/source/enemizer/enemy_deny.yaml @@ -325,6 +325,7 @@ UwGeneralDeny: - [ 0x00c2, 5, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots - [ 0x00c5, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Turtle Rock - Catwalk - Mini Helmasaur" - [ 0x00c5, 7, [ "Statue" ] ] #"Turtle Rock - Catwalk - Laser Eye (Left) 4" + - [0x00c6, 5, ["Bumper"]] - [ 0x00cb, 0, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots - [ 0x00cb, 3, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Thieves' Town - Grand Room NW - Zol 1" - [ 0x00cb, 5, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Thieves' Town - Grand Room NW - Zol 2" @@ -419,10 +420,12 @@ OwGeneralDeny: - [0x03, 10, ["Gibo"]] # OldMan eating Gibo - [0x05, 11, ["Bumper", "AntiFairyCircle"]] # Blocks path to portal - [0x1e, 3, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle"]] # forbid a beamos here + - [0x35, 8, ["RollerVerticalUp", "RollerVerticalDown"]] # blocks the dock - [0x40, 0, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle", "Thief"]] - [0x40, 7, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle", "Thief"]] - [0x40, 13, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle", "Thief"]] - [0x40, 14, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle", "Thief"]] + - [0x40, 16, ["RollerVerticalUp", "RollerVerticalDown"]] # Ropa near back hole is really large as a roller - [0x55, 6, ["BigSpike"]] - [0x57, 5, ["RollerVerticalUp", "RollerVerticalDown"]] - [0x5e, 0, ["Gibo"]] # kiki eating Gibo From 89645bb4b5f10a987e63667ec2e0eca9da805a9c Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 23 Apr 2024 13:41:03 -0600 Subject: [PATCH 6/6] feat(entrances): lite/lean support enemy shuffle caves --- RELEASENOTES.md | 1 + source/overworld/EntranceShuffle2.py | 98 +++++++++++++++++++--------- 2 files changed, 69 insertions(+), 30 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 24398bb3..1f4720b4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -144,6 +144,7 @@ These are now independent of retro mode and have three options: None, Random, an * 1.4.1.11u * New Feature: Several spoiler levels added: None, Settings-only, Semi, Full, Debug * Semi includes only entrances, prizes, and medallions (potential new spoiler mode being worked on, definition may change) + * Entrance: Lite/Lean support enemy drop shuffle * Standard: Re-added tutorial guard near large rock * Enemizer * Fixed the overwriting of bonk fairies diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index c15115b6..059aefbe 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -916,12 +916,18 @@ def do_limited_shuffle_exclude_drops(pool_def, avail, lw=True): def do_vanilla_connect(pool_def, avail): - if pool_def['condition'] == 'shopsanity': + if 'shopsanity' in pool_def['condition']: if avail.world.shopsanity[avail.player]: return - elif pool_def['condition'] == 'pottery': # this condition involves whether caves with pots are shuffled or not + if 'pottery' in pool_def['condition']: # this condition involves whether caves with pots are shuffled or not if avail.world.pottery[avail.player] not in ['none', 'keys', 'dungeon']: return + if 'dropshuffle' in pool_def['condition']: + if avail.world.dropshuffle[avail.player] not in ['none', 'keys']: + return + if 'enemy_drop' in pool_def['condition']: + if avail.world.dropshuffle[avail.player] not in ['none', 'keys'] or avail.world.enemy_shuffle[avail.player] != 'none': + return defaults = {**default_connections, **(inverted_default_connections if avail.inverted else open_default_connections)} if avail.inverted: if 'Dark Death Mountain Fairy' in pool_def['entrances']: @@ -1440,28 +1446,44 @@ modes = { 'special': 'vanilla', 'condition': '', 'entrances': ['Dark Death Mountain Fairy', 'Mire Fairy', 'Archery Game', - 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Bonk Fairy (Dark)', + 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Fairy', - 'Dark Lake Hylia Shop', 'East Dark World Hint', 'Kakariko Gamble Game', 'Good Bee Cave', - 'Long Fairy Cave', 'Bush Covered House', 'Fortune Teller (Light)', 'Lost Woods Gamble', - 'Desert Fairy', 'Light Hype Fairy', 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy', - 'Bonk Fairy (Light)'], + 'East Dark World Hint', 'Kakariko Gamble Game', + 'Bush Covered House', 'Fortune Teller (Light)', 'Lost Woods Gamble', + 'Desert Fairy', 'Light Hype Fairy', 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy'], }, 'fixed_shops': { 'special': 'vanilla', 'condition': 'shopsanity', 'entrances': ['Dark Death Mountain Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', - 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', - 'Lake Hylia Shop'], + 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Lake Hylia Shop', 'Dark Lake Hylia Shop'], }, 'fixed_pottery': { 'special': 'vanilla', 'condition': 'pottery', 'entrances': ['Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', - 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', - 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', - 'Mire Hint'] - + '20 Rupee Cave', '50 Rupee Cave', 'Palace of Darkness Hint', + 'Dark Lake Hylia Ledge Spike Cave', 'Mire Hint'] + }, + 'fixed_enemy_drops_fairies': { + 'special': 'vanilla', + 'condition': 'enemy_drop', + 'entrances': ['Bonk Fairy (Dark)', 'Good Bee Cave', 'Long Fairy Cave', 'Bonk Fairy (Light)'] + }, + 'fixed_pots_n_bones_fairies': { + 'special': 'vanilla', + 'condition': ['pottery', 'enemy_drop'], + 'entrances': ['Hookshot Fairy'] + }, + 'fixed_pots_n_bones': { + 'special': 'vanilla', + 'condition': ['pottery', 'dropshuffle'], + 'entrances': ['Light World Bomb Hut'] + }, + 'fixed_shop_n_bones': { + 'special': 'vanilla', + 'condition': ['shopsanity', 'enemy_drop'], + 'entrances': ['Capacity Upgrade'] }, 'item_caves': { # shuffles shops/pottery if they weren't fixed in the last steps 'entrances': ['Mimic Cave', 'Spike Cave', 'Mire Shed', 'Hammer Peg Cave', 'Chest Game', @@ -1469,14 +1491,14 @@ modes = { 'Ice Rod Cave', 'Dam', 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Mini Moldorm Cave', 'Checkerboard Cave', 'Graveyard Cave', 'Cave 45', 'Sick Kids House', 'Blacksmiths Hut', 'Sahasrahlas Hut', 'Aginahs Cave', 'Chicken House', 'Kings Grave', 'Blinds Hideout', - 'Waterfall of Wishing', 'Dark Death Mountain Shop', + 'Waterfall of Wishing', 'Dark Death Mountain Shop', 'Dark Lake Hylia Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', 'Lake Hylia Shop', 'Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', - 'Mire Hint', - 'Links House', 'Tavern North'] + 'Bonk Fairy (Dark)', 'Good Bee Cave', 'Long Fairy Cave', 'Bonk Fairy (Light)', + 'Mire Hint', 'Links House', 'Tavern North'] }, 'old_man_cave': { # have to do old man cave first so lw dungeon don't use up everything 'special': 'old_man_cave_east', @@ -1516,28 +1538,44 @@ modes = { 'special': 'vanilla', 'condition': '', 'entrances': ['Dark Death Mountain Fairy', 'Mire Fairy', 'Archery Game', - 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Bonk Fairy (Dark)', + 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Fairy', - 'Dark Lake Hylia Shop', 'East Dark World Hint', 'Kakariko Gamble Game', 'Good Bee Cave', - 'Long Fairy Cave', 'Bush Covered House', 'Fortune Teller (Light)', 'Lost Woods Gamble', - 'Desert Fairy', 'Light Hype Fairy', 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy', - 'Bonk Fairy (Light)'], + 'East Dark World Hint', 'Kakariko Gamble Game', + 'Bush Covered House', 'Fortune Teller (Light)', 'Lost Woods Gamble', + 'Desert Fairy', 'Light Hype Fairy', 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy'], }, 'fixed_shops': { 'special': 'vanilla', 'condition': 'shopsanity', 'entrances': ['Dark Death Mountain Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', - 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', - 'Lake Hylia Shop'], + 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Lake Hylia Shop', 'Dark Lake Hylia Shop'], }, 'fixed_pottery': { 'special': 'vanilla', 'condition': 'pottery', 'entrances': ['Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', - 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', - 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', - 'Mire Hint'] - + '20 Rupee Cave', '50 Rupee Cave', 'Palace of Darkness Hint', + 'Dark Lake Hylia Ledge Spike Cave', 'Mire Hint'] + }, + 'fixed_enemy_drops_fairies': { + 'special': 'vanilla', + 'condition': 'enemy_drop', + 'entrances': ['Bonk Fairy (Dark)', 'Good Bee Cave', 'Long Fairy Cave', 'Bonk Fairy (Light)'] + }, + 'fixed_pots_n_bones_fairies': { + 'special': 'vanilla', + 'condition': ['pottery', 'enemy_drop'], + 'entrances': ['Hookshot Fairy'] + }, + 'fixed_pots_n_bones': { + 'special': 'vanilla', + 'condition': ['pottery', 'dropshuffle'], + 'entrances': ['Light World Bomb Hut'] + }, + 'fixed_shop_n_bones': { + 'special': 'vanilla', + 'condition': ['shopsanity', 'enemy_drop'], + 'entrances': ['Capacity Upgrade'] }, 'item_caves': { # shuffles shops/pottery if they weren't fixed in the last steps 'entrances': ['Mimic Cave', 'Spike Cave', 'Mire Shed', 'Hammer Peg Cave', 'Chest Game', @@ -1545,14 +1583,14 @@ modes = { 'Ice Rod Cave', 'Dam', 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Mini Moldorm Cave', 'Checkerboard Cave', 'Graveyard Cave', 'Cave 45', 'Sick Kids House', 'Blacksmiths Hut', 'Sahasrahlas Hut', 'Aginahs Cave', 'Chicken House', 'Kings Grave', 'Blinds Hideout', - 'Waterfall of Wishing', 'Dark Death Mountain Shop', + 'Waterfall of Wishing', 'Dark Death Mountain Shop', 'Dark Lake Hylia Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', 'Lake Hylia Shop', 'Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', - 'Mire Hint', - 'Links House', 'Tavern North'] # inverted links house gets substituted + 'Bonk Fairy (Dark)', 'Good Bee Cave', 'Long Fairy Cave', 'Bonk Fairy (Light)', + 'Mire Hint', 'Links House', 'Tavern North'] # inverted links house gets substituted } } },