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 fed6120e..a3983a65 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/mystery_example.yml b/mystery_example.yml index 26fb559f..93e882b4 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -82,6 +82,8 @@ pedestal: 2 triforce-hunt: 2 trinity: 2 + ganonhunt: 2 + completionist: 1 triforce_goal_min: 10 triforce_goal_max: 30 triforce_pool_min: 20 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 5758ca15..a09b1722 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -67,7 +67,9 @@ "dungeons", "triforcehunt", "trinity", - "crystals" + "crystals", + "ganonhunt", + "completionist" ] }, "difficulty": { diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index a6dcc3ee..0d3d4cdd 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -107,7 +107,10 @@ "Triforce Hunt: Places 30 Triforce Pieces in the world, collect", " 20 of them to beat the game.", "Trinity: Can beat the game by defeating Ganon, pulling", - " Pedestal, or delivering Triforce Pieces." + " Pedestal, or delivering Triforce Pieces.", + "Ganon Hunt: Places 30 Triforce Pieces in the world, collect", + " 20 of them then defeat Ganon.", + "Completionist: Find everything then defeat Ganon." ], "difficulty": [ "Select game difficulty. Affects available itempool. (default: %(default)s)", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 9a55893a..aafeaaf8 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -248,6 +248,8 @@ "randomizer.item.goal.triforcehunt": "Triforce Hunt", "randomizer.item.goal.trinity": "Trinity", "randomizer.item.goal.crystals": "Crystals", + "randomizer.item.goal.ganonhunt": "Triforce Hunt + Ganon", + "randomizer.item.goal.completionist": "Completionist", "randomizer.item.crystals_gt": "Crystals to open GT", "randomizer.item.crystals_gt.0": "0", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 88f85858..8e64f916 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -36,7 +36,9 @@ "dungeons", "triforcehunt", "trinity", - "crystals" + "crystals", + "ganonhunt", + "completionist" ] }, "crystals_gt": { diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 38ee8c1a..88833fdd 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -113,7 +113,9 @@ def roll_settings(weights): 'dungeons': 'dungeons', 'pedestal': 'pedestal', 'triforce-hunt': 'triforcehunt', - 'trinity': 'trinity' + 'trinity': 'trinity', + 'ganonhunt': 'ganonhunt', + 'completionist': 'completionist' }[goal] ret.openpyramid = goal in ['fast_ganon', 'trinity'] if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False