From 8af70ce1efd1139730c683aec25d031dd6089582 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 1 Dec 2022 10:04:13 -0700 Subject: [PATCH 01/11] Version bump --- Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Main.py b/Main.py index 08e29254..907e7a97 100644 --- a/Main.py +++ b/Main.py @@ -34,7 +34,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -__version__ = '1.2.0.0-x' +__version__ = '1.2.0.0-u' from source.classes.BabelFish import BabelFish From 1c91eef29aade02924a2b827327fe5757111a4aa Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 1 Dec 2022 11:47:54 -0700 Subject: [PATCH 02/11] Another known issue --- RELEASENOTES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8fc802a6..1905b64f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -105,4 +105,5 @@ None yet # Known Issues * Standing in the doorway when defeating Aga 1 and being teleported to the Dark World will not clear door state. It may cause issues requiring a Save & Quit to fix. -* Decoupled doors can lead to situations where you aren't logically supposed to go back through a door without a big key or small key, but you can if you press the correct direction back through the door first. There are some transitions where you may get stuck without a bomb. These problems are planned to be fixed. \ No newline at end of file +* Decoupled doors can lead to situations where you aren't logically supposed to go back through a door without a big key or small key, but you can if you press the correct direction back through the door first. There are some transitions where you may get stuck without a bomb. These problems are planned to be fixed. +* Logic getting to Skull X room may be wrong if a trap door, big key door, or bombable wall. A bomb jump to get to those pot may be required if you don't have boots to bonk across. \ No newline at end of file From becba348b9f410a0c0b6c09c84d75dae4e24af52 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 1 Dec 2022 14:10:10 -0700 Subject: [PATCH 03/11] New goals and rom update --- BaseClasses.py | 13 ++++++-- Fill.py | 2 +- ItemList.py | 28 ++++++++++-------- Main.py | 2 +- RELEASENOTES.md | 17 ++++++++--- Rom.py | 15 ++++++++-- Rules.py | 4 +++ data/base2current.bps | Bin 93456 -> 93489 bytes mystery_example.yml | 2 ++ resources/app/cli/args.json | 4 ++- resources/app/cli/lang/en.json | 5 +++- resources/app/gui/lang/en.json | 2 ++ resources/app/gui/randomize/item/widgets.json | 4 ++- source/tools/MysteryUtils.py | 4 ++- 14 files changed, 74 insertions(+), 28 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index d7724a98..6cbc146e 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -155,7 +155,9 @@ class World(object): def finish_init(self): for player in range(1, self.players + 1): if self.mode[player] == 'retro': - self.mode[player] == 'open' + self.mode[player] = 'open' + if self.goal[player] == 'completionist': + self.accessibility[player] = 'locations' def get_name_string_for_object(self, obj): return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' @@ -1035,6 +1037,10 @@ class CollectionState(object): def item_count(self, item, player): return self.prog_items[item, player] + def everything(self, player): + return (len([x for x in self.locations_checked if x.player == player]) + >= len(self.world.get_filled_locations(player))) + def has_crystals(self, count, player): crystals = ['Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'] return len([crystal for crystal in crystals if self.has(crystal, player)]) >= count @@ -2587,7 +2593,7 @@ class Spoiler(object): 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']: + 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]))) @@ -2867,7 +2873,8 @@ world_mode = {"open": 0, "standard": 1, "inverted": 2} sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3} # byte 2: GGGD DFFH (goal, diff, item_func, hints) -goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'crystals': 4, 'trinity': 5} +goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'crystals': 4, 'trinity': 5, + 'ganonhunt': 6, 'completionist': 7} diff_mode = {"normal": 0, "hard": 1, "expert": 2} func_mode = {"normal": 0, "hard": 1, "expert": 2} diff --git a/Fill.py b/Fill.py index 0a50470b..0cee1ab3 100644 --- a/Fill.py +++ b/Fill.py @@ -400,7 +400,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None else: max_trash = gt_count scaled_trash = math.floor(max_trash * scale_factor) - if world.goal[player] in ['triforcehunt', 'trinity']: + if world.goal[player] in ['triforcehunt', 'trinity', 'ganonhunt']: gftower_trash_count = random.randint(scaled_trash, max_trash) else: gftower_trash_count = random.randint(0, scaled_trash) diff --git a/ItemList.py b/ItemList.py index c17ce509..1b4b6087 100644 --- a/ItemList.py +++ b/ItemList.py @@ -181,8 +181,12 @@ def get_custom_array_key(item): def generate_itempool(world, player): - if (world.difficulty[player] not in ['normal', 'hard', 'expert'] or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'trinity', 'crystals'] - or world.mode[player] not in ['open', 'standard', 'inverted'] or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] or world.progressive not in ['on', 'off', 'random']): + if (world.difficulty[player] not in ['normal', 'hard', 'expert'] + or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'trinity', 'crystals', + 'ganonhunt', 'completionist'] + or world.mode[player] not in ['open', 'standard', 'inverted'] + or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] + or world.progressive not in ['on', 'off', 'random']): raise NotImplementedError('Not supported yet') if world.timer in ['ohko', 'timed-ohko']: @@ -344,7 +348,7 @@ def generate_itempool(world, player): world.clock_mode = clock_mode goal = world.goal[player] - if goal in ['triforcehunt', 'trinity']: + if goal in ['triforcehunt', 'trinity', 'ganonhunt']: g, t = set_default_triforce(goal, world.treasure_hunt_count[player], world.treasure_hunt_total[player]) world.treasure_hunt_count[player], world.treasure_hunt_total[player] = g, t world.treasure_hunt_icon[player] = 'Triforce Piece' @@ -817,15 +821,15 @@ def add_pot_contents(world, player): world.itempool.append(ItemFactory(item, player)) -def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, bombbag, - door_shuffle, logic, flute_activated): +def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, + bombbag, door_shuffle, logic, flute_activated): pool = [] placed_items = {} precollected_items = [] clock_mode = None - if goal in ['triforcehunt', 'trinity']: + if goal in ['triforcehunt', 'trinity', 'ganonhunt']: if treasure_hunt_total == 0: - treasure_hunt_total = 30 if goal == 'triforcehunt' else 10 + treasure_hunt_total = 30 if goal in ['triforcehunt', 'ganonhunt'] else 10 # triforce pieces max out triforcepool = ['Triforce Piece'] * min(treasure_hunt_total, max_goal) @@ -928,7 +932,7 @@ def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt elif timer == 'timed-ohko': pool.extend(diff.timedohko) clock_mode = 'countdown-ohko' - if goal in ['triforcehunt', 'trinity']: + if goal in ['triforcehunt', 'trinity', 'ganonhunt']: pool.extend(triforcepool) for extra in diff.extras: @@ -987,7 +991,7 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer customitemarray["triforce"] = total_items_to_place # Triforce Pieces - if goal in ['triforcehunt', 'trinity']: + if goal in ['triforcehunt', 'trinity', 'ganonhunt']: g, t = set_default_triforce(goal, customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"]) customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"] = g, t @@ -1025,8 +1029,8 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer treasure_hunt_count = max(min(customitemarray["triforcepiecesgoal"], max_goal), 1) treasure_hunt_icon = 'Triforce Piece' # Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling. - if ((customitemarray["triforcepieces"] < treasure_hunt_count) and (goal in ['triforcehunt', 'trinity']) - and (customitemarray["triforce"] == 0)): + if ((customitemarray["triforcepieces"] < treasure_hunt_count) + and (goal in ['triforcehunt', 'trinity', 'ganonhunt']) and (customitemarray["triforce"] == 0)): extrapieces = treasure_hunt_count - customitemarray["triforcepieces"] pool.extend(['Triforce Piece'] * extrapieces) itemtotal = itemtotal + extrapieces @@ -1214,7 +1218,7 @@ def get_player_dungeon_item_pool(world, player): # location pool doesn't support larger values at this time def set_default_triforce(goal, custom_goal, custom_total): triforce_goal, triforce_total = 0, 0 - if goal == 'triforcehunt': + if goal in ['triforcehunt', 'ganonhunt']: triforce_goal, triforce_total = 20, 30 elif goal == 'trinity': triforce_goal, triforce_total = 8, 10 diff --git a/Main.py b/Main.py index 907e7a97..48ba4c81 100644 --- a/Main.py +++ b/Main.py @@ -34,7 +34,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -__version__ = '1.2.0.0-u' +__version__ = '1.2.0.1-u' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1905b64f..c69d8094 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -53,7 +53,16 @@ This is similar to insanity mode in ER where door entrances and exits are not pa ## Customizer -Please see [Customizer documentation](docs/Customizer.md) on how to create custom seeds. +Please see [Customizer documentation](docs/Customizer.md) on how to create custom seeds. + +## New Goals + +### Triforce Hunt + Ganon +Collect the requisite triforce pieces, then defeat Ganon. (Aga2 not required). Use `ganonhunt` on CLI + +### Completionist +All dungeons not enough for you? You have to obtain every item in the game too. This option turns on the collection rate counter and forces accessibility to be 100% locations. Finish by defeating Ganon. + ## Standard Generation Change @@ -100,10 +109,10 @@ These are now independent of retro mode and have three options: None, Random, an # Bug Fixes and Notes -None yet +* 1.2.0.1-u + * Fixed the issue when defeating Agahnim and standing in the doorway can cause door state to linger. # Known Issues -* Standing in the doorway when defeating Aga 1 and being teleported to the Dark World will not clear door state. It may cause issues requiring a Save & Quit to fix. * Decoupled doors can lead to situations where you aren't logically supposed to go back through a door without a big key or small key, but you can if you press the correct direction back through the door first. There are some transitions where you may get stuck without a bomb. These problems are planned to be fixed. -* Logic getting to Skull X room may be wrong if a trap door, big key door, or bombable wall. A bomb jump to get to those pot may be required if you don't have boots to bonk across. \ No newline at end of file +* Logic getting to Skull X room may be wrong if a trap door, big key door, or bombable wall is shuffled there. A bomb jump to get to those pot may be required if you don't have boots to bonk across. \ No newline at end of file diff --git a/Rom.py b/Rom.py index f8b5b107..1f1dfed8 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'b6fcbc0d61faffa178135545f18fadbd' +RANDOMIZERBASEHASH = 'fb7f9a0d501ba9ecd0a31066f9a0a973' class JsonRom(object): @@ -730,7 +730,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal if world.doorShuffle[player] not in ['vanilla', 'basic']: dr_flags |= DROptions.Map_Info - if world.collection_rate[player] and world.goal[player] not in ['triforcehunt', 'trinity']: + if ((world.collection_rate[player] or world.goal[player] == 'completionist') + and world.goal[player] not in ['triforcehunt', 'trinity', 'ganonhunt']): dr_flags |= DROptions.Debug if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic'\ and world.mixed_travel[player] == 'prevent': @@ -1275,7 +1276,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # set up goals for treasure hunt rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28]) - if world.goal[player] in ['triforcehunt', 'trinity']: + if world.goal[player] in ['triforcehunt', 'trinity', 'ganonhunt']: rom.write_bytes(0x180167, int16_as_bytes(world.treasure_hunt_count[player])) rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled) @@ -1340,6 +1341,10 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(0x18003E, 0x02) # make ganon invincible until all dungeons are beat elif world.goal[player] in ['crystals', 'trinity']: rom.write_byte(0x18003E, 0x04) # make ganon invincible until all crystals + elif world.goal[player] in ['ganonhunt']: + rom.write_byte(0x18003E, 0x05) # make ganon invincible until all triforce pieces collected + elif world.goal[player] in ['completionist']: + rom.write_byte(0x18003E, 0x09) # make ganon invincible until everything is collected else: rom.write_byte(0x18003E, 0x03) # make ganon invincible until all crystals and aga 2 are collected @@ -2362,6 +2367,10 @@ def write_strings(rom, world, player, team): trinity_crystal_text = ('%d crystal to beat Ganon.' if world.crystals_needed_for_ganon[player] == 1 else '%d crystals to beat Ganon.') % world.crystals_needed_for_ganon[player] tt['sign_ganon'] = 'Three ways to victory! %s Get to it!' % trinity_crystal_text tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\nhidden in a hollow tree. If you bring\n%d triforce pieces, I can reassemble it." % int(world.treasure_hunt_count[player]) + elif world.goal[player] == 'ganonhunt': + tt['sign_ganon'] = 'Go find the Triforce pieces to beat Ganon' + elif world.goal[player] == 'completionist': + tt['sign_ganon'] = 'Ganon only respects those who have done everything' tt['ganon_fall_in'] = Ganon1_texts[random.randint(0, len(Ganon1_texts) - 1)] tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!' tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!' diff --git a/Rules.py b/Rules.py index 67417aac..3f4e6b4d 100644 --- a/Rules.py +++ b/Rules.py @@ -60,6 +60,10 @@ def set_rules(world, player): add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player)) elif world.goal[player] in ['triforcehunt', 'trinity']: add_rule(world.get_location('Murahdahla', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player])) + elif world.goal[player] == 'ganonhunt': + add_rule(world.get_location('Ganon', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player])) + elif world.goal[player] == 'completionist': + add_rule(world.get_location('Ganon', player), lambda state: state.everything(player)) if world.mode[player] != 'inverted': set_big_bomb_rules(world, player) diff --git a/data/base2current.bps b/data/base2current.bps index fed6120e263f9a2a335fbb4bb826d44f5dcd6d98..a3983a652d56acb5442bb49a4732d16ddd2c9109 100644 GIT binary patch delta 6122 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 bMS|K1<@2VtOr8pEOqObw*{@Y68K(R{F(Ryn delta 6061 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 Date: Thu, 1 Dec 2022 16:22:15 -0700 Subject: [PATCH 04/11] District generation fix --- source/item/FillUtil.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 6b242589..07f36ac8 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -189,26 +189,27 @@ def district_item_pool_config(world): if district.dungeon: adjustment = len([i for i in world.get_dungeon(name, p).all_items if i.is_inside_dungeon_item(world)]) - dist_len = len(district.locations) - adjustment + dist_adj = adjustment if name not in district_choices: - district_choices[name] = (district.sphere_one, dist_len) + district_choices[name] = (district.sphere_one, dist_adj) else: so, amt = district_choices[name] - district_choices[name] = (so or district.sphere_one, amt + dist_len) + district_choices[name] = (so or district.sphere_one, amt + dist_adj) chosen_locations = defaultdict(set) - location_cnt = 0 + adjustment_cnt = 0 # choose a sphere one district sphere_one_choices = [d for d, info in district_choices.items() if info[0]] sphere_one = random.choice(sphere_one_choices) - so, amt = district_choices[sphere_one] - location_cnt += amt + so, adj = district_choices[sphere_one] for player in range(1, world.players + 1): for location in world.districts[player][sphere_one].locations: chosen_locations[location].add(player) del district_choices[sphere_one] config.recorded_choices.append(sphere_one) + adjustment_cnt += adj + location_cnt = len(chosen_locations) - adjustment_cnt scale_factors = defaultdict(int) scale_total = 0 @@ -217,8 +218,9 @@ def district_item_pool_config(world): dungeon = world.get_entrance(ent, p).connected_region.dungeon if dungeon: scale = world.crystals_needed_for_gt[p] - scale_total += scale - scale_factors[dungeon.name] += scale + if scale > 0: + scale_total += scale + scale_factors[dungeon.name] += scale scale_total = max(1, scale_total) scale_divisors = defaultdict(lambda: 1) scale_divisors.update(scale_factors) @@ -226,13 +228,15 @@ def district_item_pool_config(world): while location_cnt < item_cnt: weights = [scale_total / scale_divisors[d] for d in district_choices.keys()] choice = random.choices(list(district_choices.keys()), weights=weights, k=1)[0] - so, amt = district_choices[choice] - location_cnt += amt + so, adj = district_choices[choice] + for player in range(1, world.players + 1): for location in world.districts[player][choice].locations: chosen_locations[location].add(player) del district_choices[choice] config.recorded_choices.append(choice) + adjustment_cnt += adj + location_cnt = len(chosen_locations) - adjustment_cnt config.placeholders = location_cnt - item_cnt config.location_groups[0].locations = chosen_locations @@ -383,7 +387,10 @@ def vanilla_fallback(item_to_place, locations, world): def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion=False): config = world.item_pool_config - item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name + if not isinstance(item_to_place, str): + item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name + else: + item_name = item_to_place if world.algorithm == 'vanilla_fill': filtered = [] item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name @@ -443,7 +450,6 @@ def filter_pot_locations(locations, world): return locations - vanilla_mapping = { 'Green Pendant': ['Eastern Palace - Prize'], 'Red Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'], From 9bf0391e603588b534d7963fb20467f3d6520d25 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 1 Dec 2022 16:23:11 -0700 Subject: [PATCH 05/11] Cleanup and generation fix --- DoorShuffle.py | 1 - DungeonGenerator.py | 4 ++-- KeyDoorShuffle.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index bab0bf56..44685e5d 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -15,7 +15,6 @@ from Items import ItemFactory from RoomData import DoorKind, PairedDoor, reset_rooms from source.dungeon.DungeonStitcher import GenerationException, generate_dungeon from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2 -# from DungeonGenerator import generate_dungeon from DungeonGenerator import ExplorationState, convert_regions, pre_validate, determine_required_paths, drop_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances from DungeonGenerator import dungeon_portals, dungeon_drops, connect_doors, count_reserved_locations diff --git a/DungeonGenerator.py b/DungeonGenerator.py index b1ce5456..6a21817b 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -860,7 +860,7 @@ class ExplorationState(object): self.crystal = exp_door.crystal return exp_door - def visit_region(self, region, key_region=None, key_checks=False, bk_Flag=False): + def visit_region(self, region, key_region=None, key_checks=False, bk_flag=False): if region.type != RegionType.Dungeon: self.crystal = CrystalBarrier.Orange if self.crystal == CrystalBarrier.Either: @@ -881,7 +881,7 @@ class ExplorationState(object): self.ttl_locations += 1 if location not in self.found_locations: self.found_locations.append(location) - if not bk_Flag and (not location.forced_item or 'Big Key' in location.item.name): + if not bk_flag and (not location.forced_item or 'Big Key' in location.item.name): self.bk_found.add(location) if location.name in dungeon_events and location.name not in self.events: if self.flooded_key_check(location): diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 17859357..7b5e011f 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1445,7 +1445,7 @@ def validate_bk_layout(proposal, builder, start_regions, world, player): if loc.forced_big_key(): return True else: - return len(state.bk_found) > 0 + return state.count_locations_exclude_specials(world, player) > 0 return False From e67ff4d5dca12ba7d6f1ad960d5da719c8bcea27 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 7 Dec 2022 15:57:51 -0700 Subject: [PATCH 06/11] Completionist fixes --- BaseClasses.py | 9 +++++++-- DoorShuffle.py | 6 +++--- DungeonGenerator.py | 2 +- Main.py | 5 ++++- Rom.py | 6 +++--- Rules.py | 3 ++- data/base2current.bps | Bin 93489 -> 93478 bytes test/stats/EntranceShuffleStats.py | 6 ++++-- 8 files changed, 24 insertions(+), 13 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 6cbc146e..358a40e0 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -470,7 +470,10 @@ class World(object): if self.has_beaten_game(state): return True - prog_locations = [location for location in self.get_locations() if location.item is not None and (location.item.advancement or location.event) and location not in state.locations_checked] + prog_locations = [location for location in self.get_locations() if location.item is not None + and (location.item.advancement or location.event + or self.goal[location.player] == 'completionist') + and location not in state.locations_checked] while prog_locations: sphere = [] @@ -1038,8 +1041,10 @@ class CollectionState(object): return self.prog_items[item, player] def everything(self, player): + all_locations = self.world.get_filled_locations(player) + all_locations.remove(self.world.get_location('Ganon', player)) return (len([x for x in self.locations_checked if x.player == player]) - >= len(self.world.get_filled_locations(player))) + >= len(all_locations)) def has_crystals(self, count, player): crystals = ['Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'] diff --git a/DoorShuffle.py b/DoorShuffle.py index 44685e5d..31e5d13b 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -6,7 +6,7 @@ from enum import unique, Flag from typing import DefaultDict, Dict, List from itertools import chain -from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys +from BaseClasses import RegionType, Region, Door, DoorType, Sector, CrystalBarrier, DungeonInfo, dungeon_keys from BaseClasses import PotFlags, LocationType, Direction from Doors import reset_portals from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts @@ -15,12 +15,12 @@ from Items import ItemFactory from RoomData import DoorKind, PairedDoor, reset_rooms from source.dungeon.DungeonStitcher import GenerationException, generate_dungeon from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2 -from DungeonGenerator import ExplorationState, convert_regions, pre_validate, determine_required_paths, drop_entrances +from DungeonGenerator import ExplorationState, convert_regions, determine_required_paths, drop_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances from DungeonGenerator import dungeon_portals, dungeon_drops, connect_doors, count_reserved_locations from DungeonGenerator import valid_region_to_explore from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock -from KeyDoorShuffle import validate_bk_layout, check_bk_special +from KeyDoorShuffle import validate_bk_layout from Utils import ncr, kth_combination diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 6a21817b..af0ef5c0 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -12,7 +12,7 @@ from typing import List from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, PolSlot, flooded_keys, Sector from BaseClasses import Hook, hook_from_door, Door from Regions import dungeon_events, flooded_keys_reverse -from Dungeons import dungeon_regions, split_region_starts +from Dungeons import split_region_starts from RoomData import DoorKind from source.dungeon.DungeonStitcher import generate_dungeon_find_proposal diff --git a/Main.py b/Main.py index 48ba4c81..2897f839 100644 --- a/Main.py +++ b/Main.py @@ -617,7 +617,8 @@ def create_playthrough(world): world = copy_world(world) # get locations containing progress items - prog_locations = [location for location in world.get_filled_locations() if location.item.advancement] + prog_locations = [location for location in world.get_filled_locations() if location.item.advancement + or world.goal[location.player] == 'completionist'] optional_locations = ['Trench 1 Switch', 'Trench 2 Switch', 'Ice Block Drop', 'Skull Star Tile'] state_cache = [None] collection_spheres = [] @@ -654,6 +655,8 @@ def create_playthrough(world): for num, sphere in reversed(list(enumerate(collection_spheres))): to_delete = set() for location in sphere: + if world.goal[location.player] == 'completionist': + continue # every location for that player is required # we remove the item at location and check if game is still beatable logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) old_item = location.item diff --git a/Rom.py b/Rom.py index 1f1dfed8..f5a529fd 100644 --- a/Rom.py +++ b/Rom.py @@ -15,7 +15,7 @@ try: except ImportError: raise Exception('Could not load BPS module') -from BaseClasses import ShopType, Region, Location, Door, DoorType, RegionType, LocationType, Item +from BaseClasses import ShopType, Region, Location, Door, DoorType, RegionType, LocationType from DoorShuffle import compass_data, DROptions, boss_indicator, dungeon_portals from Dungeons import dungeon_music_addresses, dungeon_table from Regions import location_table, shop_to_location_table, retro_shops @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'fb7f9a0d501ba9ecd0a31066f9a0a973' +RANDOMIZERBASEHASH = '54cfc4c5e85c80fb2958cb458d36ad14' class JsonRom(object): @@ -1344,7 +1344,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): elif world.goal[player] in ['ganonhunt']: rom.write_byte(0x18003E, 0x05) # make ganon invincible until all triforce pieces collected elif world.goal[player] in ['completionist']: - rom.write_byte(0x18003E, 0x09) # make ganon invincible until everything is collected + rom.write_byte(0x18003E, 0x0a) # make ganon invincible until everything is collected else: rom.write_byte(0x18003E, 0x03) # make ganon invincible until all crystals and aga 2 are collected diff --git a/Rules.py b/Rules.py index 3f4e6b4d..0190aaa4 100644 --- a/Rules.py +++ b/Rules.py @@ -2075,7 +2075,8 @@ def add_key_logic_rules(world, player): for chest in d_logic.bk_chests: big_chest = world.get_location(chest.name, player) add_rule(big_chest, create_rule(d_logic.bk_name, player)) - if len(d_logic.bk_doors) == 0 and len(d_logic.bk_chests) <= 1: + if (len(d_logic.bk_doors) == 0 and len(d_logic.bk_chests) <= 1 + and world.accessibility[player] != 'locations'): set_always_allow(big_chest, allow_big_key_in_big_chest(d_logic.bk_name, player)) if world.keyshuffle[player] == 'universal': for d_name, layout in world.key_layout[player].items(): diff --git a/data/base2current.bps b/data/base2current.bps index a3983a652d56acb5442bb49a4732d16ddd2c9109..e9709333a2475617d136d5e83f868116597e4c94 100644 GIT binary patch delta 889 zcmW-be`phT7{>GFM-$TEB{50U&Zgg9Y?E}OG_%^+s_V22mqp7qu;>^Zve~f&h55%u znfkrV>zZmwF4y!;dx&Z64O5Y#_ea*v+N!Hj2GVV~xx#RBj5!#RKPqAox?_Jm&-3Bs zd7mpQrmt2^nPF%V&-`R8ha>mY!%z?U&BN-K`mi3t>YVXM1Kd#0TCP{?*K=yj_1zob zQyuTA320WA%I#gSh--QXSgwiGqyTw_Ap`i}U)*beMpF+ad0IMZfSB3VBMNMBl~!v< zv~=4FtwyNDryF2f@8WvJOrB&5dTJn@(%toncj9d(`&`2&k`^`iy+}Nn2nyyq3$5I^q1@tLo0VbH)%|_9{j}vP}vV z>-nvP9QBeNNlyCbQgrVAbVf0dBz%8iC3?8l`i3G9w8(#fA;|vK^W>9r~8Hq?4CTijnk)kLSmi zglcJO#KwgfxeF6^@K-tgHB_LOvjYp)@M}9X!?V&uJ1`J_AW_$%Dcwv{4<(wl{Bb#| zy{5l$i#lB;m`2`@$&nWlt;L~n$0%jU-`L=Ux9qK>@ke&4cT8e-xoMbgzyT-p1zC}A z$4?U}K7&Vi{5-`+E+i*A%O`0ETRSu*2f37dES{1N;d7_9B#fIjLcas^=_!mIr8lO_ zC+FKL1aWjDWSzYz*^`P{w`_>@V$l(tO<=?Ygp_ju@2Nj?fr|@VB{1IOxpSxZbhD-x z4n``I7V-&x=7J`-CFG*=zAt!2-pWjfl`2xqUy9*X7j#=k#^mj^sMXft3#|}BSnq~9 zaAU*`zS=Vw99WC5EWDZz%#g!*%??MUSCUc)kGUZPF&uZpF2{lTiIgYErSFPt{`T|= zzJ!n6(9O0eDa-6?da$_cc+^&`5#E3DyB-~8S-9D@M{56=$No~(12!wHVjqDPIDtt5 z`{5n@fxvF&#Hk_iqt1AtFuQY0Mm8eb2tONtjen59wMN(i{n*NCJCEa=EbOihs?@2T jzH*ZkAK(}Z9ngyRSm=U=lGP6lQOo^~*SWLX*WCXB6RcMp delta 957 zcmW-dZA@Er7{&Ya-L|YRw5&|%{&a!1n`jAd9dp2DLKK*BnPGEgv00|8mKljI>YVI< z%P!DO(%xR+Dco%owrecSWOzR`!ypJu#1DpHTnzi5AcJV!MmkVhPOyo;(xZI=kX#%IYkHk8w(w4}W!bJs;j z|A>Wd9AQ)R#aKdUc+X1t&c-C)RP)zBQ3}XE%+(0ff)S=C8&W~5&eX!O?(_yV%Dz3> zSpz(&hH6~j0yXDOw5KENPpS z|3`TV88L~OcU%ROam6BiM>%qc?;Q|TZ0fvrzH(|>?W4<&iE0+)0yIxlkDTouAeZ8D zjghv`q?VhP|9fzX#iF)x^)^)45x|O~ogog7opMQq;+Ztj0x6LJd@CF5xdL$t2GzL=%>y z%L1iEq0bq8EOK2Vs!0p?>-5kq*2s_bnSwsE@EpEkfgM`1V=HBQu-5`^n8%C-%HUCU z#R6t9{>W4NtR_}Qqrc!iD-`Cb`e`Bd4mta&ncPR44O%SK{oyqWe{7I9*E?thb>Jx* z9QOJ-rV2HuqD&lLWAJR030#eg)-86@T6baZm{1dp3Ll1}LJ05KK#UR&&(_UFhI?8%3OtqplgMARvkRjqB9wt6Sd;4V85?Y9FS=eghUpdIpcCj+@r zJ$WC)b|`fe`0P|T(8ZXAXUxM~PD^eiGe=RhL!+U8P}oh=vQr7J%O2oIivzYn5$bc1x?y{>KKjDa?POiN=3h`Obp`+c diff --git a/test/stats/EntranceShuffleStats.py b/test/stats/EntranceShuffleStats.py index 2be02071..25c0cbfd 100644 --- a/test/stats/EntranceShuffleStats.py +++ b/test/stats/EntranceShuffleStats.py @@ -1,5 +1,7 @@ +import os +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) import RaceRandom as random -import logging import time from collections import Counter, defaultdict @@ -106,7 +108,7 @@ def test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links # seed = 635441530 random.seed(seed) world = World(1, {1: shuffle_mode}, {1: 'vanilla'}, {1: 'noglitches'}, {1: main_mode}, {}, {}, {}, - {}, {}, {}, {}, {}, True, {}, {}, [], {}) + {}, {}, {}, {}, {}, True, {}, [], {}) world.customizer = False world.shufflelinks = {1: links} if world.mode[1] != 'inverted': From c42f3c320239a23135976aec73c044aa3d32f971 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 7 Dec 2022 16:06:31 -0700 Subject: [PATCH 07/11] Various typos and fixes --- BaseClasses.py | 8 +++++--- mystery_example.yml | 2 +- source/tools/MysteryUtils.py | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 358a40e0..f7fd2cc9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -110,7 +110,8 @@ class World(object): set_player_attr('can_access_trock_front', None) set_player_attr('can_access_trock_big_chest', None) set_player_attr('can_access_trock_middle', None) - set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['crossed', 'insanity']) + set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] + or shuffle[player] in ['lean', 'crossed', 'insanity']) set_player_attr('mapshuffle', False) set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', 'none') @@ -1163,6 +1164,8 @@ class CollectionState(object): return self.has('Fire Rod', player) or self.has('Lamp', player) def can_flute(self, player): + if self.world.mode[player] == 'standard' and not self.has('Zelda Delivered', player): + return False # can't flute in rain state if any(map(lambda i: i.name in ['Ocarina', 'Ocarina (Activated)'], self.world.precollected_items)): return True lw = self.world.get_region('Light World', player) @@ -2619,7 +2622,6 @@ class Spoiler(object): 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") - outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n") if self.metadata['goal'][player] != 'trinity': outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) @@ -2920,7 +2922,7 @@ boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique flute_mode = {'normal': 0, 'active': 1} keyshuffle_mode = {'none': 0, 'wild': 1, 'universal': 2} # reserved 8 modes? take_any_mode = {'none': 0, 'random': 1, 'fixed': 2} -bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silver': 3} +bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} # additions # psuedoboots does not effect code diff --git a/mystery_example.yml b/mystery_example.yml index 93e882b4..275359fc 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -97,7 +97,7 @@ mcu: 1 # map, compass, universal smalls # for use when you aren't using the dungeon_items above # smallkey_shuffle: -# standard: 5 +# none: 5 # wild: 1 # universal: 1 dungeon_counters: diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 88833fdd..b5fef26d 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -203,5 +203,6 @@ def roll_settings(weights): ret.ow_palettes = get_choice('ow_palettes', romweights) ret.uw_palettes = get_choice('uw_palettes', romweights) ret.shuffle_sfx = get_choice('shuffle_sfx', romweights) == 'on' + ret.msu_resume = get_choice('msu_resume', romweights) == 'on' return ret From 9ab741d481a8104eb6a38cb8f568dff8f860f677 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 9 Dec 2022 16:04:12 -0700 Subject: [PATCH 08/11] Documentation updates --- README.md | 132 +++++++++++++++++++++++++++++++++++++++++------- RELEASENOTES.md | 2 + 2 files changed, 117 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index c7ad7b42..bbb9b9bf 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,16 @@ See https://alttpr.com/ for more details on the normal randomizer. 2. [Commonly Missed Things](#commonly-missed-things) (** **Read This If New** **) 3. [Settings](#settings) 1. [Dungeon Randomization](#dungeon-settings) - 1. [Dungeon Door Shuffle](#door-shuffle---doorshuffle) + 1. [Dungeon Door Shuffle](#door-shuffle) 2. [Intensity Level](#intensity---intensity-number) 3. [Key Drop Shuffle (Legacy)](#key-drop-shuffle-legacy---keydropshuffle) - 4. [Pottery](#pottery) - 5. [Shuffle Enemy Key Drops](#shuffle-enemy-key-drops---dropshuffle) - 6. [Experimental Features](#experimental-features) - 7. [Crossed Dungeon Specific Settings](#crossed-dungeon-specific-settings) + 4. [Door Type Shuffle](#door-type_shuffle) + 5. [Decouple Doors](#decouple-doors) + 6. [Pottery](#pottery) + 7. [Small Key Shuffle](#small-key-shuffle) + 8. [Shuffle Enemy Key Drops](#shuffle-enemy-key-drops) + 9. [Experimental Features](#experimental-features) + 10. [Crossed Dungeon Specific Settings](#crossed-dungeon-specific-settings) 2. [Item Randomization Changes](#item-randomization) 1. [New "Items"](#new-items) 2. [Shopsanity](#shopsanity) @@ -23,12 +26,15 @@ See https://alttpr.com/ for more details on the normal randomizer. 4. [Goal](#goal) 5. [Item Sorting](#item-sorting) 6. [Forbidden Boss Items](#forbidden-boss-items) - 3. [Entrance Randomization](#entrance-randomization) + 3. [Customizer](#customizer) + 4. [Entrance Randomization](#entrance-randomization) 1. [Shuffle Links House](#shuffle-links-house) 2. [Overworld Map](#overworld-map) - 4. [Enemizer](#enemizer) - 5. [Game Options](#game-options) - 6. [Generation Setup & Miscellaneous](#generation-setup--miscellaneous) + 5. [Enemizer](#enemizer) + 6. [Retro Changes](#retro-changes) + 7. [Standard Changes](#standard-changes) + 8. [Game Options](#game-options) + 9. [Generation Setup & Miscellaneous](#generation-setup--miscellaneous) ## Setup and Installation @@ -105,12 +111,15 @@ You start with a “Mirror Scroll”, a dumbed-down mirror that only works in du Only extra settings are found here. All entrance randomizer settings are supported. See their [readme](https://github.com/KevinCathcart/ALttPEntranceRandomizer/blob/master/README.md) -### Door Shuffle (--doorShuffle) +### Door Shuffle * Vanilla - Doors are not shuffled * Basic - Doors are shuffled only within a single dungeon. +* Paritioned - Dungeons are shuffled in 3 pools: Light World, Early Dark World, Late Dark World. (Late Dark are the four dungeons that require Mitts in vanilla, including Ganons Tower) * Crossed - Doors are shuffled between dungeons as well. +CLI: `--doorShuffle [vanilla|basic|partitioned|crossed]` + ### Intensity (--intensity number) * Level 1 - Normal door and spiral staircases are shuffled @@ -122,7 +131,24 @@ Only extra settings are found here. All entrance randomizer settings are support Adds 33 new locations to the randomization pool. The 32 small keys found under pots and dropped by enemies and the Big Key drop location are added to the pool. The keys normally found there are added to the item pool. Retro adds 32 generic keys to the pool instead. This has been can be controlled more granularly with the [Pottery](#pottery) and -[Shuffle Enemy Key Drops](#shuffle-enemy-key-drops---dropshuffle) +[Shuffle Enemy Key Drops](#shuffle-enemy-key-drops) + +### Door Type Shuffle + +Four options here, and all of them only take effect if Dungeon Door Shuffle is not vanilla: + +* Small Key Doors, Bomb Doors, Dash Doors: This is what was normally shuffled previously +* Adds Big Keys Doors: Big key doors are now shuffled in addition to those above, and Big Key doors are enabled to be on in both vertical directions thanks to a graphic that ended up on the cutting room floor. This does change +* Adds Trap Doors: All trap doors that are permanently shut in vanilla are shuffled. +* Increases all Door Types: This is a chaos mode where each door type per dungeon is randomized between 1 less and 4 more. + +CLI: `--door_type_mode [original|big|all|chaos]` + +### Decouple Doors + +This is similar to insanity mode in ER where door entrances and exits are not paired anymore. Tends to remove more logic from dungeons as many rooms will not be required to traverse to explore. Hope you like transitions. + +CLI `--decoupledoors` ### Pottery @@ -155,11 +181,23 @@ CLI `--colorizepots` This continues to works the same by shuffling all pots on a supertile. It works with the lottery option as well to move the switches to any valid pot on the supertile regardless of the pots chosen in the pottery mode. This may increase the number of pot locations slightly depending on the mode. -### Shuffle Enemy Key Drops (--dropshuffle) +### Small Key Shuffle + +There are three options now available: + +* In Dungeon: The small key will be in their own dungeon +* Randomized: Small keys can be shuffled outside their own dungeon +* Universal: Retro keys without the other options + +CLI: `--keyshuffle [none|wild|universal]` + +### Shuffle Enemy Key Drops Enemies that drop keys can have their drop shuffled into the pool. This is the one part of the keydropshuffle option. See the pottery option for more options involving pots. +CLI: `--dropshuffle` + ### Experimental Features You will start as a bunny if your spawn point is in the dark world. CLI: `--experimental` @@ -197,14 +235,35 @@ Rooms adjacent to sanctuary get their coloring to match the Sanctuary's original ### New "Items" -#### Bombbag (--bombbag) +#### Bombbag -Two bomb bags are added to the item pool (They look like +10 Capacity upgrades). Bombs are unable to be used until one is found. Bomb capacity upgrades are otherwise unavailable. +Two bomb bags are added to the item pool (They look like +10 Capacity upgrades). Bombs are unable to be used until one is found. Bomb capacity upgrades are otherwise unavailable. -#### Pseudo Boots (--pseudoboots) +CLI `--bombbag` + +#### Pseudo Boots Dashing is allowed without the boots item however doors and certain rocks remain un-openable until boots are found. Items that require boots are still unattainable. Specific sequence breaks like hovering and water-walking are not allowed until boots are found. Bonk distance is shortened to prevent certain pits from being crossed. Finding boots restores all normal behavior. +CLI `--pseudoboots` + +#### Flute Mode + +Normal mode for flute means you need to activate it at the village statue after finding it like usual. Activated flute mode mean you can use it immediately upon finding it. The flute SFX plays to let you know this is the case. + +CLI:`--flute_mode` + +#### Bow Mode + +Four options here : + +* Progressive. Standard progressive bows. +* Silvers separate. One bow in the pool and silvers are a separate item. +* Retro (progressive). Arrows cost rupees. You need to purchase the single arrow item at a shop and there are two progressive bows places. +* Retro + Silvers. Arrows cost rupees. You need to purchase the single arrow item or find the silvers, there is only one bow, and silvers are a separate item (but count for the quiver if found). + +CLI: `--bow_mode [progressive|silvers|retro|retro_silvers]` + ### Shopsanity This adds 32 shop locations (9 more in retro) to the general location pool. @@ -316,7 +375,12 @@ CLI: `--logic owglitches` ### Goal -Trinity goal is now supported. Find one of 3 triforces to win. One is at pedestal. One is with Ganon. One is with Murahdahla who wants you to find 8 of 10 triforce pieces to complete. +New supported goals: + +* Trinity: Find one of 3 triforces to win. One is at pedestal. One is with Ganon. One is with Murahdahla who wants you to find 8 of 10 triforce pieces to complete. +* Triforce Hunt + Ganon: Collect the requisite triforce pieces, then defeat Ganon. (Aga2 not required). Use `ganonhunt` on CLI +* Completionist: All dungeons not enough for you? You have to obtain every item in the game too. This option turns on the collection rate counter and forces accessibility to be 100% locations. Finish by defeating Ganon. + ### Item Sorting @@ -394,12 +458,24 @@ CLI: ```--restrict_boss_items