diff --git a/BaseClasses.py b/BaseClasses.py index 9e44607f..87b62d66 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -324,8 +324,10 @@ class World(object): self._cached_locations = None def get_unfilled_locations(self, player=None) -> list: - return [location for location in self.get_locations() if - (player is None or location.player == player) and location.item is None] + if player is not None: + return [location for location in self.get_locations() if + location.player == player and not location.item] + return [location for location in self.get_locations() if not location.item] def get_filled_locations(self, player=None) -> list: return [location for location in self.get_locations() if diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index 1e405c8f..f6689afc 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -66,7 +66,8 @@ def parse_arguments(argv, no_defaults=False): Palace, to allow for an alternative to firerod. Vanilla: Swords are in vanilla locations. ''') - parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?', choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'], + parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?', + choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'localtriforcehunt', 'crystals'], help='''\ Select completion goal. (default: %(default)s) Ganon: Collect all crystals, beat Agahnim 2 then @@ -77,6 +78,8 @@ def parse_arguments(argv, no_defaults=False): Agahnim fights and then defeat Ganon. Triforce Hunt: Places 30 Triforce Pieces in the world, collect 20 of them to beat the game. + Local Triforce Hunt: Places 30 Triforce Pieces in your world, collect + 20 of them to beat the game. ''') parser.add_argument('--difficulty', default=defval('normal'), const='normal', nargs='?', choices=['normal', 'hard', 'expert'], help='''\ diff --git a/Fill.py b/Fill.py index 7aff1325..9478bf55 100644 --- a/Fill.py +++ b/Fill.py @@ -228,7 +228,9 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None if not gftower_trash or not world.ganonstower_vanilla[player] or world.logic[player] == 'owglitches': continue - gftower_trash_count = (random.randint(15, 50) if world.goal[player] == 'triforcehunt' else random.randint(0, 15)) + gftower_trash_count = ( + random.randint(15, 50) if world.goal[player] in {'triforcehunt', 'localtriforcehunt'} else random.randint(0, + 15)) gtower_locations = [location for location in fill_locations if 'Ganons Tower' in location.name and location.player == player] random.shuffle(gtower_locations) diff --git a/Gui.py b/Gui.py index 5cd51868..b1ab304b 100755 --- a/Gui.py +++ b/Gui.py @@ -243,7 +243,8 @@ def guiMain(args=None): goalFrame = Frame(drowDownFrame) goalVar = StringVar() goalVar.set('ganon') - goalOptionMenu = OptionMenu(goalFrame, goalVar, 'ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals') + goalOptionMenu = OptionMenu(goalFrame, goalVar, 'ganon', 'pedestal', 'dungeons', 'triforcehunt', + 'localtriforcehunt', 'crystals') goalOptionMenu.pack(side=RIGHT) goalLabel = Label(goalFrame, text='Game goal') goalLabel.pack(side=LEFT) diff --git a/ItemList.py b/ItemList.py index 60a8624f..d72229a3 100644 --- a/ItemList.py +++ b/ItemList.py @@ -125,30 +125,29 @@ difficulties = { } def generate_itempool(world, player): - if (world.difficulty[player] not in ['normal', 'hard', 'expert'] or world.goal[player] not in ['ganon', 'pedestal', - 'dungeons', - 'triforcehunt', - 'crystals'] - or world.mode[player] not in ['open', 'standard', 'inverted'] or world.timer[player] not in [False, - 'display', - 'timed', - 'timed-ohko', - 'ohko', - 'timed-countdown']): - raise NotImplementedError('Not supported yet') + if world.difficulty[player] not in ['normal', 'hard', 'expert']: + raise NotImplementedError(f"Diffulty {world.difficulty[player]}") + if world.goal[player] not in {'ganon', 'pedestal', 'dungeons', 'triforcehunt', 'localtriforcehunt', 'crystals'}: + raise NotImplementedError(f"Goal {world.goal[player]}") + if world.mode[player] not in {'open', 'standard', 'inverted'}: + raise NotImplementedError(f"Mode {world.mode[player]}") + if world.timer[player] not in {False, 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'}: + raise NotImplementedError(f"Timer {world.mode[player]}") + if world.timer[player] in ['ohko', 'timed-ohko']: world.can_take_damage[player] = False - - if world.goal[player] in ['pedestal', 'triforcehunt']: + if world.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt']: world.push_item(world.get_location('Ganon', player), ItemFactory('Nothing', player), False) else: world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False) - if world.goal[player] in ['triforcehunt']: - region = world.get_region('Light World',player) + if world.goal[player] in ['triforcehunt', 'localtriforcehunt']: + region = world.get_region('Light World', player) loc = Location(player, "Murahdahla", parent=region) - loc.access_rule = lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) > state.world.treasure_hunt_count[player] + loc.access_rule = lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', + player) > \ + state.world.treasure_hunt_count[player] region.locations.append(loc) world.dynamic_locations.append(loc) @@ -505,7 +504,7 @@ def get_pool_core(world, player: int): pool.extend(diff.timedohko) extraitems -= len(diff.timedohko) clock_mode = 'countdown-ohko' - if goal == 'triforcehunt': + if goal in {'triforcehunt', 'localtriforcehunt'}: pool.extend(diff.triforcehunt) extraitems -= len(diff.triforcehunt) treasure_hunt_count = diff.triforce_pieces_required @@ -636,7 +635,8 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s treasure_hunt_count = max(min(customitemarray[67], 99), 1) #To display, count must be between 1 and 99. treasure_hunt_icon = 'Triforce Piece' # Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling. - if (customitemarray[66] < treasure_hunt_count) and (goal == 'triforcehunt') and (customitemarray[68] == 0): + if (customitemarray[66] < treasure_hunt_count) and (goal in {'triforcehunt', 'localtriforcehunt'}) and ( + customitemarray[68] == 0): extrapieces = treasure_hunt_count - customitemarray[66] pool.extend(['Triforce Piece'] * extrapieces) itemtotal = itemtotal + extrapieces diff --git a/Mystery.py b/Mystery.py index 821cafe7..a862baa2 100644 --- a/Mystery.py +++ b/Mystery.py @@ -283,7 +283,9 @@ def roll_settings(weights): 'fast_ganon': 'crystals', 'dungeons': 'dungeons', 'pedestal': 'pedestal', - 'triforce-hunt': 'triforcehunt' + 'triforce_hunt': 'triforcehunt', + 'triforce-hunt': 'triforcehunt', # deprecated, moving all goals to `_` + 'local_triforce_hunt': 'localtriforcehunt' }[goal] ret.openpyramid = goal == 'fast_ganon' diff --git a/Rom.py b/Rom.py index 8d4d6092..73e12301 100644 --- a/Rom.py +++ b/Rom.py @@ -1097,7 +1097,7 @@ def patch_rom(world, rom, player, team, enemized): (0x02 if 'bombs' in world.escape_assist[player] else 0x00) | (0x04 if 'magic' in world.escape_assist[player] else 0x00))) # Escape assist - if world.goal[player] in ['pedestal', 'triforcehunt']: + if world.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt']: rom.write_byte(0x18003E, 0x01) # make ganon invincible elif world.goal[player] in ['dungeons']: rom.write_byte(0x18003E, 0x02) # make ganon invincible until all dungeons are beat @@ -1776,11 +1776,13 @@ def write_strings(rom, world, player, team): tt['sahasrahla_quest_have_master_sword'] = Sahasrahla2_texts[random.randint(0, len(Sahasrahla2_texts) - 1)] tt['blind_by_the_light'] = Blind_texts[random.randint(0, len(Blind_texts) - 1)] - if world.goal[player] in ['triforcehunt']: + if world.goal[player] in ['triforcehunt', 'localtriforcehunt']: tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!' - 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." % world.treasure_hunt_count[player] + 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." % \ + world.treasure_hunt_count[player] elif world.goal[player] in ['pedestal']: tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' diff --git a/Rules.py b/Rules.py index 561210c9..7dfcf31c 100644 --- a/Rules.py +++ b/Rules.py @@ -137,7 +137,10 @@ def item_name(state, location, player): def global_rules(world, player): # ganon can only carry triforce add_item_rule(world.get_location('Ganon', player), lambda item: item.name == 'Triforce' and item.player == player) - + if world.goal[player] == "localtriforcehunt": + for location in world.get_locations(): + if location.player != player: + forbid_item(location, 'Triforce Piece', player) # determines which S&Q locations are available - hide from paths since it isn't an in-game location world.get_region('Menu', player).can_reach_private = lambda state: True for exit in world.get_region('Menu', player).exits: @@ -147,7 +150,8 @@ def global_rules(world, player): set_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player)) set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player)) - set_rule(world.get_location('Purple Chest', player), lambda state: state.has('Pick Up Purple Chest', player)) # Can S&Q with chest + set_rule(world.get_location('Purple Chest', player), + lambda state: state.has('Pick Up Purple Chest', player)) # Can S&Q with chest set_rule(world.get_location('Ether Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has_beam_sword(player)) set_rule(world.get_location('Master Sword Pedestal', player), lambda state: state.has('Red Pendant', player) and state.has('Blue Pendant', player) and state.has('Green Pendant', player)) diff --git a/Utils.py b/Utils.py index 0694fbb5..38f2c11b 100644 --- a/Utils.py +++ b/Utils.py @@ -1,6 +1,6 @@ from __future__ import annotations -__version__ = "2.2.1" +__version__ = "2.3.0" _version_tuple = tuple(int(piece, 10) for piece in __version__.split(".")) import os diff --git a/easy.yaml b/easy.yaml index 7a380e73..2fe3909f 100644 --- a/easy.yaml +++ b/easy.yaml @@ -73,7 +73,8 @@ goals: fast_ganon: 0 # Only killing Ganon is required. The hole is always open. Items may still be placed in GT, however dungeons: 0 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2) pedestal: 0 # Pull the Triforce from the Master Sword pedestal - triforce-hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the world, then turn them in to Murahadala in front of Hyrule Castle + triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle + local_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle tower_open: # Crystals required to open GT '0': 8 '1': 7