From d9f0e2a7b6e9226f2c2517fc0d51210eed2c6bc0 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 6 Jul 2022 10:06:29 -0600 Subject: [PATCH] Options added for door_type_mode and new partitioned mode --- BaseClasses.py | 17 +- CLI.py | 3 +- DoorShuffle.py | 770 +++++++++++++----- DungeonGenerator.py | 26 +- KeyDoorShuffle.py | 50 +- Main.py | 1 + Rom.py | 31 +- RoomData.py | 4 +- data/base2current.bps | Bin 93061 -> 93211 bytes mystery_example.yml | 5 + resources/app/cli/args.json | 11 +- resources/app/cli/lang/en.json | 8 + resources/app/gui/lang/en.json | 7 + .../app/gui/randomize/dungeon/widgets.json | 14 + source/classes/CustomSettings.py | 2 + source/classes/constants.py | 1 + source/dungeon/DungeonStitcher.py | 29 +- source/tools/MysteryUtils.py | 1 + 18 files changed, 722 insertions(+), 258 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 8c7957d8..39286e37 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -28,6 +28,7 @@ class World(object): self.shuffle = shuffle.copy() self.doorShuffle = doorShuffle.copy() self.intensity = {} + self.door_type_mode = {} self.logic = logic.copy() self.mode = mode.copy() self.swords = swords.copy() @@ -143,6 +144,7 @@ class World(object): set_player_attr('colorizepots', False) set_player_attr('pot_pool', {}) set_player_attr('decoupledoors', False) + set_player_attr('door_type_mode', 'original') set_player_attr('shopsanity', False) set_player_attr('mixed_travel', 'prevent') @@ -1872,7 +1874,6 @@ class Sector(object): self.item_logic = set() self.chest_location_set = set() - def region_set(self): if self.r_name_set is None: self.r_name_set = dict.fromkeys(map(lambda r: r.name, self.regions)) @@ -2154,7 +2155,7 @@ class Location(object): def gen_name(self): name = self.name world = self.parent_region.world if self.parent_region and self.parent_region.world else None - if self.parent_region.dungeon and world and world.doorShuffle[self.player] == 'crossed': + if self.parent_region.dungeon and world and world.doorShuffle[self.player] not in ['basic', 'vanilla']: name += f' @ {self.parent_region.dungeon.name}' if world and world.players > 1: name += f' ({world.get_player_names(self.player)})' @@ -2377,6 +2378,8 @@ class Spoiler(object): 'overworld_map': self.world.overworld_map, 'door_shuffle': self.world.doorShuffle, 'intensity': self.world.intensity, + 'door_type_mode': self.world.door_type_mode, + 'decoupledoors': self.world.decoupledoors, 'dungeon_counters': self.world.dungeon_counters, 'item_pool': self.world.difficulty, 'item_functionality': self.world.difficulty_adjustments, @@ -2578,6 +2581,7 @@ class Spoiler(object): outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) if self.metadata['door_shuffle'][player] != 'vanilla': outfile.write(f"Intensity: {self.metadata['intensity'][player]}\n") + outfile.write(f"Door Type Mode: {self.metadata['door_type_mode'][player]}\n") outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n") outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n") @@ -2815,7 +2819,7 @@ class Pot(object): # byte 0: DDDE EEEE (DR, ER) -dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0} +dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3} er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8, 'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6} @@ -2845,7 +2849,8 @@ counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3} # byte 6: CCCC CPAA (crystals ganon, pyramid, access access_mode = {"items": 0, "locations": 1, "none": 2} -# byte 7: BSMC ??EE (big, small, maps, compass, bosses, enemies) +# byte 7: BSMC DDEE (big, small, maps, compass, door_type, enemies) +door_type_mode = {'original': 0, 'big': 1, 'all': 2, 'chaos': 3} enemy_mode = {"none": 0, "shuffled": 1, "chaos": 2, "random": 2, "legacy": 3} # byte 8: HHHD DPBS (enemy_health, enemy_dmg, potshuffle, bomb logic, shuffle links) @@ -2898,7 +2903,7 @@ class Settings(object): (0x80 if w.bigkeyshuffle[p] else 0) | (0x40 if w.keyshuffle[p] else 0) | (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0) - | (enemy_mode[w.enemy_shuffle[p]]), + | (door_type_mode[w.door_type_mode[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]), (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0) | (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0), @@ -2955,7 +2960,7 @@ class Settings(object): args.keyshuffle[p] = True if settings[7] & 0x40 else False args.mapshuffle[p] = True if settings[7] & 0x20 else False args.compassshuffle[p] = True if settings[7] & 0x10 else False - # args.shufflebosses[p] = r(boss_mode)[(settings[7] & 0xc) >> 2] + args.door_type_mode[p] = r(door_type_mode)[(settings[7] & 0xc) >> 2] args.shuffleenemies[p] = r(enemy_mode)[settings[7] & 0x3] args.enemy_health[p] = r(e_health)[(settings[8] & 0xE0) >> 5] diff --git a/CLI.py b/CLI.py index 77b020b4..2e01e4bc 100644 --- a/CLI.py +++ b/CLI.py @@ -125,7 +125,7 @@ def parse_cli(argv, no_defaults=False): 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', - 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors']: + 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -192,6 +192,7 @@ def parse_settings(): "keysanity": False, "door_shuffle": "basic", "intensity": 2, + 'door_type_mode': 'original', 'decoupledoors': False, "experimental": False, "dungeon_counters": "default", diff --git a/DoorShuffle.py b/DoorShuffle.py index b8c8abbe..42d32c24 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -4,6 +4,7 @@ import logging import time from enum import unique, Flag from typing import DefaultDict, Dict, List +from itertools import chain from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys from BaseClasses import PotFlags, LocationType, Direction @@ -18,8 +19,9 @@ from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2 from DungeonGenerator import ExplorationState, convert_regions, pre_validate, determine_required_paths, drop_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances from DungeonGenerator import dungeon_portals, dungeon_drops, connect_doors -from DungeonGenerator import valid_region_to_explore as valid_region_to_explore_lim +from DungeonGenerator import valid_region_to_explore from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock +from KeyDoorShuffle import validate_bk_layout, check_bk_special from Utils import ncr, kth_combination @@ -87,7 +89,7 @@ def link_doors_prep(world, player): find_inaccessible_regions(world, player) - if world.intensity[player] >= 3 and world.doorShuffle[player] in ['basic', 'crossed']: + if world.intensity[player] >= 3 and world.doorShuffle[player] != 'vanilla': choose_portals(world, player) else: if world.shuffle[player] == 'vanilla': @@ -128,14 +130,21 @@ def link_doors_prep(world, player): def link_doors_main(world, player): + pool = None if world.doorShuffle[player] == 'basic': - within_dungeon(world, player) + pool = [([name], regions) for name, regions in dungeon_regions.items()] + elif world.doorShuffle[player] == 'partitioned': + groups = [['Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower'], + ['Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town'], + ['Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower']] + pool = [(group, list(chain.from_iterable([dungeon_regions[d] for d in group]))) for group in groups] elif world.doorShuffle[player] == 'crossed': - cross_dungeon(world, player) + pool = [list(dungeon_regions.keys()), sum(r for r in dungeon_regions.values())] elif world.doorShuffle[player] != 'vanilla': logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player]) raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player]) - + if pool: + main_dungeon_pool(pool, world, player) if world.doorShuffle[player] != 'vanilla': create_door_spoiler(world, player) @@ -395,8 +404,20 @@ def pair_existing_key_doors(world, player, door_a, door_b): def choose_portals(world, player): - if world.doorShuffle[player] in ['basic', 'crossed']: - cross_flag = world.doorShuffle[player] == 'crossed' + if world.doorShuffle[player] != ['vanilla']: + shuffle_flag = world.doorShuffle[player] != 'basic' + allowed = {} + if world.doorShuffle[player] == 'basic': + allowed = {name: {name} for name in dungeon_regions} + elif world.doorShuffle[player] == 'partitioned': + groups = [['Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower'], + ['Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town'], + ['Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower']] + allowed = {name: set(group) for group in groups for name in group} + elif world.doorShuffle[player] == 'crossed': + all_dungeons = set(dungeon_regions.keys()) + allowed = {name: all_dungeons for name in dungeon_regions} + # key drops allow the big key in the right place in Desert Tiles 2 bk_shuffle = world.bigkeyshuffle[player] or world.dropshuffle[player] std_flag = world.mode[player] == 'standard' @@ -443,7 +464,7 @@ def choose_portals(world, player): custom = customizer_portals(master_door_list, world, player) - if cross_flag: + if shuffle_flag: random.shuffle(shuffled_info) for dungeon, info in shuffled_info: outstanding_portals = list(dungeon_portals[dungeon]) @@ -457,15 +478,15 @@ def choose_portals(world, player): info.required_passage[target_region] = [x for x in possible_portals if x != sanc.name] info.required_passage = {x: y for x, y in info.required_passage.items() if len(y) > 0} for target_region, possible_portals in info.required_passage.items(): - candidates = find_portal_candidates(master_door_list, dungeon, custom, need_passage=True, - crossed=cross_flag, bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) + candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, need_passage=True, + bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) choice, portal = assign_portal(candidates, possible_portals, custom, world, player) portal.destination = True clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) dead_end_choices = info.total - 1 - len(portal_assignment[dungeon]) for i in range(0, dead_end_choices): - candidates = find_portal_candidates(master_door_list, dungeon, custom, dead_end_allowed=True, - crossed=cross_flag, bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) + candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, dead_end_allowed=True, + bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) possible_portals = outstanding_portals if not info.sole_entrance else [x for x in outstanding_portals if x != info.sole_entrance] choice, portal = assign_portal(candidates, possible_portals, custom, world, player) if choice.deadEnd: @@ -476,7 +497,7 @@ def choose_portals(world, player): clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) the_rest = info.total - len(portal_assignment[dungeon]) for i in range(0, the_rest): - candidates = find_portal_candidates(master_door_list, dungeon, custom, crossed=cross_flag, + candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, bk_shuffle=bk_shuffle, standard=hc_flag, rupee_bow=rupee_bow_flag) choice, portal = assign_portal(candidates, outstanding_portals, custom, world, player) clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) @@ -620,7 +641,7 @@ def disconnect_portal(portal, world, player): chosen_door.entranceFlag = False -def find_portal_candidates(door_list, dungeon, custom, need_passage=False, dead_end_allowed=False, crossed=False, +def find_portal_candidates(door_list, dungeon, custom, allowed, need_passage=False, dead_end_allowed=False, bk_shuffle=False, standard=False, rupee_bow=False): custom_portals, assigned_doors = custom if assigned_doors: @@ -628,10 +649,8 @@ def find_portal_candidates(door_list, dungeon, custom, need_passage=False, dead_ else: ret = door_list ret = [x for x in ret if bk_shuffle or not x.bk_shuffle_req] - if crossed: - ret = [x for x in ret if not x.dungeonLink or x.dungeonLink == dungeon or x.dungeonLink.startswith('link')] - else: - ret = [x for x in ret if x.entrance.parent_region.dungeon.name == dungeon] + ret = [x for x in ret if not x.dungeonLink or x.dungeonLink == dungeon or x.dungeonLink.startswith('link')] + ret = [x for x in ret if x.entrance.parent_region.dungeon.name in allowed[dungeon]] if need_passage: ret = [x for x in ret if x.passage] if not dead_end_allowed: @@ -789,7 +808,7 @@ def main_dungeon_pool(dungeon_pool, world, player): # todo: which dungeon to create dungeon_builders.update(create_dungeon_builders(sector_pool, connections_tuple, world, player, pool, entrances, splits)) - door_type_pools.append((pool, DoorTypePool(sector_pool, world, player))) + door_type_pools.append((pool, DoorTypePool(pool, world, player))) update_forced_keys(dungeon_builders, entrances_map, world, player) recombinant_builders = {} @@ -816,8 +835,60 @@ def main_dungeon_pool(dungeon_pool, world, player): target_items += 19 # 19 pot keys d_items = target_items - all_dungeon_items_cnt world.pool_adjustment[player] = d_items + # todo: remove unused pairs if not world.decoupledoors[player]: smooth_door_pairs(world, player) + cross_dungeon_clean_up(world, player) + + +def cross_dungeon_clean_up(world, player): + # Re-assign dungeon bosses + gt = world.get_dungeon('Ganons Tower', player) + for name, builder in world.dungeon_layouts[player].items(): + reassign_boss('GT Ice Armos', 'bottom', builder, gt, world, player) + reassign_boss('GT Lanmolas 2', 'middle', builder, gt, world, player) + reassign_boss('GT Moldorm', 'top', builder, gt, world, player) + + sanctuary = world.get_region('Sanctuary', player) + d_name = sanctuary.dungeon.name + if d_name != 'Hyrule Castle': + possible_portals = [] + for portal_name in dungeon_portals[d_name]: + portal = world.get_portal(portal_name, player) + if portal.door.name == 'Sanctuary S': + possible_portals.clear() + possible_portals.append(portal) + break + if not portal.destination and not portal.deadEnd: + possible_portals.append(portal) + if len(possible_portals) == 1: + world.sanc_portal[player] = possible_portals[0] + else: + reachable_portals = [] + for portal in possible_portals: + start_area = portal.door.entrance.parent_region + state = ExplorationState(dungeon=d_name) + state.visit_region(start_area) + state.add_all_doors_check_unattached(start_area, world, player) + explore_state(state, world, player) + if state.visited_at_all(sanctuary): + reachable_portals.append(portal) + world.sanc_portal[player] = random.choice(reachable_portals) + if world.intensity[player] >= 3: + if player in world.sanc_portal: + portal = world.sanc_portal[player] + else: + portal = world.get_portal('Sanctuary', player) + target = portal.door.entrance.parent_region + connect_simple_door(world, 'Sanctuary Mirror Route', target, player) + + check_entrance_fixes(world, player) + + if world.standardize_palettes[player] == 'standardize' and world.doorShuffle[player] != 'basic': + palette_assignment(world, player) + + refine_hints(world.dungeon_layouts[player]) + refine_boss_exits(world, player) def update_forced_keys(dungeon_builders, entrances_map, world, player): @@ -1629,14 +1700,6 @@ def combine_layouts(recombinant_builders, dungeon_builders, entrances_map, world dungeon_builders[recombine.name] = recombine -# todo: this allows cross-dungeon exploring via HC Ledge or Inaccessible Regions -# todo: @deprecated -def valid_region_to_explore(region, world, player): - return region and (region.type == RegionType.Dungeon - or region.name in world.inaccessible_regions[player] - or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard')) - - def setup_custom_door_types(world, player): if not hasattr(world, 'custom_door_types'): world.custom_door_types = defaultdict(dict) @@ -1668,58 +1731,30 @@ def setup_custom_door_types(world, player): class DoorTypePool: - def __init__(self, sectors, world, player): + def __init__(self, pool, world, player): self.smalls = 0 self.bombable = 0 self.dashable = 0 self.bigs = 0 self.traps = 0 - # self.tricky = 0 - # self.hidden = 0 + self.tricky = 0 + self.hidden = 0 # todo: custom pools? - self.count_via_sectors(sectors, world, player) + for dungeon in pool: + counts = door_type_counts[dungeon] + if world.door_type_mode[player] == 'chaos': + counts = self.chaos_shuffle(counts) + self.smalls += counts[0] + self.bigs += counts[1] + self.traps += counts[2] + self.bombable += counts[3] + self.dashable += counts[4] + self.hidden += counts[5] + self.tricky += counts[6] - def count_via_sectors(self, sectors, world, player): - skips = set() - for sector in sectors: - for region in sector.regions: - for ext in region.exits: - if ext.door: - d = ext.door - if d.name not in skips and d.type in [DoorType.Normal, DoorType.Interior]: - if d.smallKey: - self.smalls += 1 - elif d.bigKey: - self.bigs += 1 - elif d.blocked and d.trapFlag and 'Boss' not in d.name and 'Agahnim' not in d.name: - self.traps += 1 - elif d.name == 'TR Compass Room NW': - self.tricky += 1 - elif d.name in ['Skull Vines NW', 'Tower Altar NW']: - self.hidden += 1 - else: - kind = world.get_room(d.roomIndex, player).kind(d) - if kind == DoorKind.Bombable: - self.bombable += 1 - elif kind == DoorKind.Dashable: - self.dashable += 1 - if d.type == DoorType.Interior: - skips.add(d.dest.name) ## lookup a different way for interior door shuffle - elif d.type == DoorType.Normal: - for dp in world.paired_doors[player]: - if d.name == dp.door_a or d.name == dp.door_b: - skips.add(dp.door_b if d.name == dp.door_a else dp.door_a) - break - - def chaos_shuffle(self): + def chaos_shuffle(self, counts): weights = [1, 2, 4, 3, 2, 1] - self.smalls = random.choices(self.get_choices(self.smalls), weights=weights) - self.bombable = random.choices(self.get_choices(self.bombable), weights=weights) - self.dashable = random.choices(self.get_choices(self.dashable), weights=weights) - self.bigs = random.choices(self.get_choices(self.bigs), weights=weights) - self.traps = random.choices(self.get_choices(self.traps), weights=weights) - # self.tricky = random.choices(self.get_choices(self.tricky), weights=weights) - # self.hidden = random.choices(self.get_choices(self.hidden), weights=weights) + return [random.choices(self.get_choices(counts[i]), weights=weights) for i, c in enumerate(counts)] @staticmethod def get_choices(number): @@ -1728,24 +1763,24 @@ class DoorTypePool: class BuilderDoorCandidates: def __init__(self): - self.small = set() - self.big = set() - self.trap = set() - self.bombable = set() - self.dashable = set() - - self.checked = set() + self.small = [] + self.big = [] + self.trap = [] + self.bombable = [] + self.dashable = [] def shuffle_door_types(door_type_pools, paths, world, player): start_regions_map = {} - for name, builder in world.dungeon_layouts[player]: + for name, builder in world.dungeon_layouts[player].items(): start_regions = convert_regions(builder.path_entrances, world, player) start_regions_map[name] = start_regions + builder.candidates = BuilderDoorCandidates() used_doors = shuffle_door_traps(door_type_pools, paths, start_regions_map, world, player) # big keys + used_doors = shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, player) # small keys - + used_doors = shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player) # bombable / dashable # tricky / hidden @@ -1780,13 +1815,15 @@ def shuffle_door_traps(door_type_pools, paths, start_regions_map, world, player) for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon], - start_regions_map[dungeon], paths, world, player) + start_regions_map[dungeon], paths, world, player, + drop=True) trap_map[dungeon] = valid_traps if trap_number < suggestion_map[dungeon]: flex_map[dungeon] = 0 remaining += suggestion_map[dungeon] - trap_number - suggestion_map[dungeon] = trap_number + suggestion_map[dungeon] = trap_number builder_order = [x for x in pool if flex_map[x] > 0] + random.shuffle(builder_order) queue = deque(builder_order) while len(queue) > 0 and remaining > 0: dungeon = queue.popleft() @@ -1820,15 +1857,15 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] - find_big_key_candidates(builder, used_doors, world, player) + find_big_key_candidates(builder, start_regions_map[dungeon], used_doors, world, player) if custom_bk_doors[dungeon]: - builder.candidates.trap = filter_key_door_pool(builder.candidates.big, custom_bk_doors[dungeon]) + builder.candidates.big = filter_key_door_pool(builder.candidates.big, custom_bk_doors[dungeon]) remaining -= len(custom_bk_doors[dungeon]) ttl += len(builder.candidates.big) for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] proportion = len(builder.candidates.big) - calc = int(round(proportion * door_type_pool.big/ttl)) + calc = int(round(proportion * door_type_pool.bigs/ttl)) suggested = min(proportion, calc) remaining -= suggested suggestion_map[dungeon] = suggested @@ -1836,20 +1873,21 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] valid_doors, bk_number = find_valid_bk_combination(builder, suggestion_map[dungeon], - start_regions_map[dungeon], world, player) + start_regions_map[dungeon], world, player, True) bk_map[dungeon] = valid_doors if bk_number < suggestion_map[dungeon]: flex_map[dungeon] = 0 remaining += suggestion_map[dungeon] - bk_number - suggestion_map[dungeon] = bk_number + suggestion_map[dungeon] = bk_number builder_order = [x for x in pool if flex_map[x] > 0] + random.shuffle(builder_order) queue = deque(builder_order) while len(queue) > 0 and remaining > 0: dungeon = queue.popleft() builder = world.dungeon_layouts[player][dungeon] increased = suggestion_map[dungeon] + 1 valid_doors, bk_number = find_valid_bk_combination(builder, increased, start_regions_map[dungeon], - paths, world, player) + world, player) if valid_doors: bk_map[dungeon] = valid_doors remaining -= 1 @@ -1859,6 +1897,93 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, queue.append(dungeon) # time to re-assign reassign_big_key_doors(bk_map, world, player) + for name, big_list in bk_map.items(): + used_doors.update(flatten_pair_list(big_list)) + return used_doors + + +def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player): + for pool, door_type_pool in door_type_pools: + ttl = 0 + suggestion_map, small_map, flex_map = {}, {}, {} + remaining = door_type_pool.smalls + total_keys = remaining + if player in world.custom_door_types: + custom_key_doors = world.custom_door_types[player]['Key Door'] + else: + custom_key_doors = defaultdict(list) + + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + find_small_key_door_candidates(builder, start_regions_map[dungeon], used_doors, world, player) + if custom_key_doors[dungeon]: + builder.candidates.small = filter_key_door_pool(builder.candidates.small, custom_key_doors[dungeon]) + remaining -= len(custom_key_doors[dungeon]) + builder.key_doors_num = max(0, len(builder.candidates.small) - builder.key_drop_cnt) + total_keys -= builder.key_drop_cnt + ttl += builder.key_doors_num + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + calculated = int(round(builder.key_doors_num*total_keys/ttl)) + max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder, world, player)) + cand_len = max(0, len(builder.candidates.small) - builder.key_drop_cnt) + limit = min(max_keys, cand_len) + suggested = min(calculated, limit) + combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt) + while combo_size > 500000 and suggested > 0: + suggested -= 1 + combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt) + suggestion_map[dungeon] = builder.key_doors_num = suggested + builder.key_drop_cnt + builder.combo_size = combo_size + flex_map[dungeon] = (limit - suggested) if suggested < limit else 0 + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + valid_doors, small_number = find_valid_combination(builder, suggestion_map[dungeon], + start_regions_map[dungeon], world, player) + small_map[dungeon] = valid_doors + actual_chest_keys = small_number - builder.key_drop_cnt + if actual_chest_keys < suggestion_map[dungeon]: + flex_map[dungeon] = 0 + remaining += suggestion_map[dungeon] - actual_chest_keys + suggestion_map[dungeon] = small_number + builder_order = [world.dungeon_layouts[player][x] for x in pool if flex_map[x] > 0] + builder_order.sort(key=lambda b: b.combo_size) + queue = deque(builder_order) + while len(queue) > 0 and remaining > 0: + builder = queue.popleft() + dungeon = builder.name + increased = suggestion_map[dungeon] + 1 + builder.key_doors_num = increased + valid_doors, small_number = find_valid_combination(builder, increased, start_regions_map[dungeon], + world, player) + if valid_doors: + small_map[dungeon] = valid_doors + remaining -= 1 + suggestion_map[dungeon] = increased + flex_map[dungeon] -= 1 + if flex_map[dungeon] > 0: + builder.combo_size = ncr(len(builder.candidates.small), builder.key_doors_num) + queue.append(builder) + queue = deque(sorted(queue, key=lambda b: b.combo_size)) + else: + builder.key_doors_num -= 1 + # time to re-assign + reassign_key_doors(small_map, world, player) + for dungeon_name in pool: + if not world.retro[player]: + builder = world.dungeon_layouts[player][dungeon_name] + log_key_logic(builder.name, world.key_logic[player][builder.name]) + if world.doorShuffle[player] != 'basic': + actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0) + dungeon = world.get_dungeon(dungeon_name, player) + if actual_chest_keys == 0: + dungeon.small_keys = [] + else: + dungeon.small_keys = [ItemFactory(dungeon_keys[dungeon_name], player)] * actual_chest_keys + + for name, small_list in small_map.items(): + used_doors.update(flatten_pair_list(small_list)) + return used_doors def shuffle_key_doors(builder, world, player): @@ -1900,9 +2025,9 @@ def find_current_key_doors(builder): def find_trappable_candidates(builder, world, player): - if world.door_type_mode[player] != 'original': # all, chaos + if world.door_type_mode[player] not in ['original', 'big']: # all, chaos r_set = builder.master_sector.region_set() - filtered_doors = [ext.door for r in r_set for ext in r.exits + filtered_doors = [ext.door for r in r_set for ext in world.get_region(r, player).exits if ext.door and ext.door.type in [DoorType.Interior, DoorType.Normal]] for d in filtered_doors: # I only support the first 3 due to the trapFlag right now @@ -1913,14 +2038,14 @@ def find_trappable_candidates(builder, world, player): if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.BigKey] or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name) - or (kind == DoorKind.TrapTriggerable and d.direction in [Direction.North, Direction.East]) - or (kind == DoorKind.Trap2 and d.direction in [Direction.South, Direction.West])): - builder.candidates.trap.add(d) + or (kind == DoorKind.TrapTriggerable and d.direction in [Direction.South, Direction.East]) + or (kind == DoorKind.Trap2 and d.direction in [Direction.North, Direction.West])): + builder.candidates.trap.append(d) elif d.type == DoorType.Normal: if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.BigKey] or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name)): - builder.candidates.trap.add(d) + builder.candidates.trap.append(d) else: r_set = builder.master_sector.region_set() for r in r_set: @@ -1928,7 +2053,7 @@ def find_trappable_candidates(builder, world, player): if ext.door: d = ext.door if d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name: - builder.candidates.trap.add(d) + builder.candidates.trap.append(d) def find_valid_trap_combination(builder, suggested, start_regions, paths, world, player, drop=True): @@ -1965,6 +2090,7 @@ def find_valid_trap_combination(builder, suggested, start_regions, paths, world, itr = 0 proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed) proposal.extend(custom_trap_doors) + builder.trap_door_proposal = proposal return proposal, trap_doors_needed @@ -1982,21 +2108,40 @@ def filter_start_regions(builder, start_regions, world, player): def validate_trap_layout(proposal, builder, start_regions, paths, world, player): - return check_required_paths_with_traps(paths, proposal, builder.name, start_regions, world, player) + flag, state = check_required_paths_with_traps(paths, proposal, builder.name, start_regions, world, player) + if not flag: + return False + bk_special_loc = find_bk_special_location(builder, world, player) + if bk_special_loc: + if not state.found_forced_bk(): + return False + if world.accessibility[player] != 'beatable': + all_locations = [l for r in builder.master_sector.region_set() for l in world.get_region(r, player).locations] + if any(l not in state.found_locations for l in all_locations): + return False + return True +def find_bk_special_location(builder, world, player): + for r_name in builder.master_sector.region_set(): + region = world.get_region(r_name, player) + for loc in region.locations: + if loc.forced_big_key(): + return loc + return None + def check_required_paths_with_traps(paths, proposal, dungeon_name, start_regions, world, player): + cached_initial_state = None if len(paths[dungeon_name]) > 0: - states_to_explore = {} common_starts = tuple(start_regions) + states_to_explore = {common_starts: ([], 'all')} for path in paths[dungeon_name]: if type(path) is tuple: states_to_explore[tuple([path[0]])] = (path[1], 'any') else: - if common_starts not in states_to_explore: - states_to_explore[common_starts] = ([], 'all') + # if common_starts not in states_to_explore: + # states_to_explore[common_starts] = ([], 'all') states_to_explore[common_starts][0].append(path) - cached_initial_state = None for start_regs, info in states_to_explore.items(): dest_regs, path_type = info if type(dest_regs) is not list: @@ -2005,12 +2150,14 @@ def check_required_paths_with_traps(paths, proposal, dungeon_name, start_regions start_regions = convert_regions(start_regs, world, player) initial = start_regs == common_starts if not initial or cached_initial_state is None: + if cached_initial_state and any(not cached_initial_state.visited_at_all(r) for r in start_regions): + return False, None # can't start processing the initial state because start regs aren't reachable init = determine_init_crystal(initial, cached_initial_state, start_regions) state = ExplorationState2(init, dungeon_name) for region in start_regions: state.visit_region(region) state.add_all_doors_check_proposed_traps(region, proposal, world, player) - explore_state_proposed_traps(state, world, player) + explore_state_proposed_traps(state, proposal, world, player) if initial and cached_initial_state is None: cached_initial_state = state else: @@ -2020,8 +2167,8 @@ def check_required_paths_with_traps(paths, proposal, dungeon_name, start_regions else: valid, bad_region = check_if_all_regions_visited(state, check_paths) if not valid: - return False - return True + return False, None + return True, cached_initial_state def reassign_trap_doors(trap_map, world, player): @@ -2041,13 +2188,14 @@ def reassign_trap_doors(trap_map, world, player): elif kind in [DoorKind.Trap2, DoorKind.TrapTriggerable]: room.change(d.doorListPos, DoorKind.Normal) d.blocked = False + connect_one_way(world, d.name, d.dest.name, player) elif d.type is DoorType.Normal and d not in traps: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) d.blocked = False for d in traps: change_door_to_trap(d, world, player) world.spoiler.set_door_type(d.name, 'Trap Door', player) - logger.debug('Key Door: %s', d.name) + logger.debug('Trap Door: %s', d.name) def find_current_trap_doors(builder): @@ -2078,34 +2226,37 @@ def change_door_to_trap(d, world, player): verify_door_list_pos(d, room, world, player, pos=3) d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1}[d.doorListPos] room.change(d.doorListPos, new_kind) + if d.entrance.connected_region is not None: + d.entrance.connected_region.entrances.remove(d.entrance) + d.entrance.connected_region = None elif d.type is DoorType.Normal: d.blocked = True verify_door_list_pos(d, room, world, player, pos=3) d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1}[d.doorListPos] room.change(d.doorListPos, DoorKind.Trap) + if d.entrance.connected_region is not None: + d.entrance.connected_region.entrances.remove(d.entrance) + d.entrance.connected_region = None -def find_big_key_candidates(builder, used, world, player): - if world.door_type_mode[player] != 'original': # all, chaos - r_set = builder.master_sector.region_set() - filtered_doors = [ext.door for r in r_set for ext in r.exits - if ext.door and ext.door.type in [DoorType.Interior, DoorType.Normal]] - for d in filtered_doors: - if 0 <= d.doorListPos < 4 and not d.entranceFlag: - room = world.get_room(d.roomIndex, player) - kind = room.kind(d) - if d.type in [DoorType.Interior, DoorType.Normal]: - if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, - DoorKind.BigKey] and d not in used) - or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name) - or (kind == DoorKind.TrapTriggerable and d.direction in [Direction.North, Direction.East]) - or (kind == DoorKind.Trap2 and d.direction in [Direction.South, Direction.West])): - builder.candidates.big.add(d) - elif d.type == DoorType.Normal: - if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, - DoorKind.BigKey] - or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name)): - builder.candidates.trap.add(d) +def find_big_key_candidates(builder, start_regions, used, world, player): + if world.door_type_mode[player] != 'original': # big, all, chaos + # traverse dungeon and find candidates + candidates = [] + checked_doors = set() + for region in start_regions: + possible, checked = find_big_key_door_candidates(region, checked_doors, used, world, player) + candidates.extend([x for x in possible if x not in candidates]) + checked_doors.update(checked) + flat_candidates = [] + for candidate in candidates: + # not valid if: Normal Coupled and Pair in is Checked and Pair is not in Candidates + if (world.decoupledoors[player] or candidate.type != DoorType.Normal + or candidate.dest not in checked_doors or candidate.dest in candidates): + flat_candidates.append(candidate) + + paired_candidates = build_pair_list(flat_candidates) + builder.candidates.big = paired_candidates else: r_set = builder.master_sector.region_set() for r in r_set: @@ -2113,15 +2264,177 @@ def find_big_key_candidates(builder, used, world, player): if ext.door: d = ext.door if d.bigKey and d.type in [DoorType.Normal, DoorType.Interior]: - builder.candidates.big.add(d) + builder.candidates.big.append(d) -def find_small_key_door_candidates(builder, start_regions, world, player): +def find_big_key_door_candidates(region, checked, used, world, player): + decoupled = world.decoupledoors[player] + dungeon_name = region.dungeon.name + candidates = [] + checked_doors = list(checked) + queue = deque([(region, None, None)]) + while len(queue) > 0: + current, last_door, last_region = queue.pop() + for ext in current.exits: + d = ext.door + controlled = d + if d and d.controller: + d = d.controller + if (d and not d.blocked and d.dest is not last_door and d.dest is not last_region + and d not in checked_doors and d not in used): + valid = False + if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal] + and not d.entranceFlag and d.direction in [Direction.North, Direction.South]): + room = world.get_room(d.roomIndex, player) + position, kind = room.doorList[d.doorListPos] + if d.type == DoorType.Interior: + valid = kind in okay_interiors + if valid and d.dest not in candidates: # interior doors are not separable yet + candidates.append(d.dest) + elif d.type == DoorType.Normal: + if decoupled: + valid = kind in okay_normals + else: + d2 = d.dest + if d2 not in candidates and d2 not in used: + if d2.type == DoorType.Normal: + room_b = world.get_room(d2.roomIndex, player) + pos_b, kind_b = room_b.doorList[d2.doorListPos] + valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2) + else: + valid = kind in okay_normals + if valid and 0 <= d2.doorListPos < 4: + candidates.append(d2) + else: + valid = True + if valid and d not in candidates: + candidates.append(d) + connected = ext.connected_region + if valid_region_to_explore(connected, dungeon_name, world, player): + queue.append((ext.connected_region, controlled, current)) + if d is not None: + checked_doors.append(d) + return candidates, checked_doors + + +def find_valid_bk_combination(builder, suggested, start_regions, world, player, drop=True): + bk_door_pool = builder.candidates.big + bk_doors_needed = suggested + if player in world.custom_door_types: + custom_bk_doors = world.custom_door_types[player]['Big Key Door'][builder.name] + else: + custom_bk_doors = [] + if custom_bk_doors: + bk_door_pool = filter_key_door_pool(bk_door_pool, custom_bk_doors) + bk_doors_needed -= len(custom_bk_doors) + if len(bk_door_pool) < bk_doors_needed: + if not drop: + return None, 0 + bk_doors_needed = len(bk_door_pool) + combinations = ncr(len(bk_door_pool), bk_doors_needed) + itr = 0 + sample_list = build_sample_list(combinations, 10000) + proposal = kth_combination(sample_list[itr], bk_door_pool, bk_doors_needed) + proposal.extend(custom_bk_doors) + + start_regions = filter_start_regions(builder, start_regions, world, player) + while not validate_bk_layout(proposal, builder, start_regions, world, player): + itr += 1 + if itr >= len(sample_list): + if not drop: + return None, 0 + bk_doors_needed -= 1 + if bk_doors_needed < 0: + raise Exception(f'Bad dungeon {builder.name} - maybe custom bk doors are bad') + combinations = ncr(len(bk_door_pool), bk_doors_needed) + sample_list = build_sample_list(combinations, 10000) + itr = 0 + proposal = kth_combination(sample_list[itr], bk_door_pool, bk_doors_needed) + proposal.extend(custom_bk_doors) + builder.bk_door_proposal = proposal + return proposal, bk_doors_needed + + +def find_current_bk_doors(builder): + current_doors = [] + for region in builder.master_sector.regions: + for ext in region.exits: + d = ext.door + if d and d.type != DoorType.Logical and d.bigKey: + current_doors.append(d) + return current_doors + + +def reassign_big_key_doors(bk_map, world, player): + for name, big_doors in bk_map.items(): + flat_proposal = flatten_pair_list(big_doors) + builder = world.dungeon_layouts[player][name] + queue = deque(find_current_bk_doors(builder)) + while len(queue) > 0: + d = queue.pop() + if d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.bigKey = False + elif d.type is DoorType.Normal and d not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.bigKey = False + for obj in big_doors: + if type(obj) is tuple: + d1 = obj[0] + d2 = obj[1] + if d1.type is DoorType.Interior: + change_door_to_big_key(d1, world, player) + d2.bigKey = True # ensure flag is set + else: + names = [d1.name, d2.name] + found = False + for dp in world.paired_doors[player]: + if dp.door_a in names and dp.door_b in names: + dp.pair = True + found = True + elif dp.door_a in names: + dp.pair = False + elif dp.door_b in names: + dp.pair = False + if not found: + world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) + change_door_to_big_key(d1, world, player) + change_door_to_big_key(d2, world, player) + world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Big Key Door', player) + else: + d = obj + if d.type is DoorType.Interior: + change_door_to_big_key(d, world, player) + d.dest.bigKey = True # ensure flag is set + elif d.type is DoorType.SpiralStairs: + pass # we don't have spiral stairs candidates yet that aren't already key doors + elif d.type is DoorType.Normal: + change_door_to_big_key(d, world, player) + if not world.decoupledoors[player] and d.dest: + if d.dest.type in [DoorType.Normal]: + dest_room = world.get_room(d.dest.roomIndex, player) + if stateful_door(d.dest, dest_room.kind(d.dest)): + change_door_to_big_key(d.dest, world, player) + add_pair(d, d.dest, world, player) + world.spoiler.set_door_type(d.name, 'Big Key Door', player) + + +def change_door_to_big_key(d, world, player): + d.bigKey = True + room = world.get_room(d.roomIndex, player) + if room.doorList[d.doorListPos][1] != DoorKind.BigKey: + verify_door_list_pos(d, room, world, player) + room.change(d.doorListPos, DoorKind.BigKey) + + +def find_small_key_door_candidates(builder, start_regions, used, world, player): # traverse dungeon and find candidates candidates = [] checked_doors = set() for region in start_regions: - possible, checked = find_key_door_candidates(region, checked_doors, world, player) + possible, checked = find_key_door_candidates(region, checked_doors, used, world, player) candidates.extend([x for x in possible if x not in candidates]) checked_doors.update(checked) flat_candidates = [] @@ -2132,25 +2445,26 @@ def find_small_key_door_candidates(builder, start_regions, world, player): flat_candidates.append(candidate) paired_candidates = build_pair_list(flat_candidates) - builder.candidates = paired_candidates + builder.candidates.small = paired_candidates def calc_used_dungeon_items(builder, world, player): base = 2 + basic_flag = world.doorShuffle[player] == 'basic' if not world.bigkeyshuffle[player]: if builder.bk_required and not builder.bk_provided: base += 1 - if not world.compassshuffle[player]: + if not world.compassshuffle[player] and (builder.name not in ['Hyrule Castle', 'Agahnims Tower'] or not basic_flag): base += 1 - if not world.mapshuffle[player]: + if not world.mapshuffle[player] and (builder.name != 'Agahnims Tower' or not basic_flag): base += 1 return base -def find_valid_combination(builder, start_regions, world, player, drop_keys=True): +def find_valid_combination(builder, target, start_regions, world, player, drop_keys=True): logger = logging.getLogger('') - key_door_pool = list(builder.candidates) # can these be a set? - key_doors_needed = builder.key_doors_num + key_door_pool = list(builder.candidates.small) + key_doors_needed = target if player in world.custom_door_types: custom_key_doors = world.custom_door_types[player]['Key Door'][builder.name] else: @@ -2163,8 +2477,9 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True if len(key_door_pool) < key_doors_needed: if not drop_keys: logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) - return False + return None, 0 builder.key_doors_num -= key_doors_needed - len(key_door_pool) # reduce number of key doors + key_doors_needed = len(key_door_pool) logger.info('%s: %s', world.fish.translate("cli", "cli", "lowering.keys.candidates"), builder.name) combinations = ncr(len(key_door_pool), key_doors_needed) itr = 0 @@ -2181,12 +2496,12 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True if itr >= len(sample_list): if not drop_keys: logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) - return False + return None, 0 logger.info('%s: %s', world.fish.translate("cli","cli","lowering.keys.layouts"), builder.name) builder.key_doors_num -= 1 key_doors_needed -= 1 - if builder.key_doors_num < 0: - raise Exception('Bad dungeon %s - less than 0 key doors not valid' % builder.name) + if key_doors_needed < 0: + raise Exception(f'Bad dungeon {builder.name} - less than 0 key doors or invalid custom key door') combinations = ncr(len(key_door_pool), max(0, key_doors_needed)) sample_list = build_sample_list(combinations) itr = 0 @@ -2200,14 +2515,15 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True # make changes if player not in world.key_logic.keys(): world.key_logic[player] = {} + builder.total_keys = builder.key_doors_num analyze_dungeon(key_layout, world, player) builder.key_door_proposal = proposal world.key_logic[player][builder.name] = key_layout.key_logic world.key_layout[player][builder.name] = key_layout - return True + return builder.key_door_proposal, key_doors_needed -def build_sample_list(combinations, max_combinations=1000000): +def build_sample_list(combinations, max_combinations=100000): if combinations <= max_combinations: sample_list = list(range(0, int(combinations))) else: @@ -2273,10 +2589,13 @@ def flatten_pair_list(paired_list): return flat_list -okay_normals = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.DungeonChanger] +okay_normals = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, + DoorKind.DungeonChanger, DoorKind.BigKey] + +okay_interiors = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.BigKey] -def find_key_door_candidates(region, checked, world, player): +def find_key_door_candidates(region, checked, used, world, player): decoupled = world.decoupledoors[player] dungeon_name = region.dungeon.name candidates = [] @@ -2289,14 +2608,15 @@ def find_key_door_candidates(region, checked, world, player): controlled = d if d and d.controller: d = d.controller - if d and not d.blocked and d.dest is not last_door and d.dest is not last_region and d not in checked_doors: + if (d and not d.blocked and d.dest is not last_door and d.dest is not last_region + and d not in checked_doors and d not in used): valid = False if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal, DoorType.SpiralStairs] and not d.entranceFlag): room = world.get_room(d.roomIndex, player) position, kind = room.doorList[d.doorListPos] if d.type == DoorType.Interior: - valid = kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable] + valid = kind in okay_interiors if valid and d.dest not in candidates: # interior doors are not separable yet candidates.append(d.dest) elif d.type == DoorType.SpiralStairs: @@ -2306,7 +2626,7 @@ def find_key_door_candidates(region, checked, world, player): valid = kind in okay_normals else: d2 = d.dest - if d2 not in candidates: + if d2 not in candidates and d2 not in used: if d2.type == DoorType.Normal: room_b = world.get_room(d2.roomIndex, player) pos_b, kind_b = room_b.doorList[d2.doorListPos] @@ -2320,7 +2640,7 @@ def find_key_door_candidates(region, checked, world, player): if valid and d not in candidates: candidates.append(d) connected = ext.connected_region - if valid_region_to_explore_lim(connected, dungeon_name, world, player): + if valid_region_to_explore(connected, dungeon_name, world, player): queue.append((ext.connected_region, controlled, current)) if d is not None: checked_doors.append(d) @@ -2333,72 +2653,80 @@ def valid_key_door_pair(door1, door2): return len(door1.entrance.parent_region.exits) <= 1 or len(door2.entrance.parent_region.exits) <= 1 -def reassign_key_doors(builder, world, player): +def reassign_key_doors(small_map, world, player): logger = logging.getLogger('') - logger.debug('Key doors for %s', builder.name) - proposal = builder.key_door_proposal - flat_proposal = flatten_pair_list(proposal) - queue = deque(find_current_key_doors(builder)) - while len(queue) > 0: - d = queue.pop() - if d.type is DoorType.SpiralStairs and d not in proposal: - room = world.get_room(d.roomIndex, player) - if room.doorList[d.doorListPos][1] == DoorKind.StairKeyLow: - room.delete(d.doorListPos) - else: - if len(room.doorList) > 1: - room.mirror(d.doorListPos) # I think this works for crossed now - else: + for name, small_doors in small_map.items(): + logger.debug(f'Key doors for {name}') + builder = world.dungeon_layouts[player][name] + proposal = builder.key_door_proposal + flat_proposal = flatten_pair_list(proposal) + queue = deque(find_current_key_doors(builder)) + while len(queue) > 0: + d = queue.pop() + if d.type is DoorType.SpiralStairs and d not in proposal: + room = world.get_room(d.roomIndex, player) + if room.doorList[d.doorListPos][1] == DoorKind.StairKeyLow: room.delete(d.doorListPos) - d.smallKey = False - elif d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: - if not d.entranceFlag: - world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) - d.smallKey = False - d.dest.smallKey = False - queue.remove(d.dest) - elif d.type is DoorType.Normal and d not in flat_proposal: - if not d.entranceFlag: - world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) - d.smallKey = False - for dp in world.paired_doors[player]: - if dp.door_a == d.name or dp.door_b == d.name: - dp.pair = False - for obj in proposal: - if type(obj) is tuple: - d1 = obj[0] - d2 = obj[1] - if d1.type is DoorType.Interior: - change_door_to_small_key(d1, world, player) - d2.smallKey = True # ensure flag is set - else: - names = [d1.name, d2.name] - found = False + else: + if len(room.doorList) > 1: + room.mirror(d.doorListPos) # I think this works for crossed now + else: + room.delete(d.doorListPos) + d.smallKey = False + elif d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.smallKey = False + d.dest.smallKey = False + queue.remove(d.dest) + elif d.type is DoorType.Normal and d not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.smallKey = False for dp in world.paired_doors[player]: - if dp.door_a in names and dp.door_b in names: - dp.pair = True - found = True - elif dp.door_a in names: + if dp.door_a == d.name or dp.door_b == d.name: dp.pair = False - elif dp.door_b in names: - dp.pair = False - if not found: - world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) - change_door_to_small_key(d1, world, player) - change_door_to_small_key(d2, world, player) - world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Key Door', player) - logger.debug('Key Door: %s', d1.name+' <-> '+d2.name) - else: - d = obj - if d.type is DoorType.Interior: - change_door_to_small_key(d, world, player) - d.dest.smallKey = True # ensure flag is set - elif d.type is DoorType.SpiralStairs: - pass # we don't have spiral stairs candidates yet that aren't already key doors - elif d.type is DoorType.Normal: - change_door_to_small_key(d, world, player) - world.spoiler.set_door_type(d.name, 'Key Door', player) - logger.debug('Key Door: %s', d.name) + for obj in proposal: + if type(obj) is tuple: + d1 = obj[0] + d2 = obj[1] + if d1.type is DoorType.Interior: + change_door_to_small_key(d1, world, player) + d2.smallKey = True # ensure flag is set + else: + names = [d1.name, d2.name] + found = False + for dp in world.paired_doors[player]: + if dp.door_a in names and dp.door_b in names: + dp.pair = True + found = True + elif dp.door_a in names: + dp.pair = False + elif dp.door_b in names: + dp.pair = False + if not found: + world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) + change_door_to_small_key(d1, world, player) + change_door_to_small_key(d2, world, player) + world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Key Door', player) + logger.debug('Key Door: %s', d1.name+' <-> '+d2.name) + else: + d = obj + if d.type is DoorType.Interior: + change_door_to_small_key(d, world, player) + d.dest.smallKey = True # ensure flag is set + elif d.type is DoorType.SpiralStairs: + pass # we don't have spiral stairs candidates yet that aren't already key doors + elif d.type is DoorType.Normal: + change_door_to_small_key(d, world, player) + if not world.decoupledoors[player] and d.dest: + if d.dest.type in [DoorType.Normal]: + dest_room = world.get_room(d.dest.roomIndex, player) + if stateful_door(d.dest, dest_room.kind(d.dest)): + change_door_to_small_key(d.dest, world, player) + add_pair(d, d.dest, world, player) + world.spoiler.set_door_type(d.name, 'Key Door', player) + logger.debug('Key Door: %s', d.name) def change_door_to_small_key(d, world, player): @@ -2499,7 +2827,7 @@ def remove_pair(door, world, player): def stateful_door(door, kind): if 0 <= door.doorListPos < 4: - return kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable] #, DoorKind.BigKey] + return kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.BigKey] return False @@ -2780,7 +3108,8 @@ def explore_state(state, world, player): while len(state.avail_doors) > 0: door = state.next_avail_door().door connect_region = world.get_entrance(door.name, player).connected_region - if state.can_traverse(door) and not state.visited(connect_region) and valid_region_to_explore(connect_region, world, player): + if (state.can_traverse(door) and not state.visited(connect_region) + and valid_region_to_explore(connect_region, state.dungeon, world, player)): state.visit_region(connect_region) state.add_all_doors_check_unattached(connect_region, world, player) @@ -2789,7 +3118,8 @@ def explore_state_proposed_traps(state, proposed_traps, world, player): while len(state.avail_doors) > 0: door = state.next_avail_door().door connect_region = world.get_entrance(door.name, player).connected_region - if not state.visited(connect_region) and valid_region_to_explore(connect_region, world, player): + if (not state.visited(connect_region) + and valid_region_to_explore(connect_region, state.dungeon, world, player)): state.visit_region(connect_region) state.add_all_doors_check_proposed_traps(connect_region, proposed_traps, world, player) @@ -2851,6 +3181,7 @@ class DROptions(Flag): # Open_Desert_Wall = 0x80 # No longer pre-opening desert wall - unused Hide_Total = 0x100 DarkWorld_Spawns = 0x200 + BigKeyDoor_Shuffle = 0x400 # DATA GOES DOWN HERE @@ -3913,4 +4244,21 @@ bomb_dash_counts = { 'Ganons Tower': (2, 1) } +# small, big, trap, bomb, dash, hidden, tricky +door_type_counts = { + 'Hyrule Castle': (4, 0, 1, 0, 2, 0, 0), + 'Eastern Palace': (2, 2, 0, 0, 0, 0, 0), + 'Desert Palace': (4, 1, 0, 0, 0, 0, 0), + 'Agahnims Tower': (4, 0, 1, 0, 0, 1, 0), + 'Swamp Palace': (6, 0, 0, 2, 0, 0, 0), + 'Palace of Darkness': (6, 1, 1, 3, 2, 0, 0), + 'Misery Mire': (6, 3, 5, 2, 0, 0, 0), + 'Skull Woods': (5, 0, 2, 2, 0, 1, 0), + 'Ice Palace': (6, 1, 3, 0, 0, 0, 0), + 'Tower of Hera': (1, 1, 0, 0, 0, 0, 0), + 'Thieves Town': (3, 1, 2, 1, 1, 0, 0), + 'Turtle Rock': (6, 2, 2, 0, 2, 0, 1), # 2 bombs kind of for entrances, but I put 0 here + 'Ganons Tower': (8, 2, 5, 2, 1, 0, 0) +} + diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 7e3b2ef7..f06c3535 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -808,6 +808,7 @@ class ExplorationState(object): self.prize_door_set = {} self.prize_doors = [] self.prize_doors_opened = False + self.prize_received = False def copy(self): ret = ExplorationState(dungeon=self.dungeon) @@ -839,6 +840,7 @@ class ExplorationState(object): ret.prize_door_set = dict(self.prize_door_set) ret.prize_doors = list(self.prize_doors) ret.prize_doors_opened = self.prize_doors_opened + ret.prize_received = self.prize_received return ret def next_avail_door(self): @@ -984,6 +986,20 @@ class ExplorationState(object): elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors) + def add_all_doors_check_big_keys(self, region, big_key_door_proposal, world, player): + for door in get_doors(world, region, player): + if self.can_traverse(door): + if door.controller: + door = door.controller + if (door in big_key_door_proposal or door.name in special_big_key_doors) and not self.big_key_opened: + if not self.in_door_list(door, self.big_doors): + self.append_door_to_list(door, self.big_doors) + elif door.req_event is not None and door.req_event not in self.events: + if not self.in_door_list(door, self.event_doors): + self.append_door_to_list(door, self.event_doors) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors) + def visited(self, region): if self.crystal == CrystalBarrier.Either: return region in self.visited_blue and region in self.visited_orange @@ -1213,6 +1229,8 @@ class DungeonBuilder(object): self.combo_size = None self.flex = 0 self.key_door_proposal = None + self.bk_door_proposal = None + self.trap_door_proposal = None self.allowance = None if 'Stonewall' in name: @@ -1279,9 +1297,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge dungeon_map = {} for key in dungeon_pool: - dungeon_map[key] = DungeonBuilder(key) - for key in dungeon_boss_sectors.keys(): - current_dungeon = dungeon_map[key] + current_dungeon = dungeon_map[key] = DungeonBuilder(key) for r_name in dungeon_boss_sectors[key]: assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole) if key == 'Hyrule Castle' and world.mode[player] == 'standard': @@ -1293,7 +1309,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge candidate_sectors, global_pole) entrances_map, potentials, connections = connections_tuple accessible_sectors, reverse_d_map = set(), {} - for key in dungeon_entrances.keys(): + for key in dungeon_pool: current_dungeon = dungeon_map[key] current_dungeon.all_entrances = dungeon_entrances[key] for r_name in current_dungeon.all_entrances: @@ -1419,6 +1435,8 @@ def identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, if ent_name in found_connections: continue sector = find_sector(ent_name, reverse_d_map.keys()) + if sector is None: + continue if sector in accessible_sectors: found_connections.add(ent_name) accessible_overworld.add(region) # todo: drops don't give ow access diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index bb4f6835..8a70abcf 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -248,7 +248,7 @@ def find_all_locations(sector): def calc_max_chests(builder, key_layout, world, player): - if world.doorShuffle[player] != 'crossed': + if world.doorShuffle[player] in ['basic', 'vanilla']: return len(world.get_dungeon(key_layout.sector.name, player).small_keys) return max(0, builder.key_doors_num - key_layout.max_drops) @@ -1169,6 +1169,16 @@ def expand_key_state(state, flat_proposal, world, player): state.add_all_doors_check_keys(connect_region, flat_proposal, world, player) +def expand_big_key_state(state, flat_proposal, world, player): + while len(state.avail_doors) > 0: + exp_door = state.next_avail_door() + door = exp_door.door + connect_region = world.get_entrance(door.name, player).connected_region + if state.validate(door, connect_region, world, player): + state.visit_region(connect_region, key_checks=True) + state.add_all_doors_check_big_keys(connect_region, flat_proposal, world, player) + + def flatten_pair_list(paired_list): flat_list = [] for d in paired_list: @@ -1398,6 +1408,42 @@ def prize_relevance(key_layout, dungeon_entrance): return None +def prize_relevance_sig2(start_regions, d_name, dungeon_entrance): + if len(start_regions) > 1 and dungeon_entrance and dungeon_table[d_name].prize: + if dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower']: + return 'GT' + elif dungeon_entrance.name == 'Pyramid Fairy': + return 'BigBomb' + return None + + +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] or not bk_special): + return True + flat_proposal = flatten_pair_list(proposal) + state = ExplorationState(dungeon=builder.name) + state.big_key_special = bk_special + 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) + if prize_relevant_flag: + 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 + else: + state.visit_region(region, key_checks=True) + state.add_all_doors_check_big_keys(region, flat_proposal, world, player) + expand_big_key_state(state, flat_proposal, world, player) + if bk_special: + for loc in state.found_locations: + if loc.forced_big_key(): + return True + else: + return len(state.bk_found) > 0 + return False + + # Soft lock stuff def validate_key_layout(key_layout, world, player): # retro is all good - except for hyrule castle in standard mode @@ -1601,7 +1647,7 @@ def create_key_counters(key_layout, world, player): state.key_locations = len(builder.key_door_proposal) - builder.key_drop_cnt else: builder = world.dungeon_layouts[player][key_layout.sector.name] - state.key_locations = builder.total_keys - builder.key_drop_cnt + state.key_locations = max(0, builder.total_keys - builder.key_drop_cnt) state.big_key_special = False for region in key_layout.sector.regions: for location in region.locations: diff --git a/Main.py b/Main.py index 80c79887..4e6b9c4b 100644 --- a/Main.py +++ b/Main.py @@ -106,6 +106,7 @@ def main(args, seed=None, fish=None): world.enemy_damage = args.enemy_damage.copy() world.beemizer = args.beemizer.copy() world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)} + world.door_type_mode = args.door_type_mode.copy() world.experimental = args.experimental.copy() world.dungeon_counters = args.dungeon_counters.copy() world.fish = fish diff --git a/Rom.py b/Rom.py index 4cb0354c..d6717617 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'add982e935888df04ddfa570bc07bede' +RANDOMIZERBASEHASH = 'afcd895b87559cd29b04aa3714cbc929' class JsonRom(object): @@ -738,11 +738,11 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # setup dr option flags based on experimental, etc. dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: dr_flags |= DROptions.Map_Info if world.collection_rate[player] and world.goal[player] not in ['triforcehunt', 'trinity']: dr_flags |= DROptions.Debug - if world.doorShuffle[player] == 'crossed' and world.logic[player] != 'nologic'\ + if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic'\ and world.mixed_travel[player] == 'prevent': # PoD Falling Bridge or Hammjump # 1FA607: db $2D, $79, $69 ; 0x0069: Vertical Rail ↕ | { 0B, 1E } | Size: 05 @@ -763,6 +763,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): dr_flags |= DROptions.DarkWorld_Spawns if world.logic[player] != 'nologic': dr_flags |= DROptions.Fix_EG + if world.door_type_mode in ['big', 'chaos']: + dr_flags |= DROptions.BigKeyDoor_Shuffle my_locations = world.get_filled_locations(player) valid_locations = [l for l in my_locations if ((l.type == LocationType.Pot and not l.forced_item) @@ -772,7 +774,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): valid_loc_by_dungeon = valid_dungeon_locations(valid_locations) # fix hc big key problems (map and compass too) - if world.doorShuffle[player] == 'crossed' or world.dropshuffle[player] or world.pottery[player] not in ['none', 'cave']: + if (world.doorShuffle[player] not in ['vanilla', 'basic'] or world.dropshuffle[player] + or world.pottery[player] not in ['none', 'cave']): rom.write_byte(0x151f1, 2) rom.write_byte(0x15270, 2) sanctuary = world.get_region('Sanctuary', player) @@ -800,7 +803,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x13fff4, [0xe4, 0x00]) # patch doors - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: rom.write_byte(0x138002, 2) for name, layout in world.key_layout[player].items(): offset = compass_data[name][4]//2 @@ -1377,7 +1380,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if len(portal_list) == 1: portal_idx = 0 else: - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: # the random choice excludes sanctuary portal_idx = next((i for i, elem in enumerate(portal_list) if world.get_portal(elem, player).chosen), random.choice([1, 2, 3])) @@ -1392,7 +1395,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x53E56+ow_map_index*2, int16_as_bytes(coords[1])) rom.write_byte(0x53EA6+ow_map_index, world_indicator) # in crossed doors - flip the compass exists flags - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: for dungeon, portal_list in dungeon_portals.items(): ow_map_index = dungeon_table[dungeon].map_index exists_flag = any(x for x in world.get_dungeon(dungeon, player).dungeon_items if x.type == 'Compass') @@ -1495,7 +1498,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x180188, [0x20, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0x20, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) magic_max, magic_small = 0x80, 0x20 - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180185, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)]) rom.write_bytes(0x180188, [0x20, 3, 10]) # Zelda respawn refills (magic, bombs, arrows) @@ -2081,13 +2084,13 @@ def write_strings(rom, world, player, team): # Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable. locations_to_hint = InconvenientLocations.copy() - if world.doorShuffle[player] != 'crossed': + if world.doorShuffle[player] == 'vanilla': locations_to_hint.extend(InconvenientDungeonLocations) if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: locations_to_hint.extend(InconvenientVanillaLocations) random.shuffle(locations_to_hint) hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 5 - hint_count -= 2 if world.doorShuffle[player] == 'crossed' else 0 + hint_count -= 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0 del locations_to_hint[hint_count:] for location in locations_to_hint: if location == 'Swamp Left': @@ -2150,7 +2153,7 @@ def write_strings(rom, world, player, team): items_to_hint.extend(BigKeys) random.shuffle(items_to_hint) hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 8 - hint_count += 2 if world.doorShuffle[player] == 'crossed' else 0 + hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0 while hint_count > 0 and len(items_to_hint) > 0: this_item = items_to_hint.pop(0) this_location = world.find_items_not_key_only(this_item, player) @@ -2163,8 +2166,8 @@ def write_strings(rom, world, player, team): tt[hint_locations.pop(0)] = this_hint hint_count -= 1 - # Adding a hint for the Thieves' Town Attic location in Crossed door shuffle. - if world.doorShuffle[player] in ['crossed']: + # Adding a hint for the Thieves' Town Attic location in mixed door shuffles. + if world.doorShuffle[player] not in ['vanilla', 'basic']: attic_hint = world.get_location("Thieves' Town - Attic", player).parent_region.dungeon.name this_hint = 'A cracked floor can be found in ' + attic_hint + '.' if world.intensity[player] < 2 and hint_locations[0] == 'telepathic_tile_thieves_town_upstairs': @@ -2324,7 +2327,7 @@ def write_strings(rom, world, player, team): tt['tablet_bombos_book'] = bombos_text # attic hint - if world.doorShuffle[player] in ['crossed']: + if world.doorShuffle[player] not in ['vanilla', 'basic']: attic_hint = world.get_location("Thieves' Town - Attic", player).parent_region.dungeon.name tt['blind_not_that_way'] = f'{attic_hint} is too bright for my eyes' # see tagalog.asm tables at 957,967 or Follower_HandleTrigger in JPDASM diff --git a/RoomData.py b/RoomData.py index cfdd183f..03700e32 100644 --- a/RoomData.py +++ b/RoomData.py @@ -395,8 +395,8 @@ class DoorKind(Enum): Bombable = 0x2E BlastWall = 0x30 Hidden = 0x32 - TrapTriggerable = 0x36 # right side trap or south side trap (West, South) - Trap2 = 0x38 # left side trap or north side trap (East, North) + TrapTriggerable = 0x36 # right side trap or bottom side trap (West, North) + Trap2 = 0x38 # left side trap or top side trap (East, South) NormalLow2 = 0x40 TrapTriggerableLow = 0x44 Warp = 0x46 diff --git a/data/base2current.bps b/data/base2current.bps index 060733f6e6579357fc34f52d728345c5cfe9ea01..d657ad40ba6c5ce7ceabd188463dc93b16f6db49 100644 GIT binary patch delta 3819 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 {secondary_door.name}') + logger.debug(f' Linking {primary_door.name} <-> {secondary_door.name}') else: - logger.debug(f'Linking {primary_door.name} -> {secondary_door.name}') + logger.debug(f' Linking {primary_door.name} -> {secondary_door.name}') def decouple_check(primary_list, secondary_list, primary_door, secondary_door, world, player): @@ -203,11 +203,11 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se itr += 1 if not world.decoupledoors[player]: - logger.debug(f'Re-linking {attempt.name} <-> {new_door.name}') - logger.debug(f'Re-linking {old_attempt.name} <-> {old_target.name}') + logger.debug(f' Re-linking {attempt.name} <-> {new_door.name}') + logger.debug(f' Re-linking {old_attempt.name} <-> {old_target.name}') else: - logger.debug(f'Re-Linking {attempt.name} -> {new_door.name}') - logger.debug(f'Re-Linking {old_attempt.name} -> {old_target.name}') + logger.debug(f' Re-Linking {attempt.name} -> {new_door.name}') + logger.debug(f' Re-Linking {old_attempt.name} -> {old_target.name}') hash_code_set.add(hash_code) return proposed_map, hash_code @@ -349,12 +349,7 @@ def connect_doors(a, b): return # Connect supported types if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]: - if a.blocked: - connect_one_way(b.entrance, a.entrance) - elif b.blocked: - connect_one_way(a.entrance, b.entrance) - else: - connect_two_way(a.entrance, b.entrance) + connect_two_way(a.entrance, b.entrance) dep_doors, target = [], None if len(a.dependents) > 0: dep_doors, target = a.dependents, b @@ -527,7 +522,7 @@ class ExplorationState(object): self.key_locations += 1 if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']: self.ttl_locations += 1 - if location not in self.found_locations: # todo: special logic for TT Boss? + if location not in self.found_locations: self.found_locations.append(location) if not bk_flag: self.bk_found.add(location) @@ -616,7 +611,7 @@ class ExplorationState(object): elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors, flag) - def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, flag, world, player): + def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, world, player): for door in get_doors(world, region, player): if door in proposed_map and door.name in valid_doors: self.visited_doors.add(door) @@ -625,16 +620,16 @@ class ExplorationState(object): door = door.controller if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors: if not self.in_door_list_ic(door, self.unattached_doors): - self.append_door_to_list(door, self.unattached_doors, flag) + self.append_door_to_list(door, self.unattached_doors) else: other = self.find_door_in_list(door, self.unattached_doors) if self.crystal != other.crystal: other.crystal = CrystalBarrier.Either elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors): - self.append_door_to_list(door, self.event_doors, flag) + self.append_door_to_list(door, self.event_doors) elif not self.in_door_list(door, self.avail_doors): - self.append_door_to_list(door, self.avail_doors, flag) + self.append_door_to_list(door, self.avail_doors) def add_all_doors_check_proposed_traps(self, region, proposed_traps, world, player): for door in get_doors(world, region, player): diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 8325719b..3816ef7c 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -77,6 +77,7 @@ def roll_settings(weights): door_shuffle = get_choice('door_shuffle') ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla' ret.intensity = get_choice('intensity') + ret.door_type_mode = get_choice('door_type_mode') ret.decoupledoors = get_choice('decoupledoors') == 'on' ret.experimental = get_choice('experimental') == 'on' ret.collection_rate = get_choice('collection_rate') == 'on'