From 1247716e9288c9acecd8fa92bd8988f27394215b Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 24 Dec 2024 11:49:24 -0600 Subject: [PATCH] Implemented District/Nearby Dungeon Item Shuffle --- BaseClasses.py | 85 ++++++++++++------- CLI.py | 9 +- DoorShuffle.py | 8 +- DungeonGenerator.py | 14 +-- Fill.py | 57 ++++++++++--- ItemList.py | 23 ++--- KeyDoorShuffle.py | 18 ++-- README.md | 8 ++ Rom.py | 58 +++++++------ Rules.py | 2 +- mystery_example.yml | 17 ++-- resources/app/cli/args.json | 22 +++-- resources/app/gui/lang/en.json | 11 +++ .../app/gui/randomize/dungeon/keysanity.json | 37 +++++++- .../app/gui/randomize/dungeon/widgets.json | 1 + source/classes/CustomSettings.py | 9 +- source/dungeon/DungeonStitcher.py | 4 +- source/gui/randomize/dungeon.py | 14 ++- source/item/District.py | 6 ++ source/item/FillUtil.py | 23 ++--- source/tools/MysteryUtils.py | 17 +++- 21 files changed, 295 insertions(+), 148 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 8869ed53..98f20269 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -139,10 +139,10 @@ class World(object): set_player_attr('can_access_trock_middle', None) set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'hybridglitches', 'nologic'] or shuffle[player] in ['lean', 'swapped', 'crossed', 'insanity']) - set_player_attr('mapshuffle', False) - set_player_attr('compassshuffle', False) + set_player_attr('mapshuffle', 'none') + set_player_attr('compassshuffle', 'none') set_player_attr('keyshuffle', 'none') - set_player_attr('bigkeyshuffle', False) + set_player_attr('bigkeyshuffle', 'none') set_player_attr('prizeshuffle', 'none') set_player_attr('restrict_boss_items', 'none') set_player_attr('bombbag', False) @@ -477,7 +477,7 @@ class World(object): item.world = self if ((item.prize and self.prizeshuffle[item.player] != 'none') or (item.smallkey and self.keyshuffle[item.player] != 'none') - or (item.bigkey and self.bigkeyshuffle[item.player])): + or (item.bigkey and self.bigkeyshuffle[item.player] != 'none')): item.advancement = True self.precollected_items.append(item) self.state.collect(item, True) @@ -1100,7 +1100,7 @@ class CollectionState(object): new_locations = True while new_locations: reachable_events = [location for location in locations if location.event and - (not key_only or (self.world.keyshuffle[location.item.player] == 'none' and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey)) + (not key_only or (self.world.keyshuffle[location.item.player] in ['none', 'district'] and location.item.smallkey) or (self.world.bigkeyshuffle[location.item.player] in ['none', 'district'] and location.item.bigkey)) and location.can_reach(self)] reachable_events = self._do_not_flood_the_keys(reachable_events) new_locations = False @@ -1577,6 +1577,7 @@ class Region(object): self.exits = [] self.locations = [] self.dungeon = None + self.districts = [] self.shop = None self.world = None self.is_light_world = False # will be set aftermaking connections. @@ -1607,10 +1608,16 @@ class Region(object): return False def can_fill(self, item): + if item.is_near_dungeon_item(self.world): + item_dungeon = self.world.get_dungeon(item.dungeon, self.player) if item.dungeon else item.dungeon_object + ret = (self.dungeon and self.dungeon.is_dungeon_item(item)) + ret = ret or (len(self.districts) and item_dungeon and len([d for d in self.districts if d in item_dungeon.districts])) + return ret and item.player == self.player + inside_dungeon_item = ((item.smallkey and self.world.keyshuffle[item.player] == 'none') - or (item.bigkey and not self.world.bigkeyshuffle[item.player]) - or (item.map and not self.world.mapshuffle[item.player]) - or (item.compass and not self.world.compassshuffle[item.player]) + or (item.bigkey and self.world.bigkeyshuffle[item.player] == 'none') + or (item.map and self.world.mapshuffle[item.player] == 'none') + or (item.compass and self.world.compassshuffle[item.player] == 'none') or (item.prize and self.world.prizeshuffle[item.player] == 'dungeon')) # not all small keys to escape must be in escape # sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)' @@ -1859,6 +1866,7 @@ class Dungeon(object): def __init__(self, name, regions, big_key, small_keys, dungeon_items, player, dungeon_id): self.name = name self.regions = regions + self.districts = [] self.prize = None self.big_key = big_key self.small_keys = small_keys @@ -1887,8 +1895,8 @@ class Dungeon(object): return self.dungeon_items + self.keys + ([self.prize] if self.prize else []) def is_dungeon_item(self, item): - if item.prize: - return item.player == self.player and self.prize is None and self.name not in ['Hyrule Castle', 'Agahnims Tower', 'Ganons Tower'] + if item.prize and item.dungeon is None: + return item.player == self.player and self.name not in ['Hyrule Castle', 'Agahnims Tower', 'Ganons Tower'] else: return item.player == self.player and item.name in [dungeon_item.name for dungeon_item in self.all_items] @@ -2757,6 +2765,7 @@ class Item(object): self.code = code self.price = price self.location = None + self.dungeon_object = None self.world = None self.player = player @@ -2792,9 +2801,16 @@ class Item(object): def is_inside_dungeon_item(self, world): return ((self.prize and world.prizeshuffle[self.player] in ['none', 'dungeon']) or (self.smallkey and world.keyshuffle[self.player] == 'none') - or (self.bigkey and not world.bigkeyshuffle[self.player]) - or (self.compass and not world.compassshuffle[self.player]) - or (self.map and not world.mapshuffle[self.player])) + or (self.bigkey and world.bigkeyshuffle[self.player] == 'none') + or (self.compass and world.compassshuffle[self.player] == 'none') + or (self.map and world.mapshuffle[self.player] == 'none')) + + def is_near_dungeon_item(self, world): + return ((self.prize and world.prizeshuffle[self.player] == 'district') + or (self.smallkey and world.keyshuffle[self.player] == 'district') + or (self.bigkey and world.bigkeyshuffle[self.player] == 'district') + or (self.compass and world.compassshuffle[self.player] == 'district') + or (self.map and world.mapshuffle[self.player] == 'district')) def get_map_location(self): if self.location: @@ -3266,10 +3282,10 @@ class Spoiler(object): outfile.write('Pyramid Hole Pre-opened:'.ljust(line_width) + '%s\n' % self.metadata['open_pyramid'][player]) outfile.write('Overworld Map:'.ljust(line_width) + '%s\n' % self.metadata['overworld_map'][player]) outfile.write('\n') - outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['mapshuffle'][player])) - outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['compassshuffle'][player])) + outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['mapshuffle'][player]) + outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['compassshuffle'][player]) outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['keyshuffle'][player]) - outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['bigkeyshuffle'][player])) + outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['bigkeyshuffle'][player]) outfile.write('Prize Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['prizeshuffle'][player]) outfile.write('Key Logic Algorithm:'.ljust(line_width) + '%s\n' % self.metadata['key_logic'][player]) outfile.write('\n') @@ -3627,9 +3643,11 @@ counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3} # byte 6: LCCC CPAA (shuffle links, crystals ganon, pyramid, access access_mode = {"items": 0, "locations": 1, "none": 2} -# byte 7: B?MC DDPP (big, ?, maps, compass, door_type, prize shuffle) -door_type_mode = {'original': 0, 'big': 1, 'all': 2, 'chaos': 3} -prizeshuffle_mode = {'none': 0, 'dungeon': 1, 'wild': 3} +# byte 7: MMCC SSBB (maps, compass, small, big) +mapshuffle_mode = {'none': 0, 'off': 0, 'district': 2, 'wild': 3, 'on': 3} +compassshuffle_mode = {'none': 0, 'off': 0, 'district': 2, 'wild': 3, 'on': 3} +keyshuffle_mode = {'none': 0, 'off': 0, 'universal': 1, 'district': 2, 'wild': 3, 'on': 3} +bigkeyshuffle_mode = {'none': 0, 'off': 0, 'district': 2, 'wild': 3, 'on': 3} # byte 8: HHHD DPEE (enemy_health, enemy_dmg, potshuffle, enemies) e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4} @@ -3651,11 +3669,11 @@ orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "unrestricted": 4} # byte 12: KMB? FF?? (keep similar, mixed/tile flip, bonk drops, flute spots) flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2} -# byte 13: FBBB TTSS (flute_mode, bow_mode, take_any, small_key_mode) +# byte 13: FBBB TTPP (flute_mode, bow_mode, take_any, prize shuffle) flute_mode = {'normal': 0, 'active': 1} bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} # reserved 8 modes? take_any_mode = {'none': 0, 'random': 1, 'fixed': 2} -keyshuffle_mode = {'none': 0, 'off': 0, 'wild': 1, 'on': 1, 'universal': 2} +prizeshuffle_mode = {'none': 0, 'dungeon': 1, 'district': 2, 'wild': 3} # additions # byte 14: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo) @@ -3663,9 +3681,10 @@ overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2} trap_door_mode = {'vanilla': 0, 'optional': 1, 'boss': 2, 'oneway': 3} key_logic_algo = {'dangerous': 0, 'partial': 1, 'strict': 2} -# byte 15: SSDD ???? (skullwoods, linked_drops, 4 free bytes) +# byte 15: SSLL ??DD (skullwoods, linked_drops, 2 free bytes, door_type) skullwoods_mode = {'original': 0, 'restricted': 1, 'loose': 2, 'followlinked': 3} linked_drops_mode = {'unset': 0, 'linked': 1, 'independent': 2} +door_type_mode = {'original': 0, 'big': 1, 'all': 2, 'chaos': 3} # sfx_shuffle and other adjust items does not affect settings code @@ -3700,9 +3719,8 @@ class Settings(object): (0x80 if w.shufflelinks[p] else 0) | ((8 if w.crystals_ganon_orig[p] == "random" else int(w.crystals_ganon_orig[p])) << 3) | (0x4 if w.is_pyramid_open(p) else 0) | access_mode[w.accessibility[p]], - (0x80 if w.bigkeyshuffle[p] else 0) - | (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0) - | (door_type_mode[w.door_type_mode[p]] << 2) | prizeshuffle_mode[w.prizeshuffle[p]], + (mapshuffle_mode[w.mapshuffle[p]] << 6) | (compassshuffle_mode[w.compassshuffle[p]] << 4) + | (keyshuffle_mode[w.keyshuffle[p]] << 2) | (bigkeyshuffle_mode[w.bigkeyshuffle[p]]), (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0) | (enemy_mode[w.enemy_shuffle[p]]), @@ -3718,12 +3736,13 @@ class Settings(object): | (0x20 if w.shuffle_bonk_drops[p] else 0) | (flutespot_mode[w.owFluteShuffle[p]] << 4), (flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4 - | take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]), + | take_any_mode[w.take_any[p]] << 2 | prizeshuffle_mode[w.prizeshuffle[p]]), ((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 5 | trap_door_mode[w.trap_door_mode[p]] << 3 | key_logic_algo[w.key_logic_algorithm[p]]), - (skullwoods_mode[w.skullwoods[p]] << 6 | linked_drops_mode[w.linked_drops[p]] << 4), + (skullwoods_mode[w.skullwoods[p]] << 6 | linked_drops_mode[w.linked_drops[p]] << 4 + | door_type_mode[w.door_type_mode[p]]), ]) return base64.b64encode(code, "+-".encode()).decode() @@ -3776,11 +3795,10 @@ class Settings(object): args.crystals_ganon[p] = "random" if cgan == 8 else cgan args.openpyramid[p] = True if settings[6] & 0x4 else False - args.bigkeyshuffle[p] = True if settings[7] & 0x80 else False - args.mapshuffle[p] = True if settings[7] & 0x20 else False - args.compassshuffle[p] = True if settings[7] & 0x10 else False - args.door_type_mode[p] = r(door_type_mode)[(settings[7] & 0xc) >> 2] - args.prizeshuffle[p] = r(prizeshuffle_mode)[settings[7] & 0x3] + args.mapshuffle[p] = r(mapshuffle_mode)[(settings[7] & 0xC0) >> 6] + args.compassshuffle[p] = r(compassshuffle_mode)[(settings[7] & 0x30) >> 4] + args.keyshuffle[p] = r(keyshuffle_mode)[(settings[7] & 0xC) >> 2] + args.bigkeyshuffle[p] = r(bigkeyshuffle_mode)[settings[7] & 0x3] args.enemy_health[p] = r(e_health)[(settings[8] & 0xE0) >> 5] args.enemy_damage[p] = r(e_dmg)[(settings[8] & 0x18) >> 3] @@ -3806,7 +3824,7 @@ class Settings(object): args.flute_mode[p] = r(flute_mode)[(settings[13] & 0x80) >> 7] args.bow_mode[p] = r(bow_mode)[(settings[13] & 0x70) >> 4] args.take_any[p] = r(take_any_mode)[(settings[13] & 0xC) >> 2] - args.keyshuffle[p] = r(keyshuffle_mode)[settings[13] & 0x3] + args.prizeshuffle[p] = r(prizeshuffle_mode)[settings[13] & 0x3] if len(settings) > 14: args.pseudoboots[p] = True if settings[14] & 0x80 else False @@ -3817,6 +3835,7 @@ class Settings(object): if len(settings) > 15: args.skullwoods[p] = r(skullwoods_mode)[(settings[15] & 0xc0) >> 6] args.linked_drops[p] = r(linked_drops_mode)[(settings[15] & 0x30) >> 4] + args.door_type_mode[p] = r(door_type_mode)[(settings[15] & 0x3)] class KeyRuleType(FastEnum): diff --git a/CLI.py b/CLI.py index 958a8da4..2d4f898e 100644 --- a/CLI.py +++ b/CLI.py @@ -106,8 +106,7 @@ def parse_cli(argv, no_defaults=False): ret = parser.parse_args(argv) if ret.keysanity: - ret.mapshuffle, ret.compassshuffle, ret.bigkeyshuffle = [True] * 3 - ret.keyshuffle = 'wild' + ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = 'wild' * 4 if ret.keydropshuffle: ret.dropshuffle = 'keys' if ret.dropshuffle == 'none' else ret.dropshuffle @@ -222,10 +221,10 @@ def parse_settings(): "pottery": "none", "colorizepots": True, "shufflepots": False, - "mapshuffle": False, - "compassshuffle": False, + "mapshuffle": "none", + "compassshuffle": "none", "keyshuffle": "none", - "bigkeyshuffle": False, + "bigkeyshuffle": "none", "prizeshuffle": "none", "keysanity": False, "door_shuffle": "vanilla", diff --git a/DoorShuffle.py b/DoorShuffle.py index 05dfcb55..71c90884 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -477,7 +477,7 @@ def choose_portals(world, player): allowed = {name: set(group[0]) for group in world.dungeon_pool[player] for name in group[0]} # key drops allow the big key in the right place in Desert Tiles 2 - bk_shuffle = world.bigkeyshuffle[player] or world.pottery[player] not in ['none', 'cave'] + bk_shuffle = world.bigkeyshuffle[player] != 'none' or world.pottery[player] not in ['none', 'cave'] std_flag = world.mode[player] == 'standard' # roast incognito doors world.get_room(0x60, player).delete(5) @@ -2689,12 +2689,12 @@ def calc_used_dungeon_items(builder, world, player): basic_flag = world.doorShuffle[player] == 'basic' base = 0 if basic_flag else 2 # at least 2 items per dungeon, except in basic base = max(count_reserved_locations(world, player, builder.location_set), base) - if not world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] == 'none': if builder.bk_required and not builder.bk_provided: base += 1 - if not world.compassshuffle[player] and (builder.name not in ['Hyrule Castle', 'Agahnims Tower'] or not basic_flag): + if world.compassshuffle[player] == 'none' and (builder.name not in ['Hyrule Castle', 'Agahnims Tower'] or not basic_flag): base += 1 - if not world.mapshuffle[player] and (builder.name != 'Agahnims Tower' or not basic_flag): + if world.mapshuffle[player] == 'none' and (builder.name != 'Agahnims Tower' or not basic_flag): base += 1 if world.prizeshuffle[player] == 'dungeon' and builder.name not in ['Hyrule Castle', 'Agahnims Tower', 'Ganons Tower']: base += 1 diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 2e539bf1..f8297aff 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -231,7 +231,7 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, all_regions, pro start = ExplorationState(dungeon=name) start.big_key_special = bk_special group_flags, door_map = find_bk_groups(name, available_sectors, proposed_map, bk_special) - bk_flag = False if world.bigkeyshuffle[player] and not bk_special else bk_needed + bk_flag = False if world.bigkeyshuffle[player] != 'none' and not bk_special else bk_needed def exception(d): return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS' @@ -436,7 +436,7 @@ def check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, a if len(dungeon.keys()) <= 1 and len(proposed_map.keys()) < len(doors_to_connect): return False # origin has no more hooks, but not all doors have been proposed - if not world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] == 'none': possible_bks = len(dungeon['Origin'].possible_bk_locations) if bk_special and check_for_special(dungeon['Origin'].visited_regions): possible_bks = 1 @@ -470,7 +470,7 @@ def check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, a if len(outstanding_doors[key]) > 0 and len(hangers[key]) == 0 and len(hooks[opp_key]) == 0: return False all_visited = set() - bk_possible = not bk_needed or (world.bigkeyshuffle[player] and not bk_special) + bk_possible = not bk_needed or (world.bigkeyshuffle[player] != 'none' and not bk_special) for piece in dungeon.values(): all_visited.update(piece.visited_regions) if ((not bk_possible and len(piece.possible_bk_locations) > 0) or @@ -544,7 +544,7 @@ def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_re start = ExplorationState(dungeon=name) start.big_key_special = bk_special - bk_flag = False if world.bigkeyshuffle[player] and not bk_special else bk_needed + bk_flag = False if world.bigkeyshuffle[player] != 'none' and not bk_special else bk_needed def exception(d): return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS' @@ -1775,11 +1775,11 @@ def requested_dungeon_items(world, player): num = 0 if world.prizeshuffle[player] == 'dungeon': num += 1 - if not world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] == 'none': num += 1 - if not world.compassshuffle[player]: + if world.compassshuffle[player] == 'none': num += 1 - if not world.mapshuffle[player]: + if world.mapshuffle[player] == 'none': num += 1 return num diff --git a/Fill.py b/Fill.py index 39825197..6654c813 100644 --- a/Fill.py +++ b/Fill.py @@ -45,12 +45,12 @@ def fill_dungeons_restrictive(world, shuffled_locations): for item in world.get_items(): if ((item.prize and world.prizeshuffle[item.player] != 'none') or (item.smallkey and world.keyshuffle[item.player] != 'none') - or (item.bigkey and world.bigkeyshuffle[item.player])): + or (item.bigkey and world.bigkeyshuffle[item.player] != 'none')): item.advancement = True - elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]): + elif (item.map and world.mapshuffle[item.player] not in ['none', 'district']) or (item.compass and world.compassshuffle[item.player] not in ['none', 'district']): item.priority = True - dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)] + dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world) or item.is_near_dungeon_item(world)] bigs, smalls, prizes, others = [], [], [], [] for i in dungeon_items: (bigs if i.bigkey else smalls if i.smallkey else prizes if i.prize else others).append(i) @@ -76,6 +76,19 @@ def fill_dungeons_restrictive(world, shuffled_locations): prizes_copy = prizes.copy() for attempt in range(15): try: + for player in range(1, world.players + 1): + if world.prizeshuffle[player] == 'district': + dungeon_pool = [] + for dungeon in world.dungeons: + from Dungeons import dungeon_table + if dungeon.player == player and dungeon_table[dungeon.name].prize: + dungeon_pool.append(dungeon) + random.shuffle(dungeon_pool) + for item in prizes: + if item.player == player: + dungeon = dungeon_pool.pop() + dungeon.prize = item + item.dungeon_object = dungeon random.shuffle(prizes) random.shuffle(shuffled_locations) prize_state_base = all_state_base.copy() @@ -86,8 +99,7 @@ def fill_dungeons_restrictive(world, shuffled_locations): logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, 14 - attempt) prizes = prizes_copy.copy() for dungeon in world.dungeons: - if world.prizeshuffle[dungeon.player] == 'dungeon': - dungeon.prize = None + dungeon.prize = None for prize in prizes: if prize.location: prize.location.item = None @@ -186,7 +198,8 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl test_state.sweep_for_events() if location.can_fill(test_state, item_to_place, perform_access_check): if valid_key_placement(item_to_place, location, key_pool, test_state, world): - if item_to_place.prize or valid_dungeon_placement(item_to_place, location, world): + if (item_to_place.prize and world.prizeshuffle[item_to_place.player] == 'none') \ + or valid_dungeon_placement(item_to_place, location, world): return location if item_to_place.smallkey or item_to_place.bigkey or item_to_place.prize: location.item = None @@ -203,6 +216,10 @@ def valid_key_placement(item, location, key_pool, collection_state, world): or world.keyshuffle[item.player] == 'universal' or world.logic[item.player] == 'nologic'): return True dungeon = location.parent_region.dungeon + if not dungeon and item.is_near_dungeon_item(world): + check_dungeon = world.get_dungeon(item.dungeon, item.player) if item.dungeon else item.dungeon_object + if len([d for d in location.parent_region.districts if d in check_dungeon.districts]): + dungeon = check_dungeon if dungeon: if dungeon.name not in item.name and (dungeon.name != 'Hyrule Castle' or 'Escape' not in item.name): return True @@ -236,13 +253,22 @@ def valid_reserved_placement(item, location, world): def valid_dungeon_placement(item, location, world): - if location.parent_region.dungeon: - layout = world.dungeon_layouts[location.player][location.parent_region.dungeon.name] + dungeon = location.parent_region.dungeon + if not dungeon and item.is_near_dungeon_item(world): + check_dungeon = world.get_dungeon(item.dungeon, item.player) if item.dungeon else item.dungeon_object + if len([d for d in location.parent_region.districts if d in check_dungeon.districts]): + dungeon = check_dungeon + if dungeon: + layout = world.dungeon_layouts[location.player][dungeon.name] if not is_dungeon_item(item, world) or item.player != location.player: + if item.prize and item.is_near_dungeon_item(world): + return item.dungeon_object == dungeon and layout.free_items > 0 return layout.free_items > 0 + elif item.prize: + return not dungeon.prize and layout.dungeon_items > 0 else: # the second half probably doesn't matter much - should always return true - return item.dungeon == location.parent_region.dungeon.name and layout.dungeon_items > 0 + return item.dungeon == dungeon.name and layout.dungeon_items > 0 return not is_dungeon_item(item, world) @@ -267,14 +293,15 @@ def track_dungeon_items(item, location, world): layout.free_items -= 1 if item.prize: location.parent_region.dungeon.prize = item + item.dungeon_object = location.parent_region.dungeon def is_dungeon_item(item, world): return ((item.prize and world.prizeshuffle[item.player] in ['none', 'dungeon']) or (item.smallkey and world.keyshuffle[item.player] == 'none') - or (item.bigkey and not world.bigkeyshuffle[item.player]) - or (item.compass and not world.compassshuffle[item.player]) - or (item.map and not world.mapshuffle[item.player])) + or (item.bigkey and world.bigkeyshuffle[item.player] == 'none') + or (item.compass and world.compassshuffle[item.player] == 'none') + or (item.map and world.mapshuffle[item.player] == 'none')) def recovery_placement(item_to_place, locations, world, state, base_state, itempool, perform_access_check, attempted, @@ -816,7 +843,8 @@ def balance_multiworld_progression(world): candidate_items = collections.defaultdict(set) while True: for location in balancing_sphere: - if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): + if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) \ + and (world.bigkeyshuffle[location.item.player] != 'none' or not location.item.bigkey): balancing_state.collect(location.item, True, location) player = location.item.player if player in balancing_players and not location.locked and location.player != player: @@ -895,7 +923,8 @@ def balance_multiworld_progression(world): sphere_locations.add(location) for location in sphere_locations: - if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): + if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) \ + and (world.bigkeyshuffle[location.item.player] != 'none' or not location.item.bigkey): state.collect(location.item, True, location) checked_locations |= sphere_locations diff --git a/ItemList.py b/ItemList.py index 6f3b7d73..5727c523 100644 --- a/ItemList.py +++ b/ItemList.py @@ -348,11 +348,11 @@ def generate_itempool(world, player): world.treasure_hunt_icon[player] = 'Triforce Piece' world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player - and ((item.prize and world.prizeshuffle[player] == 'wild') - or (item.smallkey and world.keyshuffle[player] != 'none') - or (item.bigkey and world.bigkeyshuffle[player]) - or (item.map and world.mapshuffle[player]) - or (item.compass and world.compassshuffle[player]))]) + and ((item.prize and world.prizeshuffle[player] not in ['none', 'dungeon', 'district']) + or (item.smallkey and world.keyshuffle[player] not in ['none', 'district']) + or (item.bigkey and world.bigkeyshuffle[player] not in ['none', 'district']) + or (item.map and world.mapshuffle[player] not in ['none', 'district']) + or (item.compass and world.compassshuffle[player] not in ['none', 'district']))]) if world.logic[player] == 'hybridglitches' and world.pottery[player] not in ['none', 'cave']: keys_to_remove = 2 @@ -753,6 +753,7 @@ def fill_prizes(world, attempts=15): fill_restrictive(world, all_state, prize_locs, prizepool, single_player_placement=True) for prize_loc in crystal_locations: prize_loc.parent_region.dungeon.prize = prize_loc.item + prize_loc.item.dungeon_object = prize_loc.parent_region.dungeon except FillError as e: logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts - attempt - 1) for location in empty_crystal_locations: @@ -1434,9 +1435,9 @@ def make_customizer_pool(world, player): or item_name.startswith('Crystal') or item_name.endswith('Pendant')): d_item = ItemFactory(item_name, player) if ((d_item.prize and world.prizeshuffle[player] in ['none', 'dungeon']) - or (d_item.bigkey and not world.bigkeyshuffle[player]) - or (d_item.compass and not world.compassshuffle[player]) - or (d_item.map and not world.mapshuffle[player])): + or (d_item.bigkey and world.bigkeyshuffle[player] == 'none') + or (d_item.compass and world.compassshuffle[player] == 'none') + or (d_item.map and world.mapshuffle[player] == 'none')): d_name = d_item.dungeon dungeon = world.get_dungeon(d_name, player) current_amount = 1 if dungeon.big_key and (d_item == dungeon.big_key or d_item in dungeon.dungeon_items) else 0 @@ -1694,7 +1695,7 @@ def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_ def is_dungeon_item(item, world, player): return (((item.startswith('Crystal') or item.endswith('Pendant')) and world.prizeshuffle[player] in ['none', 'dungeon']) or (item.startswith('Small Key') and world.keyshuffle[player] == 'none') - or (item.startswith('Big Key') and not world.bigkeyshuffle[player]) - or (item.startswith('Compass') and not world.compassshuffle[player]) - or (item.startswith('Map') and not world.mapshuffle[player])) + or (item.startswith('Big Key') and world.bigkeyshuffle[player] == 'none') + or (item.startswith('Compass') and world.compassshuffle[player] == 'none') + or (item.startswith('Map') and world.mapshuffle[player] == 'none')) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index a3c008b1..d5cebf00 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1303,7 +1303,7 @@ def check_rules_deep(original_counter, key_layout, world, player): big_avail = counter.big_key_opened or bk_drop big_maybe_not_found = not counter.big_key_opened and not bk_drop # better named as big_missing? if not key_layout.big_key_special and not big_avail: - if world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] != 'none': big_avail = True else: for location in counter.free_locations: @@ -1430,7 +1430,7 @@ def prize_relevance_sig2(start_regions, d_name, dungeon_entrance, is_atgt_swappe def validate_bk_layout(proposal, builder, start_regions, world, player): bk_special = check_bk_special(builder.master_sector.regions, world, player) - if world.bigkeyshuffle[player] and (world.dropshuffle[player] != 'none' or not bk_special): + if world.bigkeyshuffle[player] != 'none' and (world.dropshuffle[player] != 'none' or not bk_special): return True flat_proposal = flatten_pair_list(proposal) state = ExplorationState(dungeon=builder.name) @@ -1438,7 +1438,7 @@ def validate_bk_layout(proposal, builder, start_regions, world, player): for region in start_regions: dungeon_entrance, portal_door = find_outside_connection(region) prize_relevant_flag = prize_relevance_sig2(start_regions, builder.name, dungeon_entrance, world.is_atgt_swapped(player)) - if prize_relevant_flag and world.prizeshuffle[player] == 'none': + if prize_relevant_flag and world.prizeshuffle[player] in ['none', 'dungeon']: state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance # key_layout.prize_relevant = prize_relevant_flag @@ -1469,7 +1469,7 @@ def validate_key_layout(key_layout, world, player): for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance, world.is_atgt_swapped(player)) - if prize_relevant_flag and world.prizeshuffle[player] == 'none': + if prize_relevant_flag and world.prizeshuffle[player] in ['none', 'dungeon']: state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = prize_relevant_flag @@ -1606,7 +1606,7 @@ def determine_prize_lock(key_layout, world, player): for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance, world.is_atgt_swapped(player)) - if prize_relevant_flag and world.prizeshuffle[player] == 'none': + if prize_relevant_flag and world.prizeshuffle[player] in ['none', 'dungeon']: state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = prize_relevant_flag @@ -1650,13 +1650,13 @@ def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, pla def cnt_avail_big_locations(ttl_locations, state, world, player): - if not world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] == 'none': return max(0, ttl_locations - state.used_locations) if not state.big_key_special else 0 return 1 if not state.big_key_special else 0 def cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player): - if not world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] == 'none': bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0 used_locations = max(0, counter.used_keys - len(counter.key_only_locations)) + bk_adj return max(0, ttl_locations - used_locations) if not layout.big_key_special else 0 @@ -1683,7 +1683,7 @@ def create_key_counters(key_layout, world, player): for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance, world.is_atgt_swapped(player)) - if prize_relevant_flag and world.prizeshuffle[player] == 'none': + if prize_relevant_flag and world.prizeshuffle[player] in ['none', 'dungeon']: state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = prize_relevant_flag @@ -2096,7 +2096,7 @@ def validate_key_placement(key_layout, world, player): bigkey_name = dungeon_bigs[key_layout.sector.name] if world.keyshuffle[player] != 'none': keys_outside = key_layout.max_chests - sum(1 for i in max_counter.free_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) - if world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] != 'none': max_counter = find_max_counter(key_layout) big_key_outside = bigkey_name not in (l.item.name for l in max_counter.free_locations if l.item) for i in world.precollected_items: diff --git a/README.md b/README.md index 0da12e9e..a332ce01 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,10 @@ As far as map trackers, Bonk Locations are supported on `CodeTracker` when the B - 1 8x Bomb Pack - 1 Good Bee +## Nearby Dungeon Items + +This is a new option in addition to the traditional wild vs non-wild (keysanity/non-keysanity) options for all the dungeon item types (maps, compasses, small keys, big keys, prizes). This new option shuffles dungeon items into locations somewhere either within the dungeon that it is assigned to or within the surrounding district of that dungeon. + ## Prize Shuffle A new option has been added to shuffle the 10 dungeon prizes in ways that they haven't been shuffled before. This means that dungeon prizes can be found in other item locations, such as chests or free-standing item locations. This also means that bosses are able to drop a 2nd item in place of the shuffled prize. @@ -280,6 +284,10 @@ This is the normal prize behavior that has been a part of rando up until now. Th This option shuffles the prize into a location somewhere within the dungeon that it is assigned to. +### Nearby + +This option shuffles the prize into a location somewhere either within the dungeon that it is assigned to or within the surrounding district of that dungeon. + ### Randomized This option freely shuffles the prizes throughout the world. While the dungeon prizes can end up anywhere, they still are assigned to a specific dungeon. When you defeat the boss of a certain dungeon, checking the map on the overworld will reveal the location WHERE you can find the prize, an example shown [here](https://zelda.codemann8.com/images/shared/prizemap-all.gif). Finding the map will still reveal WHAT the prize is. If you defeated a boss but haven't collected the map for that dungeon, the prize will be indicated by a red X, example shown [here](https://zelda.codemann8.com/images/shared/prizemap-boss.gif). If you collected a map but haven't defeated the boss yet, the icon indicator on the map will be shown on the top edge (for LW dungeons) or the bottom edge (for DW dungeons), but it will show you WHAT the prize is for that dungeon, an example of that is shown [here](https://zelda.codemann8.com/images/shared/prizemap-map.gif). diff --git a/Rom.py b/Rom.py index 7a1cd7ca..6e0cd76d 100644 --- a/Rom.py +++ b/Rom.py @@ -480,14 +480,14 @@ def patch_rom(world, rom, player, team, is_mystery=False): rom.write_byte(address, value) # patch music - if world.mapshuffle[player]: + if world.mapshuffle[player] != 'none': music = random.choice([0x11, 0x16]) else: music = 0x11 if 'Pendant' in dungeon.prize.name else 0x16 for music_address in dungeon_music_addresses[dungeon.name]: rom.write_byte(music_address, music) - if world.mapshuffle[player]: + if world.mapshuffle[player] != 'none': rom.write_byte(0x155C9, random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle if world.doorShuffle[player] != 'vanilla': @@ -1131,7 +1131,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): ERtimeincrease = 10 else: ERtimeincrease = 20 - if world.keyshuffle[player] != 'none' or world.bigkeyshuffle[player] or world.mapshuffle[player]: + if world.keyshuffle[player] != 'none' or world.bigkeyshuffle[player] != 'none' or world.mapshuffle[player] != 'none': ERtimeincrease = ERtimeincrease + 15 if world.clock_mode == 'none': rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode @@ -1268,34 +1268,36 @@ def patch_rom(world, rom, player, team, is_mystery=False): # Bitfield - enable text box to show with free roaming items # - # --po bmcs + # -tpo bmcs + # t - suppress "this dungeon" textboxes (temporary fix) # p - enabled for non-prize crystals - # o - enabled for outside dungeon items + # o - enabled for outside dungeon items (unused currently?) # b - enabled for inside big keys # m - enabled for inside maps # c - enabled for inside compasses # s - enabled for inside small keys - rom.write_byte(0x18016A, 0x10 | ((0x20 if world.prizeshuffle[player] == 'wild' else 0x00) - | (0x01 if world.keyshuffle[player] == 'wild' else 0x00) - | (0x02 if world.compassshuffle[player] else 0x00) - | (0x04 if world.mapshuffle[player] else 0x00) - | (0x08 if world.bigkeyshuffle[player] else 0x00))) # free roaming item text boxes - rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] else 0x00) # maps showing crystals on overworld + free_item_text = 0x40 if 'district' in [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]] else 0x00 + rom.write_byte(0x18016A, free_item_text | 0x10 | ((0x20 if world.prizeshuffle[player] not in ['none', 'dungeon'] else 0x00) + | (0x01 if world.keyshuffle[player] not in ['none', 'universal'] else 0x00) + | (0x02 if world.compassshuffle[player] != 'none' else 0x00) + | (0x04 if world.mapshuffle[player] != 'none' else 0x00) + | (0x08 if world.bigkeyshuffle[player] != 'none' else 0x00))) # free roaming item text boxes + rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] not in ['none', 'district'] else 0x00) # maps showing crystals on overworld # compasses showing dungeon count - compass_mode = 0x80 if world.compassshuffle[player] else 0x00 + compass_mode = 0x80 if world.compassshuffle[player] not in ['none', 'district'] else 0x00 if world.clock_mode != 'none' or world.dungeon_counters[player] == 'off': pass elif world.dungeon_counters[player] == 'on': compass_mode |= 0x02 # always on - elif (world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player] != 'none' + elif (world.compassshuffle[player] not in ['none', 'district'] or world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player] != 'none' or world.dungeon_counters[player] == 'pickup' or world.pottery[player] not in ['none', 'cave']): compass_mode |= 0x01 # show on pickup if world.overworld_map[player] == 'map': compass_mode |= 0x10 # show icon if map is collected elif world.overworld_map[player] == 'compass': compass_mode |= 0x20 # show icon if compass is collected - if world.prizeshuffle[player] == 'wild': + if world.prizeshuffle[player] not in ['none', 'dungeon', 'district']: compass_mode |= 0x40 # show icon if boss is defeated, hide if collected rom.write_byte(0x18003C, compass_mode) @@ -1331,7 +1333,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): map_index = max(0, dungeon_index - 2) # write out dislocated coords - if map_index >= 0x02 and map_index < 0x18 and (world.overworld_map[player] != 'default' or world.prizeshuffle[player] == 'wild'): + if map_index >= 0x02 and map_index < 0x18 and (world.overworld_map[player] != 'default' or world.prizeshuffle[player] not in ['none', 'dungeon', 'district']): owid_map = [0x1E, 0x30, 0xFF, 0x7B, 0x5E, 0x70, 0x40, 0x75, 0x03, 0x58, 0x47] x_map_position_generic = [0x03c0, 0x0740, 0xff00, 0x03c0, 0x01c0, 0x0bc0, 0x05c0, 0x09c0, 0x0ac0, 0x07c0, 0x0dc0] y_map_position_generic = [0xff00, 0xff00, 0xff00, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0xff00, 0x0fc0, 0x0fc0] @@ -1345,7 +1347,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): write_int16(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+6, y_map_position_generic[idx]) # write out icon coord data - if world.prizeshuffle[player] == 'wild' and dungeon_table[dungeon].prize: + if world.prizeshuffle[player] not in ['none', 'dungeon', 'district'] and dungeon_table[dungeon].prize: dungeon_obj = world.get_dungeon(dungeon, player) entrance = dungeon_obj.prize.get_map_location() coords = get_entrance_coords(entrance) @@ -1385,7 +1387,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): # figure out compass entrances and what world (light/dark) write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6), coords) - if world.prizeshuffle[player] != 'wild' and dungeon_table[dungeon].prize: + if world.prizeshuffle[player] in ['none', 'dungeon', 'district'] and dungeon_table[dungeon].prize: # prize location write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+8, coords) @@ -1424,11 +1426,11 @@ def patch_rom(world, rom, player, team, is_mystery=False): # b - Big Key # a - Small Key # - enable_menu_map_check = (world.overworld_map[player] != 'default' and world.shuffle[player] != 'vanilla') or world.prizeshuffle[player] == 'wild' - rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] == 'wild' else 0x00) - | (0x02 if world.bigkeyshuffle[player] else 0x00) - | (0x04 if world.mapshuffle[player] or enable_menu_map_check else 0x00) - | (0x08 if world.compassshuffle[player] else 0x00) # free roaming items in menu + enable_menu_map_check = (world.overworld_map[player] != 'default' and world.shuffle[player] != 'vanilla') or world.prizeshuffle[player] not in ['none', 'dungeon', 'district'] + rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] not in ['none', 'universal'] else 0x00) + | (0x02 if world.bigkeyshuffle[player] != 'none' else 0x00) + | (0x04 if world.mapshuffle[player] != 'none' or enable_menu_map_check else 0x00) + | (0x08 if world.compassshuffle[player] != 'none' else 0x00) # free roaming items in menu | (0x10 if world.logic[player] == 'nologic' else 0))) # boss icon def get_reveal_bytes(itemName): @@ -2203,11 +2205,11 @@ def write_strings(rom, world, player, team): this_hint = this_hint[0].upper() + this_hint[1:] tt[hint_locations.pop(0)] = this_hint items_to_hint.remove(flute_item) - if world.keyshuffle[player] == 'wild': + if world.keyshuffle[player] not in ['none', 'universal']: items_to_hint.extend(SmallKeys) - if world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] != 'none': items_to_hint.extend(BigKeys) - if world.prizeshuffle[player] == 'wild': + if world.prizeshuffle[player] not in ['none', 'dungeon']: items_to_hint.extend(Prizes) random.shuffle(items_to_hint) hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 8 @@ -2323,10 +2325,14 @@ def write_strings(rom, world, player, team): crystal5 = world.find_items('Crystal 5', player)[0] crystal6 = world.find_items('Crystal 6', player)[0] greenpendant = world.find_items('Green Pendant', player)[0] - if world.prizeshuffle[player] == 'none': + if world.prizeshuffle[player] in ['none', 'dungeon']: (crystal5, crystal6, greenpendant) = tuple([x.parent_region.dungeon.name for x in [crystal5, crystal6, greenpendant]]) tt['bomb_shop'] = 'Big Bomb?\nMy supply is blocked until you clear %s and %s.' % (crystal5, crystal6) tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant + elif world.prizeshuffle[player] == 'district': + (crystal5, crystal6, greenpendant) = tuple([x.item.dungeon_object.name for x in [crystal5, crystal6, greenpendant]]) + tt['bomb_shop'] = 'Big Bomb?\nThe crystals can be found near %s and %s.' % (crystal5, crystal6) + tt['sahasrahla_bring_courage'] = 'I lost my family heirloom near %s' % greenpendant else: tt['bomb_shop'] = 'Big Bomb?\nThe crystals can be found %s and %s.' % (crystal5.hint_text, crystal6.hint_text) tt['sahasrahla_bring_courage'] = 'My family heirloom can be found %s' % greenpendant.hint_text diff --git a/Rules.py b/Rules.py index b75a80c3..4020ce69 100644 --- a/Rules.py +++ b/Rules.py @@ -2029,7 +2029,7 @@ def add_hmg_key_logic_rules(world, player): def add_key_logic_rules(world, player): key_logic = world.key_logic[player] eval_func = eval_small_key_door - if world.key_logic_algorithm[player] == 'strict' and world.keyshuffle[player] == 'wild': + if world.key_logic_algorithm[player] == 'strict' and world.keyshuffle[player] not in ['none', 'universal']: eval_func = eval_small_key_door_strict elif world.key_logic_algorithm[player] != 'dangerous': eval_func = eval_small_key_door_partial diff --git a/mystery_example.yml b/mystery_example.yml index 79f9815d..8fde1fa0 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -151,6 +151,7 @@ prize_shuffle: none: 1 dungeon: 1 + district: 1 wild: 1 dungeon_items: standard: 10 @@ -160,18 +161,22 @@ mcu: 1 # map, compass, universal smalls # for use when you aren't using the dungeon_items above # map_shuffle: - # on: 1 - # off: 1 + # none: 1 + # district: 1 + # wild: 1 # compass_shuffle: - # on: 1 - # off: 1 + # none: 1 + # district: 1 + # wild: 1 # smallkey_shuffle: # none: 5 + # district: 1 # wild: 1 # universal: 1 # bigkey_shuffle: - # on: 1 - # off: 1 + # none: 1 + # district: 1 + # wild: 1 dungeon_counters: on: 5 off: 0 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 7d2de9da..91c84430 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -369,12 +369,18 @@ "type": "bool" }, "mapshuffle": { - "action": "store_true", - "type": "bool" + "choices": [ + "none", + "district", + "wild" + ] }, "compassshuffle": { - "action": "store_true", - "type": "bool" + "choices": [ + "none", + "district", + "wild" + ] }, "keyshuffle": { "choices": [ @@ -384,13 +390,17 @@ ] }, "bigkeyshuffle": { - "action": "store_true", - "type": "bool" + "choices": [ + "none", + "district", + "wild" + ] }, "prizeshuffle": { "choices": [ "none", "dungeon", + "district", "wild" ] }, diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 68fba490..d40944ae 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -53,15 +53,26 @@ "randomizer.dungeon.keysanity": "Shuffle: ", "randomizer.dungeon.mapshuffle": "Maps", + "randomizer.dungeon.mapshuffle.none": "In Dungeon", + "randomizer.dungeon.mapshuffle.district": "Nearby", + "randomizer.dungeon.mapshuffle.wild": "Randomized", "randomizer.dungeon.compassshuffle": "Compasses", + "randomizer.dungeon.compassshuffle.none": "In Dungeon", + "randomizer.dungeon.compassshuffle.district": "Nearby", + "randomizer.dungeon.compassshuffle.wild": "Randomized", "randomizer.dungeon.smallkeyshuffle": "Small Keys", "randomizer.dungeon.smallkeyshuffle.none": "In Dungeon", + "randomizer.dungeon.smallkeyshuffle.district": "Nearby", "randomizer.dungeon.smallkeyshuffle.wild": "Randomized", "randomizer.dungeon.smallkeyshuffle.universal": "Universal", "randomizer.dungeon.bigkeyshuffle": "Big Keys", + "randomizer.dungeon.bigkeyshuffle.none": "In Dungeon", + "randomizer.dungeon.bigkeyshuffle.district": "Nearby", + "randomizer.dungeon.bigkeyshuffle.wild": "Randomized", "randomizer.dungeon.prizeshuffle": "Prizes", "randomizer.dungeon.prizeshuffle.none": "On Boss", "randomizer.dungeon.prizeshuffle.dungeon": "In Dungeon", + "randomizer.dungeon.prizeshuffle.district": "Nearby", "randomizer.dungeon.prizeshuffle.wild": "Randomized", "randomizer.dungeon.decoupledoors": "Decouple Doors", "randomizer.dungeon.door_self_loops": "Allow Self-Looping Spiral Stairs", diff --git a/resources/app/gui/randomize/dungeon/keysanity.json b/resources/app/gui/randomize/dungeon/keysanity.json index 5a2a8e60..9cf39dd0 100644 --- a/resources/app/gui/randomize/dungeon/keysanity.json +++ b/resources/app/gui/randomize/dungeon/keysanity.json @@ -1,9 +1,32 @@ { "keysanity": { + "mapshuffle": { + "type": "selectbox", + "options": [ + "none", + "district", + "wild" + ], + "config": { + "padx": [20,0] + } + }, + "compassshuffle": { + "type": "selectbox", + "options": [ + "none", + "district", + "wild" + ], + "config": { + "padx": [20,0] + } + }, "smallkeyshuffle": { "type": "selectbox", "options": [ "none", + "district", "wild", "universal" ], @@ -11,8 +34,16 @@ "padx": [20,0] } }, - "mapshuffle": { "type": "checkbox" }, - "compassshuffle": { "type": "checkbox" }, - "bigkeyshuffle": { "type": "checkbox" } + "bigkeyshuffle": { + "type": "selectbox", + "options": [ + "none", + "district", + "wild" + ], + "config": { + "padx": [20,0] + } + } } } diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index 89addb90..ec22f036 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -6,6 +6,7 @@ "options": [ "none", "dungeon", + "district", "wild" ], "config": { diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index fb5674e3..17d66946 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -147,11 +147,14 @@ class CustomSettings(object): args.compassshuffle[p] = get_setting(settings['compassshuffle'], args.compassshuffle[p]) if get_setting(settings['keysanity'], args.keysanity): - args.bigkeyshuffle[p] = True + if args.bigkeyshuffle[p] == 'none': + args.bigkeyshuffle[p] = 'wild' if args.keyshuffle[p] == 'none': args.keyshuffle[p] = 'wild' - args.mapshuffle[p] = True - args.compassshuffle[p] = True + if args.mapshuffle[p] == 'none': + args.mapshuffle[p] = 'wild' + if args.compassshuffle[p] == 'none': + args.compassshuffle[p] = 'wild' args.shufflebosses[p] = get_setting(settings['boss_shuffle'], get_setting(settings['shufflebosses'], args.shufflebosses[p])) args.shuffleenemies[p] = get_setting(settings['enemy_shuffle'], get_setting(settings['shuffleenemies'], args.shuffleenemies[p])) diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index 7f0efec5..b521bfcd 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -233,7 +233,7 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se def explore_proposal(name, entrance_regions, all_regions, proposed_map, valid_doors, bk_special, world, player): start = ExplorationState(dungeon=name) - bk_relevant = (world.door_type_mode[player] == 'original' and not world.bigkeyshuffle[player]) or bk_special + bk_relevant = (world.door_type_mode[player] == 'original' and world.bigkeyshuffle[player] == 'none') or bk_special start.big_key_special = bk_special original_state = extend_reachable_state_lenient(entrance_regions, start, proposed_map, all_regions, valid_doors, bk_relevant, world, player) @@ -302,7 +302,7 @@ def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_re target_regions.add(region) start = ExplorationState(dungeon=name) - bk_relevant = (world.door_type_mode[player] == 'original' and not world.bigkeyshuffle[player]) or bk_special + bk_relevant = (world.door_type_mode[player] == 'original' and world.bigkeyshuffle[player] == 'none') or bk_special start.big_key_special = bk_special original_state = extend_reachable_state_lenient(starting_regions, start, proposed_map, all_regions, valid_doors, bk_relevant, world, player) diff --git a/source/gui/randomize/dungeon.py b/source/gui/randomize/dungeon.py index 7400dbe4..88f11379 100644 --- a/source/gui/randomize/dungeon.py +++ b/source/gui/randomize/dungeon.py @@ -14,6 +14,8 @@ def dungeon_page(parent): self.frames = {} self.frames["keysanity"] = Frame(self) self.frames["keysanity"].pack(anchor=W) + self.frames["keysanity2"] = Frame(self) + self.frames["keysanity2"].pack(anchor=W) ## Dungeon Item Shuffle mscbLabel = Label(self.frames["keysanity"], text="Dungeon Items: ") @@ -23,9 +25,15 @@ def dungeon_page(parent): # Defns include frame name, widget type, widget options, widget placement attributes # This first set goes in the Keysanity frame with open(os.path.join("resources","app","gui","randomize","dungeon","keysanity.json")) as keysanityItems: - myDict = json.load(keysanityItems) - myDict = myDict["keysanity"] - dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["keysanity"]) + myDictFile = json.load(keysanityItems) + myDict = myDictFile["keysanity"] + myDict1, myDict2 = dict(), dict() + count = 2 + for key in myDict.keys(): + (myDict1 if count > 0 else myDict2)[key] = myDict[key] + count -= 1 + dictWidgets = {**widgets.make_widgets_from_dict(self, myDict1, self.frames["keysanity"]), \ + **widgets.make_widgets_from_dict(self, myDict2, self.frames["keysanity2"])} for key in dictWidgets: self.widgets[key] = dictWidgets[key] packAttrs = {"side":LEFT} diff --git a/source/item/District.py b/source/item/District.py index 1ad4fba6..bec993bd 100644 --- a/source/item/District.py +++ b/source/item/District.py @@ -96,12 +96,15 @@ def resolve_districts(world): for name, district in world.districts[player].items(): if district.dungeon: + dungeon = world.get_dungeon(district.dungeon, player) + dungeon.districts = [district] + dungeon.districts layout = world.dungeon_layouts[player][district.dungeon] district.locations.update([l.name for r in layout.master_sector.regions for l in r.locations if not l.item and l.real]) else: for region_name in district.regions: region = world.get_region(region_name, player) + region.districts.append(district) for location in region.locations: if not location.item and location.real: district.locations.add(location.name) @@ -115,6 +118,7 @@ def resolve_districts(world): RuntimeError(f'No region connected to entrance: {ent.name} Likely a missing entry in OWExitTypes') visited.add(region) if region.type == RegionType.Cave: + region.districts.append(district) for location in region.locations: if not location.item and location.real: district.locations.add(location.name) @@ -123,6 +127,8 @@ def resolve_districts(world): queue.appendleft(ext.connected_region) elif region.type == RegionType.Dungeon and region.dungeon: district.dungeons.add(region.dungeon.name) + if district not in region.dungeon.districts: + region.dungeon.districts.append(district) elif region.name in inaccessible: district.access_points.add(region) diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 8c800f95..c21fa9ca 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -134,7 +134,7 @@ def create_item_pool_config(world): groups = LocationGroup('Major').locs(init_set) if world.prizeshuffle[player] != 'none': groups.locations.extend(mode_grouping['Prizes']) - if world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] != 'none': groups.locations.extend(mode_grouping['Big Keys']) if world.dropshuffle[player] != 'none': groups.locations.extend(mode_grouping['Big Key Drops']) @@ -144,9 +144,9 @@ def create_item_pool_config(world): groups.locations.extend(mode_grouping['Key Drops']) if world.pottery[player] not in ['none', 'cave']: groups.locations.extend(mode_grouping['Pot Keys']) - if world.compassshuffle[player]: + if world.compassshuffle[player] != 'none': groups.locations.extend(mode_grouping['Compasses']) - if world.mapshuffle[player]: + if world.mapshuffle[player] != 'none': groups.locations.extend(mode_grouping['Maps']) if world.shopsanity[player]: groups.locations.append('Capacity Upgrade - Left') @@ -259,12 +259,12 @@ def location_prefilled(location, world, player): def previously_reserved(location, world, player): if '- Boss' in location.name or '- Prize' in location.name: - if world.restrict_boss_items[player] == 'mapcompass' and (not world.compassshuffle[player] - or not world.mapshuffle[player]): + if world.restrict_boss_items[player] == 'mapcompass' and (world.compassshuffle[player] == 'none' + or world.mapshuffle[player] == 'none'): return True - if world.restrict_boss_items[player] == 'dungeon' and (not world.compassshuffle[player] - or not world.mapshuffle[player] - or not world.bigkeyshuffle[player] + if world.restrict_boss_items[player] == 'dungeon' and (world.compassshuffle[player] == 'none' + or world.mapshuffle[player] == 'none' + or world.bigkeyshuffle[player] == 'none' or world.keyshuffle[player] == 'none' or world.prizeshuffle[player] in ['none', 'dungeon']): return True @@ -303,6 +303,7 @@ def massage_item_pool(world): if item.prize: dungeon = dungeon_pool[item.player].pop() dungeon.prize = item + item.dungeon_object = dungeon player_pool[item.player].append(item) for dungeon in world.dungeons: for item in dungeon.all_items: @@ -381,13 +382,13 @@ def determine_major_items(world, player): pass # now what? if world.prizeshuffle[player] not in ['none', 'dungeon']: major_item_set.update({x for x, y in item_table.items() if y[2] == 'Prize'}) - if world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] != 'none': major_item_set.update({x for x, y in item_table.items() if y[2] == 'BigKey'}) if world.keyshuffle[player] != 'none': major_item_set.update({x for x, y in item_table.items() if y[2] == 'SmallKey'}) - if world.compassshuffle[player]: + if world.compassshuffle[player] != 'none': major_item_set.update({x for x, y in item_table.items() if y[2] == 'Compass'}) - if world.mapshuffle[player]: + if world.mapshuffle[player] != 'none': major_item_set.update({x for x, y in item_table.items() if y[2] == 'Map'}) if world.shopsanity[player]: major_item_set.add('Bomb Upgrade (+5)') diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index e7da45df..6c347db3 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -96,8 +96,14 @@ def roll_settings(weights): dungeon_items = get_choice('dungeon_items') dungeon_items = '' if dungeon_items == 'standard' or dungeon_items is None else dungeon_items dungeon_items = 'mcsb' if dungeon_items == 'full' else dungeon_items - ret.mapshuffle = get_choice_bool('map_shuffle') if 'map_shuffle' in weights else 'm' in dungeon_items - ret.compassshuffle = get_choice_bool('compass_shuffle') if 'compass_shuffle' in weights else 'c' in dungeon_items + if 'map_shuffle' in weights: + ret.mapshuffle = get_choice('map_shuffle') + elif 'm' in dungeon_items: + ret.mapshuffle = 'wild' + if 'compass_shuffle' in weights: + ret.compassshuffle = get_choice('compass_shuffle') + elif 'c' in dungeon_items: + ret.compassshuffle = 'wild' if 'smallkey_shuffle' in weights: ret.keyshuffle = get_choice('smallkey_shuffle') else: @@ -105,7 +111,10 @@ def roll_settings(weights): ret.keyshuffle = 'wild' if 'u' in dungeon_items: ret.keyshuffle = 'universal' - ret.bigkeyshuffle = get_choice_bool('bigkey_shuffle') if 'bigkey_shuffle' in weights else 'b' in dungeon_items + if 'bigkey_shuffle' in weights: + ret.bigkeyshuffle = get_choice('bigkey_shuffle') + elif 'b' in dungeon_items: + ret.bigkeyshuffle = 'wild' ret.prizeshuffle = get_choice('prize_shuffle') ret.accessibility = get_choice('accessibility') @@ -140,7 +149,7 @@ def roll_settings(weights): ret.dungeon_counters = get_choice_non_bool('dungeon_counters') if 'dungeon_counters' in weights else 'default' if ret.dungeon_counters == 'default': - ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle == 'on' else 'off' + ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle != 'none' else 'off' ret.pseudoboots = get_choice_bool('pseudoboots') ret.shopsanity = get_choice_bool('shopsanity')