diff --git a/BaseClasses.py b/BaseClasses.py index 20d4f136..cf82fe8c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -5,7 +5,7 @@ from enum import Enum, unique import logging import json from collections import OrderedDict, Counter, deque -from typing import Union, Optional, List, Set +from typing import Union, Optional, List, Set, Dict import secrets import random @@ -20,6 +20,8 @@ class World(object): _region_cache: dict difficulty_requirements: dict required_medallions: dict + dark_room_logic: Dict[int, str] + restrict_dungeon_item_on_boss: Dict[int, bool] def __init__(self, players: int, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, @@ -126,6 +128,8 @@ class World(object): set_player_attr('shop_shuffle', 'off') set_player_attr('shuffle_prizes', "g") set_player_attr('sprite_pool', []) + set_player_attr('dark_room_logic', "lamp") + set_player_attr('restrict_dungeon_item_on_boss', False) def secure(self): self.random = secrets.SystemRandom() @@ -357,6 +361,9 @@ class World(object): location.player == player and not location.item] return [location for location in self.get_locations() if not location.item] + def get_unfilled_dungeon_locations(self): + return [location for location in self.get_locations() if not location.item and location.parent_region.dungeon] + def get_filled_locations(self, player=None) -> list: if player is not None: return [location for location in self.get_locations() if @@ -601,7 +608,6 @@ class CollectionState(object): def can_shoot_arrows(self, player: int) -> bool: if self.world.retro[player]: - # TODO: Progressive and Non-Progressive silvers work differently (progressive is not usable until the shop arrow is bought) return (self.has('Bow', player) or self.has('Silver Bow', player)) and self.can_buy('Single Arrow', player) return self.has('Bow', player) or self.has('Silver Bow', player) @@ -615,6 +621,11 @@ class CollectionState(object): self.is_not_bunny(cave, player) ) + def can_retrieve_tablet(self, player:int) -> bool: + return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or + ((self.world.swords[player] == "swordless" or self.world.difficulty_adjustments[player] == "easy") and + self.has("Hammer", player))) + def has_sword(self, player: int) -> bool: return self.has('Fighter Sword', player) \ or self.has('Master Sword', player) \ @@ -644,7 +655,10 @@ class CollectionState(object): return self.has('Flute', player) and lw.can_reach(self) and self.is_not_bunny(lw, player) def can_melt_things(self, player: int) -> bool: - return self.has('Fire Rod', player) or (self.has('Bombos', player) and (self.has_sword(player) or self.world.swords[player] == "swordless")) + return self.has('Fire Rod', player) or \ + (self.has('Bombos', player) and + (self.world.difficulty_adjustments[player] == "easy" or self.world.swords[player] == "swordless" or + self.has_sword(player))) def can_avoid_lasers(self, player: int) -> bool: return self.has('Mirror Shield', player) or self.has('Cane of Byrna', player) or self.has('Cape', player) @@ -1260,6 +1274,7 @@ class Spoiler(object): from Utils import __version__ as ERVersion self.metadata = {'version': ERVersion, 'logic': self.world.logic, + 'dark_room_logic': self.world.dark_room_logic, 'mode': self.world.mode, 'retro': self.world.retro, 'weapons': self.world.swords, @@ -1293,7 +1308,8 @@ class Spoiler(object): 'triforce_pieces_required': self.world.triforce_pieces_required, 'shop_shuffle': self.world.shop_shuffle, 'shuffle_prizes': self.world.shuffle_prizes, - 'sprite_pool': self.world.sprite_pool + 'sprite_pool': self.world.sprite_poolm, + 'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss } def to_json(self): @@ -1337,6 +1353,9 @@ class Spoiler(object): f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if self.world.teams > 1 else 'Hash: ', self.hashes[player, team])) outfile.write('Logic: %s\n' % self.metadata['logic'][player]) + outfile.write('Dark Room Logic: %s\n' % self.metadata['dark_room_logic'][player]) + outfile.write('Restricted Boss Drops: %s\n' % + bool_to_text(self.metadata['restrict_dungeon_item_on_boss'][player])) if self.world.players > 1: outfile.write('Progression Balanced: %s\n' % ( 'Yes' if self.metadata['progression_balancing'][player] else 'No')) diff --git a/Bosses.py b/Bosses.py index 00b4c4dd..f4e7156a 100644 --- a/Bosses.py +++ b/Bosses.py @@ -116,6 +116,27 @@ def AgahnimDefeatRule(state, player: int): return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player) +def GanonDefeatRule(state, player: int): + if state.world.swords[player] == "swordless": + return state.has('Hammer', player) and \ + state.has_fire_source(player) and \ + state.has('Silver Bow', player) and \ + state.can_shoot_arrows(player) + easy_hammer = state.world.difficulty_adjustments[player] == "easy" and state.has("Hammer", player) and \ + state.has('Silver Bow', player) and state.can_shoot_arrows(player) + can_hurt = state.has_beam_sword(player) or easy_hammer + common = can_hurt and state.has_fire_source(player) + # silverless ganon may be needed in minor glitches + if state.world.logic[player] in {"owglitches", "minorglitches", "none"}: + # need to light torch a sufficient amount of times + return common and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or ( + state.has('Silver Bow', player) and state.can_shoot_arrows(player)) or + state.has('Lamp', player) or state.can_extend_magic(player, 12)) + + else: + return common and state.has('Silver Bow', player) and state.can_shoot_arrows(player) + + boss_table = { 'Armos Knights': ('Armos', ArmosKnightsDefeatRule), 'Lanmolas': ('Lanmola', LanmolasDefeatRule), diff --git a/Dungeons.py b/Dungeons.py index 87c304ad..9e4111e8 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -2,6 +2,7 @@ from BaseClasses import Dungeon from Bosses import BossFactory from Fill import fill_restrictive from Items import ItemFactory +from Regions import lookup_boss_drops def create_dungeons(world, player): @@ -116,7 +117,14 @@ def fill_dungeons(world): def get_dungeon_item_pool(world): return [item for dungeon in world.dungeons for item in dungeon.all_items] -def fill_dungeons_restrictive(world, shuffled_locations): +def fill_dungeons_restrictive(world): + """Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside.""" + restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if restricted} + + locations = [location for location in world.get_unfilled_dungeon_locations() + if not (location.player in restricted_players and location.name in lookup_boss_drops)] # filter boss + + world.random.shuffle(locations) all_state_base = world.get_all_state() # with shuffled dungeon items they are distributed as part of the normal item pool @@ -131,13 +139,11 @@ def fill_dungeons_restrictive(world, shuffled_locations): or (item.bigkey and not world.bigkeyshuffle[item.player]) or (item.map and not world.mapshuffle[item.player]) or (item.compass and not world.compassshuffle[item.player]))] - - # sort in the order Big Key, Small Key, Other before placing dungeon items - sort_order = {"BigKey": 3, "SmallKey": 2} - dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1)) - - fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items, True) - + if dungeon_items: + # sort in the order Big Key, Small Key, Other before placing dungeon items + sort_order = {"BigKey": 3, "SmallKey": 2} + dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1)) + fill_restrictive(world, all_state_base, locations, dungeon_items, True) dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index 8ab9e097..064f96b1 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -317,6 +317,11 @@ def parse_arguments(argv, no_defaults=False): parser.add_argument('--shuffle_prizes', default=defval('g'), choices=['', 'g', 'b', 'gb']) parser.add_argument('--sprite_pool', help='''\ Specifies a colon separated list of sprites used for random/randomonevent. If not specified, the full sprite pool is used.''') + parser.add_argument('--dark_room_logic', default=('Lamp'), choices=["lamp", "torches", "none"], help='''\ + For unlit dark rooms, require the Lamp to be considered in logic by default. + Torches means additionally easily accessible Torches that can be lit with Fire Rod are considered doable. + None means full traversal through dark rooms without tools is considered doable.''') + parser.add_argument('--restrict_dungeon_item_on_boss', default=defval(False), action="store_true") parser.add_argument('--remote_items', default=defval(False), action='store_true') parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--names', default=defval('')) @@ -362,7 +367,7 @@ def parse_arguments(argv, no_defaults=False): 'heartbeep', "skip_progression_balancing", "triforce_pieces_available", "triforce_pieces_required", "shop_shuffle", 'remote_items', 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves', - 'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool']: + 'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic', 'restrict_dungeon_item_on_boss']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) diff --git a/Gui.py b/Gui.py index 861f3d9d..0cd476bf 100755 --- a/Gui.py +++ b/Gui.py @@ -256,6 +256,15 @@ def guiMain(args=None): logicLabel = Label(logicFrame, text='Game logic') logicLabel.pack(side=LEFT) + darklogicFrame = Frame(drowDownFrame) + darklogicVar = StringVar() + darklogicVar.set('Lamp') + darklogicOptionMenu = OptionMenu(darklogicFrame, darklogicVar, 'Lamp', 'Lamp or easy Fire Rod torches', + 'dark traversal') + darklogicOptionMenu.pack(side=RIGHT) + darklogicLabel = Label(darklogicFrame, text='Dark Room Logic') + darklogicLabel.pack(side=LEFT) + goalFrame = Frame(drowDownFrame) goalVar = StringVar() goalVar.set('ganon') @@ -366,6 +375,7 @@ def guiMain(args=None): modeFrame.pack(expand=True, anchor=E) logicFrame.pack(expand=True, anchor=E) + darklogicFrame.pack(expand=True, anchor=E) goalFrame.pack(expand=True, anchor=E) crystalsGTFrame.pack(expand=True, anchor=E) crystalsGanonFrame.pack(expand=True, anchor=E) @@ -488,6 +498,9 @@ def guiMain(args=None): guiargs.count = int(countVar.get()) if countVar.get() != '1' else None guiargs.mode = modeVar.get() guiargs.logic = logicVar.get() + guiargs.dark_room_logic = {"Lamp": "lamp", + "Lamp or easy Fire Rod torches": "torches", + "dark traversal": "none"}[darklogicVar.get()] guiargs.goal = goalVar.get() guiargs.crystals_gt = crystalsGTVar.get() guiargs.crystals_ganon = crystalsGanonVar.get() diff --git a/ItemPool.py b/ItemPool.py index 1c99ce39..f531dcca 100644 --- a/ItemPool.py +++ b/ItemPool.py @@ -608,13 +608,13 @@ def get_pool_core(world, player: int): if want_progressives(): pool.extend(diff.progressivebow) - elif swords != 'swordless': - pool.extend(diff.basicbow) - else: + elif swords == 'swordless' or logic == 'noglitches': swordless_bows = ['Bow', 'Silver Bow'] if difficulty == "easy": swordless_bows *= 2 pool.extend(swordless_bows) + else: + pool.extend(diff.basicbow) if swords == 'swordless': pool.extend(diff.swordless) diff --git a/Main.py b/Main.py index ad71d6da..75d2bf19 100644 --- a/Main.py +++ b/Main.py @@ -82,6 +82,8 @@ def main(args, seed=None): world.progression_balancing = {player: not balance for player, balance in args.skip_progression_balancing.items()} world.shuffle_prizes = args.shuffle_prizes.copy() world.sprite_pool = args.sprite_pool.copy() + world.dark_room_logic = args.dark_room_logic.copy() + world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy() world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)} @@ -100,9 +102,6 @@ def main(args, seed=None): for player in range(1, world.players + 1): world.difficulty_requirements[player] = difficulties[world.difficulty[player]] - if world.mode[player] == 'standard' and world.enemy_shuffle[player] != 'none': - world.escape_assist[player].append('bombs') # enemized escape assumes infinite bombs available and will likely be unbeatable without it - for tok in filter(None, args.startinventory[player].split(',')): item = ItemFactory(tok.strip(), player) if item: @@ -151,9 +150,7 @@ def main(args, seed=None): shuffled_locations = None if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) + list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())): - shuffled_locations = world.get_unfilled_locations() - world.random.shuffle(shuffled_locations) - fill_dungeons_restrictive(world, shuffled_locations) + fill_dungeons_restrictive(world) else: fill_dungeons(world) @@ -373,6 +370,9 @@ def copy_world(world): ret.beemizer = world.beemizer.copy() ret.timer = world.timer.copy() ret.shufflepots = world.shufflepots.copy() + ret.shuffle_prizes = world.shuffle_prizes.copy() + ret.dark_room_logic = world.dark_room_logic.copy() + ret.restrict_dungeon_item_on_boss = world.restrict_dungeon_item_on_boss.copy() for player in range(1, world.players + 1): if world.mode[player] != 'inverted': diff --git a/Mystery.py b/Mystery.py index 70871d5a..3be2bb30 100644 --- a/Mystery.py +++ b/Mystery.py @@ -275,6 +275,17 @@ def roll_settings(weights): ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches', 'minor_glitches': 'minorglitches'}[ glitches_required] + + ret.dark_room_logic = get_choice("dark_room_logic", weights, "lamp") + if not ret.dark_room_logic: # None/False + ret.dark_room_logic = "none" + if ret.dark_room_logic == "sconces": + ret.dark_room_logic = "torches" + if ret.dark_room_logic not in {"lamp", "torches", "none"}: + raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"") + + ret.restrict_dungeon_item_on_boss = get_choice('restrict_dungeon_item_on_boss', weights, False) + ret.progression_balancing = get_choice('progression_balancing', weights, True) # item_placement = get_choice('item_placement') # not supported in ER diff --git a/Regions.py b/Regions.py index aedc2604..58049039 100644 --- a/Regions.py +++ b/Regions.py @@ -746,3 +746,6 @@ lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 19125 60127: 'Ganons Tower', 60118: 'Ganons Tower', 60148: 'Ganons Tower', 60151: 'Ganons Tower', 60145: 'Ganons Tower', 60157: 'Ganons Tower', 60160: 'Ganons Tower', 60163: 'Ganons Tower', 60166: 'Ganons Tower'} + +lookup_prizes = {location for location in location_table if location.endswith(" - Prize")} +lookup_boss_drops = {location for location in location_table if location.endswith(" - Boss")} \ No newline at end of file diff --git a/Rom.py b/Rom.py index d93c664a..ae32f3e6 100644 --- a/Rom.py +++ b/Rom.py @@ -802,7 +802,7 @@ def patch_rom(world, rom, player, team, enemized): #Work around for json patch ordering issues - write bow limit separately so that it is replaced in the patch rom.write_bytes(0x180098, [difficulty.progressive_bow_limit, overflow_replacement]) - if difficulty.progressive_bow_limit < 2 and world.swords[player] == 'swordless': + if difficulty.progressive_bow_limit < 2 and (world.swords[player] == 'swordless' or world.logic[player] == 'noglitches'): rom.write_bytes(0x180098, [2, overflow_replacement]) rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup @@ -1880,7 +1880,7 @@ def write_strings(rom, world, player, team): prog_bow_locs = world.find_items('Progressive Bow', player) distinguished_prog_bow_loc = next((location for location in prog_bow_locs if location.item.code == 0x65), None) - progressive_silvers = world.difficulty_requirements[player].progressive_bow_limit >= 2 or world.swords[player] == 'swordless' + progressive_silvers = world.difficulty_requirements[player].progressive_bow_limit >= 2 or (world.swords[player] == 'swordless' or world.logic[player] == 'noglitches') if distinguished_prog_bow_loc: prog_bow_locs.remove(distinguished_prog_bow_loc) silverarrow_hint = (' %s?' % hint_text(distinguished_prog_bow_loc).replace('Ganon\'s', 'my')) if progressive_silvers else '?\nI think not!' diff --git a/Rules.py b/Rules.py index b0c17c48..2af3d301 100644 --- a/Rules.py +++ b/Rules.py @@ -4,6 +4,7 @@ import OverworldGlitchRules from BaseClasses import RegionType, World, Entrance from Items import ItemFactory, progression_items, item_name_groups from OverworldGlitchRules import overworld_glitches_rules, no_logic_rules +from Bosses import GanonDefeatRule def set_rules(world, player): @@ -19,7 +20,8 @@ def set_rules(world, player): exit.hide_path = True return else: - # Set access rules according to max glitches for multiworld progression. Set accessibility to none, and shuffle assuming the no logic players can always win + # Set access rules according to max glitches for multiworld progression. + # Set accessibility to none, and shuffle assuming the no logic players can always win world.accessibility[player] = 'none' world.progression_balancing[player] = False @@ -122,8 +124,18 @@ def add_rule(spot, rule, combine='and'): spot.access_rule = lambda state: rule(state) and old_rule(state) -def add_lamp_requirement(spot, player): - add_rule(spot, lambda state: state.has('Lamp', player)) +def add_lamp_requirement(world: World, spot, player: int, has_accessible_torch: bool = False): + if world.dark_room_logic[player] == "lamp": + add_rule(spot, lambda state: state.has('Lamp', player)) + elif world.dark_room_logic[player] == "torch": # implicitly lamp as well + if has_accessible_torch: + add_rule(spot, lambda state: state.has('Lamp', player) or state.has('Fire Rod', player)) + else: + add_rule(spot, lambda state: state.has('Lamp', player)) + elif world.dark_room_logic[player] == "none": + pass + else: + raise ValueError(f"Unknown Dark Room Logic: {world.dark_room_logic[player]}") def forbid_item(location, item, player: int): @@ -131,10 +143,15 @@ def forbid_item(location, item, player: int): location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i) -def forbid_items(location, items: set, player: int): +def forbid_items_for_player(location, items: set, player: int): old_rule = location.item_rule location.item_rule = lambda i: (i.player != player or i.name not in items) and old_rule(i) +def forbid_items(location, items: set): + """unused, but kept as a debugging tool.""" + old_rule = location.item_rule + location.item_rule = lambda i: i.name not in items and old_rule(i) + def add_item_rule(location, rule): old_rule = location.item_rule @@ -161,7 +178,7 @@ def locality_rules(world, player): if world.local_items[player]: for location in world.get_locations(): if location.player != player: - forbid_items(location, world.local_items[player], player) + forbid_items_for_player(location, world.local_items[player], player) non_crossover_items = (item_name_groups["Small Keys"] | item_name_groups["Big Keys"] | progression_items) - { @@ -182,7 +199,7 @@ def global_rules(world, 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('Ether Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has_beam_sword(player)) + set_rule(world.get_location('Ether Tablet', player), lambda state: state.can_retrieve_tablet(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)) set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith @@ -225,13 +242,16 @@ def global_rules(world, player): set_rule(world.get_location('Eastern Palace - Big Chest', player), lambda state: state.has('Big Key (Eastern Palace)', player)) set_rule(world.get_location('Eastern Palace - Boss', player), - lambda state: state.can_shoot_arrows(player) and state.has('Big Key (Eastern Palace)', - player) and state.world.get_location( + lambda state: state.has('Big Key (Eastern Palace)', player) and state.world.get_location( 'Eastern Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state)) set_rule(world.get_location('Eastern Palace - Prize', player), - lambda state: state.can_shoot_arrows(player) and state.has('Big Key (Eastern Palace)', - player) and state.world.get_location( + lambda state: state.has('Big Key (Eastern Palace)', player) and state.world.get_location( 'Eastern Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state)) + if not world.enemy_shuffle[player]: + add_rule(world.get_location('Eastern Palace - Boss', player), + lambda state: state.can_shoot_arrows(player)) + add_rule(world.get_location('Eastern Palace - Prize', player), + lambda state: state.can_shoot_arrows(player)) set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player)) set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has_Boots(player)) @@ -328,7 +348,8 @@ def global_rules(world, player): set_defeat_dungeon_boss_rule(world.get_location('Turtle Rock - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Turtle Rock - Prize', player)) - set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: state.can_shoot_arrows(player)) + if not world.enemy_shuffle[player]: + set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and state.can_shoot_arrows(player) and state.has('Hammer', player)) @@ -377,23 +398,33 @@ def global_rules(world, player): set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player)) - set_rule(world.get_location('Ganons Tower - Big Key Room - Left', player), lambda state: state.world.get_location('Ganons Tower - Big Key Room - Left', player).parent_region.dungeon.bosses['bottom'].can_defeat(state)) - set_rule(world.get_location('Ganons Tower - Big Key Chest', player), lambda state: state.world.get_location('Ganons Tower - Big Key Chest', player).parent_region.dungeon.bosses['bottom'].can_defeat(state)) - set_rule(world.get_location('Ganons Tower - Big Key Room - Right', player), lambda state: state.world.get_location('Ganons Tower - Big Key Room - Right', player).parent_region.dungeon.bosses['bottom'].can_defeat(state)) - - set_rule(world.get_entrance('Ganons Tower Big Key Door', player), lambda state: state.has('Big Key (Ganons Tower)', player) and state.can_shoot_arrows(player)) - set_rule(world.get_entrance('Ganons Tower Torch Rooms', player), lambda state: state.has_fire_source(player) and state.world.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state)) - set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3)) - set_rule(world.get_entrance('Ganons Tower Moldorm Door', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 4)) - set_rule(world.get_entrance('Ganons Tower Moldorm Gap', player), lambda state: state.has('Hookshot', player) and state.world.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state)) - set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player)) - - if world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']: - set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has_triforce_pieces(world.treasure_hunt_count[player], player) - and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Bow', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times + set_rule(world.get_location('Ganons Tower - Big Key Room - Left', player), + lambda state: state.world.get_location('Ganons Tower - Big Key Room - Left', player).parent_region.dungeon.bosses['bottom'].can_defeat(state)) + set_rule(world.get_location('Ganons Tower - Big Key Chest', player), + lambda state: state.world.get_location('Ganons Tower - Big Key Chest', player).parent_region.dungeon.bosses['bottom'].can_defeat(state)) + set_rule(world.get_location('Ganons Tower - Big Key Room - Right', player), + lambda state: state.world.get_location('Ganons Tower - Big Key Room - Right', player).parent_region.dungeon.bosses['bottom'].can_defeat(state)) + if world.enemy_shuffle[player]: + set_rule(world.get_entrance('Ganons Tower Big Key Door', player), + lambda state: state.has('Big Key (Ganons Tower)', player)) else: - set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has_crystals(world.crystals_needed_for_ganon[player], player) - and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Bow', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times + set_rule(world.get_entrance('Ganons Tower Big Key Door', player), + lambda state: state.has('Big Key (Ganons Tower)', player) and state.can_shoot_arrows(player)) + set_rule(world.get_entrance('Ganons Tower Torch Rooms', player), + lambda state: state.has_fire_source(player) and state.world.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state)) + set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player), + lambda state: state.has_key('Small Key (Ganons Tower)', player, 3)) + set_rule(world.get_entrance('Ganons Tower Moldorm Door', player), + lambda state: state.has_key('Small Key (Ganons Tower)', player, 4)) + set_rule(world.get_entrance('Ganons Tower Moldorm Gap', player), + lambda state: state.has('Hookshot', player) and state.world.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state)) + set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player)) + ganon = world.get_location('Ganon', player) + set_rule(ganon, lambda state: GanonDefeatRule(state, player)) + if world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']: + add_rule(ganon, lambda state: state.has_triforce_pieces(world.treasure_hunt_count[player], player)) + else: + add_rule(ganon, lambda state: state.has_crystals(world.crystals_needed_for_ganon[player], player)) set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop @@ -452,7 +483,7 @@ def default_rules(world, player): set_rule(world.get_entrance('Hyrule Castle Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Hyrule Castle Main Gate', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: (state.has_Pearl(player) and state.has('Flippers', player) or state.has_Mirror(player))) # Overworld Bunny Revival - set_rule(world.get_location('Bombos Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has_beam_sword(player)) + set_rule(world.get_location('Bombos Tablet', player), lambda state: state.can_retrieve_tablet(player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player)) # ToDo any fake flipper set up? set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has_Pearl(player)) # bomb required set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: state.can_lift_rocks(player) and state.has_Pearl(player)) @@ -586,7 +617,7 @@ def inverted_rules(world, player): set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has_Boots(player)) set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_location('Bombos Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has_beam_sword(player)) + set_rule(world.get_location('Bombos Tablet', player), lambda state: state.can_retrieve_tablet(player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Flippers', player)) # ToDo any fake flipper set up? set_rule(world.get_entrance('Dark Lake Hylia Ledge Pier', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: state.can_lift_rocks(player)) @@ -740,41 +771,44 @@ def add_conditional_lamps(world, player): # Light cones in standard depend on which world we actually are in, not which one the location would normally be # We add Lamp requirements only to those locations which lie in the dark world (or everything if open - def add_conditional_lamp(spot, region, spottype='Location'): - if spottype == 'Location': - spot = world.get_location(spot, player) - else: - spot = world.get_entrance(spot, player) - if (not world.dark_world_light_cone and check_is_dark_world(world.get_region(region, player))) or (not world.light_world_light_cone and not check_is_dark_world(world.get_region(region, player))): - add_lamp_requirement(spot, player) + def add_conditional_lamp(spot, region, spottype='Location', accessible_torch=False): + if (not world.dark_world_light_cone and check_is_dark_world(world.get_region(region, player))) or ( + not world.light_world_light_cone and not check_is_dark_world(world.get_region(region, player))): + if spottype == 'Location': + spot = world.get_location(spot, player) + else: + spot = world.get_entrance(spot, player) + add_lamp_requirement(world, spot, player, accessible_torch) add_conditional_lamp('Misery Mire (Vitreous)', 'Misery Mire (Entrance)', 'Entrance') add_conditional_lamp('Turtle Rock (Dark Room) (North)', 'Turtle Rock (Entrance)', 'Entrance') add_conditional_lamp('Turtle Rock (Dark Room) (South)', 'Turtle Rock (Entrance)', 'Entrance') add_conditional_lamp('Palace of Darkness Big Key Door', 'Palace of Darkness (Entrance)', 'Entrance') add_conditional_lamp('Palace of Darkness Maze Door', 'Palace of Darkness (Entrance)', 'Entrance') - add_conditional_lamp('Palace of Darkness - Dark Basement - Left', 'Palace of Darkness (Entrance)', 'Location') - add_conditional_lamp('Palace of Darkness - Dark Basement - Right', 'Palace of Darkness (Entrance)', 'Location') + add_conditional_lamp('Palace of Darkness - Dark Basement - Left', 'Palace of Darkness (Entrance)', + 'Location', True) + add_conditional_lamp('Palace of Darkness - Dark Basement - Right', 'Palace of Darkness (Entrance)', + 'Location', True) if world.mode[player] != 'inverted': add_conditional_lamp('Agahnim 1', 'Agahnims Tower', 'Entrance') - add_conditional_lamp('Castle Tower - Dark Maze', 'Agahnims Tower', 'Location') + add_conditional_lamp('Castle Tower - Dark Maze', 'Agahnims Tower') else: add_conditional_lamp('Agahnim 1', 'Inverted Agahnims Tower', 'Entrance') - add_conditional_lamp('Castle Tower - Dark Maze', 'Inverted Agahnims Tower', 'Location') - add_conditional_lamp('Old Man', 'Old Man Cave', 'Location') + add_conditional_lamp('Castle Tower - Dark Maze', 'Inverted Agahnims Tower') + add_conditional_lamp('Old Man', 'Old Man Cave') add_conditional_lamp('Old Man Cave Exit (East)', 'Old Man Cave', 'Entrance') add_conditional_lamp('Death Mountain Return Cave Exit (East)', 'Death Mountain Return Cave', 'Entrance') add_conditional_lamp('Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave', 'Entrance') add_conditional_lamp('Old Man House Front to Back', 'Old Man House', 'Entrance') add_conditional_lamp('Old Man House Back to Front', 'Old Man House', 'Entrance') - add_conditional_lamp('Eastern Palace - Big Key Chest', 'Eastern Palace', 'Location') - add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location') - add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location') + add_conditional_lamp('Eastern Palace - Big Key Chest', 'Eastern Palace') + add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location', True) + add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location', True) if not world.sewer_light_cone[player]: - add_lamp_requirement(world.get_location('Sewers - Dark Cross', player), player) - add_lamp_requirement(world.get_entrance('Sewers Back Door', player), player) - add_lamp_requirement(world.get_entrance('Throne Room', player), player) + add_lamp_requirement(world, world.get_location('Sewers - Dark Cross', player), player) + add_lamp_requirement(world, world.get_entrance('Sewers Back Door', player), player) + add_lamp_requirement(world, world.get_entrance('Throne Room', player), player) def open_rules(world, player): @@ -786,27 +820,19 @@ def open_rules(world, player): def swordless_rules(world, player): - set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or state.can_shoot_arrows(player) or state.has('Cane of Somaria', player)) and state.has_key('Small Key (Agahnims Tower)', player, 2)) - set_rule(world.get_location('Ether Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has('Hammer', player)) set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace - if world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']: - set_rule(world.get_location('Ganon', player), lambda state: state.has('Hammer', player) and state.has_fire_source(player) and state.has('Silver Bow', player) and state.can_shoot_arrows(player) and state.has_triforce_pieces(world.treasure_hunt_count[player], player)) - else: - set_rule(world.get_location('Ganon', player), lambda state: state.has('Hammer', player) and state.has_fire_source(player) and state.has('Silver Bow', player) and state.can_shoot_arrows(player) and state.has_crystals(world.crystals_needed_for_ganon[player], player)) set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop if world.mode[player] != 'inverted': set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_Pearl(player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_Pearl(player) and state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!) - set_rule(world.get_location('Bombos Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has('Hammer', player) and state.has_Mirror(player)) else: # only need ddm access for aga tower in inverted set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!) - set_rule(world.get_location('Bombos Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has('Hammer', player)) def add_connection(parent_name, target_name, entrance_name, world, player): diff --git a/playerSettings.yaml b/playerSettings.yaml index aebc18fa..4a1141d6 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -19,12 +19,21 @@ description: Template Name # Used to describe your yaml. Useful if you have multiple files name: YourName # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit +### Logic Section ### glitches_required: # Determine the logic required to complete the seed none: 50 # No glitches required minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches (fake flipper, super bunny shenanigans, water walk and etc.) no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx. # Other players items are placed into your world under OWG logic +dark_room_logic: # Logic for unlit dark rooms + lamp: 50 # require the Lamp for these rooms to be considered accessible. + sconces: 0 # in addition to lamp, allow the fire rod and presence of easily accessible sconces for access + none: 0 # all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness +restrict_dungeon_item_on_boss: # aka ambrosia boss items + on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items + off: 50 +### End of Logic Section ### meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option here guarantees it will not occur in your seed, even if the .yaml file specifies it mode: - inverted # Never play inverted seeds @@ -305,8 +314,8 @@ rom: on: 0 off: 50 quickswap: # Enable switching items by pressing the L+R shoulder buttons - on: 0 - off: 50 + on: 50 + off: 0 menuspeed: # Controls how fast the item menu opens and closes normal: 50 instant: 0