diff --git a/BaseClasses.py b/BaseClasses.py index 3145ee16..404428a1 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -310,7 +310,7 @@ class World(object): return self.is_tile_swapped(0x03, player) and self.is_tile_swapped(0x1b, player) def is_bombshop_start(self, player): - return self.is_tile_swapped(0x2c, player) and (self.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] or not self.shufflelinks[player]) + return self.is_tile_swapped(0x2c, player) def is_pyramid_open(self, player): if self.open_pyramid[player] == 'yes': @@ -318,7 +318,7 @@ class World(object): elif self.open_pyramid[player] == 'no': return False else: - if self.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: + if self.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district']: return False elif self.goal[player] in ['crystals', 'trinity', 'ganonhunt']: return True @@ -3464,7 +3464,7 @@ class Pot(object): # byte 0: DDDE EEEE (DR, ER) dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3, 'paired': 4} er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8, - 'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6, "swapped": 10} + 'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6, "swapped": 10, "district": 11} # byte 1: LLLW WSS? (logic, mode, sword) logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4, "hybridglitches": 5} diff --git a/Bosses.py b/Bosses.py index 7052bb5c..a4886be1 100644 --- a/Bosses.py +++ b/Bosses.py @@ -4,11 +4,12 @@ import RaceRandom as random from BaseClasses import Boss, FillError -def BossFactory(boss, player): +def BossFactory(boss, player, on_ice=False): if boss is None: return None if boss in boss_table: - enemizer_name, defeat_rule = boss_table[boss] + enemizer_name, normal_defeat_rule, ice_defeat_rule = boss_table[boss] + defeat_rule = ice_defeat_rule if on_ice else normal_defeat_rule return Boss(boss, enemizer_name, defeat_rule, player) logging.getLogger('').error('Unknown Boss: %s', boss) @@ -41,16 +42,21 @@ def MoldormDefeatRule(state, player): def HelmasaurKingDefeatRule(state, player): return (state.has('Hammer', player) or state.can_use_bombs(player)) and (state.has_sword(player) or state.can_shoot_arrows(player)) + +def IceHelmasaurKingDefeatRule(state, player): + return state.can_use_bombs(player) and (state.has_sword(player) or state.can_shoot_arrows(player)) + + def ArrghusDefeatRule(state, player): if not state.has('Hookshot', player): return False - # TODO: ideally we would have a check for bow and silvers, which combined with the - # hookshot is enough. This is not coded yet because the silvers that only work in pyramid feature - # makes this complicated if state.has_blunt_weapon(player): return True - return ((state.has('Fire Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 12))) or #assuming mostly gitting two puff with one shot + if state.can_shoot_arrows(player) and state.has('Silver Arrows', player) and state.world.difficulty_adjustments[player] not in ['hard', 'expert']: + return True + + return ((state.has('Fire Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 12))) or # assuming mostly getting two puffs with one shot (state.has('Ice Rod', player) and state.can_use_bombs(player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16)))) @@ -64,9 +70,27 @@ def MothulaDefeatRule(state, player): (state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) ) + def BlindDefeatRule(state, player): return state.has_blunt_weapon(player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player) + +def IceBlindDefeatRule(state, player): + return ( + ( + # weapon + state.has_beam_sword(player) or + state.has('Cane of Somaria', player) or + (state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) + ) and + ( + # protection + state.has('Red Shield', player) or + (state.has('Cane of Byrna', player) and state.world.difficulty_adjustments[player] not in ['hard', 'expert']) + ) + ) + + def KholdstareDefeatRule(state, player): return ( ( @@ -90,9 +114,39 @@ def KholdstareDefeatRule(state, player): ) ) + +def IceKholdstareDefeatRule(state, player): + return ( + ( + state.has('Fire Rod', player) or + ( + state.has('Bombos', player) and + # FIXME: the following only actually works for the vanilla location for swordless + (state.has_sword(player) or state.world.swords[player] == 'swordless') + ) + ) and + ( + state.has_beam_sword(player) or + (state.has('Fire Rod', player) and state.can_extend_magic(player, 20)) or + # FIXME: this actually only works for the vanilla location for swordless + ( + state.has('Fire Rod', player) and + state.has('Bombos', player) and + (state.has_sword(player) or state.world.swords[player] == 'swordless') and + state.can_extend_magic(player, 16) + ) + ) + ) + + def VitreousDefeatRule(state, player): return (state.can_shoot_arrows(player) and state.can_use_bombs(player)) or state.has_blunt_weapon(player) + +def IceVitreousDefeatRule(state, player): + return (state.can_shoot_arrows(player) and state.can_use_bombs(player)) or state.has_beam_sword(player) + + def TrinexxDefeatRule(state, player): if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)): return False @@ -102,24 +156,36 @@ def TrinexxDefeatRule(state, player): (state.has('Master Sword', player) and state.can_extend_magic(player, 16)) or (state.has_sword(player) and state.can_extend_magic(player, 32))) + +def IceTrinexxDefeatRule(state, player): + if not (state.has('Fire Rod', player) and state.has('Ice Rod', player) and state.has_Boots(player)): + return False + return (state.has('Golden Sword', player) or + (state.has('Tempered Sword', player) and state.can_extend_magic(player, 16)) or + ((state.has('Hammer', player) or + state.has('Master Sword', player)) and state.can_extend_magic(player, 32))) # rod spam rule + + def AgahnimDefeatRule(state, player): return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player) + boss_table = { - 'Armos Knights': ('Armos', ArmosKnightsDefeatRule), - 'Lanmolas': ('Lanmola', LanmolasDefeatRule), - 'Moldorm': ('Moldorm', MoldormDefeatRule), - 'Helmasaur King': ('Helmasaur', HelmasaurKingDefeatRule), - 'Arrghus': ('Arrghus', ArrghusDefeatRule), - 'Mothula': ('Mothula', MothulaDefeatRule), - 'Blind': ('Blind', BlindDefeatRule), - 'Kholdstare': ('Kholdstare', KholdstareDefeatRule), - 'Vitreous': ('Vitreous', VitreousDefeatRule), - 'Trinexx': ('Trinexx', TrinexxDefeatRule), - 'Agahnim': ('Agahnim', AgahnimDefeatRule), - 'Agahnim2': ('Agahnim2', AgahnimDefeatRule) + 'Armos Knights': ('Armos', ArmosKnightsDefeatRule, ArmosKnightsDefeatRule), + 'Lanmolas': ('Lanmola', LanmolasDefeatRule, LanmolasDefeatRule), + 'Moldorm': ('Moldorm', MoldormDefeatRule, MoldormDefeatRule), + 'Helmasaur King': ('Helmasaur', HelmasaurKingDefeatRule, IceHelmasaurKingDefeatRule), + 'Arrghus': ('Arrghus', ArrghusDefeatRule, ArrghusDefeatRule), + 'Mothula': ('Mothula', MothulaDefeatRule, MothulaDefeatRule), + 'Blind': ('Blind', BlindDefeatRule, IceBlindDefeatRule), + 'Kholdstare': ('Kholdstare', KholdstareDefeatRule, IceKholdstareDefeatRule), + 'Vitreous': ('Vitreous', VitreousDefeatRule, IceVitreousDefeatRule), + 'Trinexx': ('Trinexx', TrinexxDefeatRule, IceTrinexxDefeatRule), + 'Agahnim': ('Agahnim', AgahnimDefeatRule, AgahnimDefeatRule), + 'Agahnim2': ('Agahnim2', AgahnimDefeatRule, AgahnimDefeatRule) } + def can_place_boss(world, player, boss, dungeon_name, level=None): if world.swords[player] in ['swordless'] and boss == 'Kholdstare' and dungeon_name != 'Ice Palace': return False @@ -132,6 +198,11 @@ def can_place_boss(world, player, boss, dungeon_name, level=None): if boss in ["Blind"]: return False + # no Trinexx on Ice in doors without doing some health modelling + if world.doorShuffle[player] != 'vanilla' and boss == 'Trinexx': + if dungeon_name == 'Ganons Tower' and level == 'bottom': + return False + if dungeon_name == 'Tower of Hera' and boss in ["Armos Knights", "Arrghus", "Blind", "Trinexx", "Lanmolas"]: return False @@ -151,6 +222,7 @@ def place_bosses(world, player): ['Tower of Hera', None], ['Skull Woods', None], ['Ganons Tower', 'middle'], + ['Ganons Tower', 'bottom'], ['Eastern Palace', None], ['Desert Palace', None], ['Palace of Darkness', None], @@ -159,7 +231,6 @@ def place_bosses(world, player): ['Ice Palace', None], ['Misery Mire', None], ['Turtle Rock', None], - ['Ganons Tower', 'bottom'], ] all_bosses = sorted(boss_table.keys()) #s orted to be deterministic on older pythons @@ -246,4 +317,4 @@ def place_boss(boss, level, loc, loc_text, world, player): loc = [x.name for x in world.dungeons if x.player == player and level in x.bosses.keys()][0] loc_text = loc + ' (' + level + ')' logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text) - world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player) + world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player, level == 'bottom') diff --git a/CHANGELOG.md b/CHANGELOG.md index 98cb8905..552be77a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,18 @@ # Changelog +## 0.3.4.1 +- Implemented new District ER mode option +- Added alternate boss logic when in GT Ice Basement +- Updated Inverted 2.0 to start in Bomb Shop regardless of ER mode +- Fixed broken customizer features with OWR Tile Flip +- Allowing Insanity ER + Standard to decouple standard entrances + ## 0.3.4.0 -- \~Merged in some things from DR v1.4.0.0-v~ +- \~Merged in DR v1.2.0.23~ - Improved bunny-walking algorithm - Improved multiworld balancing -- Implemented Hyrid Major Glitches logic (thanks Muffins/Espeon) +- \~Merged in some things from DR v1.4.0.0-v~ +- Implemented Hybrid Major Glitches logic (thanks Muffins/Espeon) - Added sparkles to Bonk Drop locations for better visibility - Some tweaks/improvements to Shuffle Song Instruments - Replaced Save Settings on Exit with Settings on Load diff --git a/DoorShuffle.py b/DoorShuffle.py index 99577f37..3ae1c709 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -262,23 +262,24 @@ def vanilla_key_logic(world, player): world.key_layout[player][builder.name] = key_layout log_key_logic(builder.name, key_layout.key_logic) # special adjustments for vanilla - if world.mode[player] != 'standard' and world.dropshuffle[player] == 'none': - # adjust hc doors - def adjust_hc_door(door_rule): - if door_rule.new_rules[KeyRuleType.WorstCase] == 3: - door_rule.new_rules[KeyRuleType.WorstCase] = 2 - door_rule.small_key_num = 2 + if world.keyshuffle[player] != 'universal': + if world.mode[player] != 'standard' and not world.dropshuffle[player]: + # adjust hc doors + def adjust_hc_door(door_rule): + if door_rule.new_rules[KeyRuleType.WorstCase] == 3: + door_rule.new_rules[KeyRuleType.WorstCase] = 2 + door_rule.small_key_num = 2 - rules = world.key_logic[player]['Hyrule Castle'].door_rules - adjust_hc_door(rules['Sewers Secret Room Key Door S']) - adjust_hc_door(rules['Hyrule Dungeon Map Room Key Door S']) - adjust_hc_door(rules['Sewers Dark Cross Key Door N']) - # adjust pod front door - pod_front = world.key_logic[player]['Palace of Darkness'].door_rules['PoD Middle Cage N'] - if pod_front.new_rules[KeyRuleType.WorstCase] == 6: - pod_front.new_rules[KeyRuleType.WorstCase] = 1 - pod_front.small_key_num = 1 - # gt logic? I'm unsure it needs adjusting + rules = world.key_logic[player]['Hyrule Castle'].door_rules + adjust_hc_door(rules['Sewers Secret Room Key Door S']) + adjust_hc_door(rules['Hyrule Dungeon Map Room Key Door S']) + adjust_hc_door(rules['Sewers Dark Cross Key Door N']) + # adjust pod front door + pod_front = world.key_logic[player]['Palace of Darkness'].door_rules['PoD Middle Cage N'] + if pod_front.new_rules[KeyRuleType.WorstCase] == 6: + pod_front.new_rules[KeyRuleType.WorstCase] = 1 + pod_front.small_key_num = 1 + # gt logic? I'm unsure it needs adjusting def validate_vanilla_reservation(dungeon, world, player): diff --git a/ItemList.py b/ItemList.py index 9af16cf6..fa658559 100644 --- a/ItemList.py +++ b/ItemList.py @@ -986,7 +986,8 @@ def balance_prices(world, player): def check_hints(world, player): - if world.shuffle[player] in ['simple', 'restricted', 'full', 'lite', 'lean', 'swapped', 'crossed', 'insanity']: + if (world.shuffle[player] in ['simple', 'restricted', 'full', 'district', 'swapped', 'crossed', 'insanity'] + or (world.shuffle[player] in ['lite', 'lean'] and world.shopsanity[player])): for shop, location_list in shop_to_location_table.items(): if shop in ['Capacity Upgrade', 'Paradox Shop', 'Potion Shop']: continue # near the queen, near potions, and near 7 chests are fine diff --git a/Main.py b/Main.py index efc97d89..ba575006 100644 --- a/Main.py +++ b/Main.py @@ -37,7 +37,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -version_number = '1.2.0.22' +version_number = '1.2.0.23' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 0388d70d..3d6510c6 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -8,7 +8,7 @@ from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitType from OverworldGlitchRules import create_owg_connections from Utils import bidict -version_number = '0.3.4.0' +version_number = '0.3.4.1' # branch indicator is intentionally different across branches version_branch = '' @@ -1044,10 +1044,10 @@ def shuffle_tiles(world, groups, result_list, do_grouped, forced_flips, player): raise GenerationException('Could not find valid tile flips') # tile shuffle happens here - removed = copy.deepcopy(nonflipped_groups) + removed = [] if 0 < undefined_chance < 100: - for group in [g for g in groups if g not in nonflipped_groups]: - if group not in flipped_groups and random.randint(1, 100) > undefined_chance: + for group in groups: + if group[0] in nonflipped_groups or (group[0] not in flipped_groups and random.randint(1, 100) > undefined_chance): removed.append(group) # save shuffled tiles to list @@ -1072,7 +1072,7 @@ def shuffle_tiles(world, groups, result_list, do_grouped, forced_flips, player): attempts -= 1 continue # ensure sanc can be placed in LW in certain modes - if not do_grouped and world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lean', 'swapped', 'crossed', 'insanity'] and world.mode[player] != 'inverted' and (world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3 or world.mode[player] == 'standard'): + if not do_grouped and world.shuffle[player] in ['simple', 'restricted', 'full', 'district'] and world.mode[player] != 'inverted' and (world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3 or world.mode[player] == 'standard'): free_dw_drops = parity[5] + (1 if world.shuffle_ganon[player] else 0) free_drops = 6 + (1 if world.mode[player] != 'standard' else 0) + (1 if world.shuffle_ganon[player] else 0) if free_dw_drops == free_drops: @@ -1124,7 +1124,7 @@ def define_tile_groups(world, do_grouped, player): return False # sanctuary/chapel should not be flipped if S+Q guaranteed to output on that screen - if 0x13 in group and ((world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] \ + if 0x13 in group and ((world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district'] \ and (world.mode[player] in ['standard', 'inverted'] or world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3)) \ or (world.shuffle[player] in ['lite', 'lean'] and world.mode[player] == 'inverted')): return False @@ -1138,24 +1138,31 @@ def define_tile_groups(world, do_grouped, player): groups.append([0x80]) groups.append([0x81]) - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple']: - merge_groups([[0x03, 0x0a], [0x28, 0x29]]) - - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted', 'full', 'lite']: - merge_groups([[0x13, 0x14]]) - - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'simple', 'restricted']: - merge_groups([[0x05, 0x07]]) - - if world.shuffle[player] == 'vanilla' or (world.mode[player] == 'standard' and world.shuffle[player] in ['dungeonssimple', 'dungeonsfull']): + # hyrule castle and sanctuary connector + if world.shuffle[player] in ['vanilla', 'district'] or (world.mode[player] == 'standard' and world.shuffle[player] in ['dungeonssimple', 'dungeonsfull']): merge_groups([[0x13, 0x14, 0x1b]]) + # sanctuary and grave connector + if world.shuffle[player] in ['dungeonssimple', 'dungeonsfull', 'simple', 'restricted', 'full', 'lite']: + merge_groups([[0x13, 0x14]]) + + # cross-screen connector + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'district']: + merge_groups([[0x03, 0x0a], [0x28, 0x29]]) + + # turtle rock connector + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'simple', 'restricted', 'district']: + merge_groups([[0x05, 0x07]]) + + # all non-parallel screens if world.owShuffle[player] == 'vanilla' and (world.owCrossed[player] == 'none' or do_grouped): merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81], [0x1a, 0x1b], [0x28, 0x29], [0x30, 0x3a]]) + # special case: non-parallel keep similar if world.owShuffle[player] == 'parallel' and world.owKeepSimilar[player] and (world.owCrossed[player] == 'none' or do_grouped): merge_groups([[0x28, 0x29]]) + # whirlpool screens if not world.owWhirlpoolShuffle[player] and (world.owCrossed[player] == 'none' or do_grouped): merge_groups([[0x0f, 0x35], [0x12, 0x15, 0x33, 0x3f]]) @@ -1562,7 +1569,7 @@ def validate_layout(world, player): for dest_region in entrance_connectors[region_name]: if dest_region not in explored_regions: explore_region(dest_region) - if world.shuffle[player] not in ['insanity'] and region_name in sane_connectors: + if world.shuffle[player] not in ['district', 'insanity'] and region_name in sane_connectors: for dest_region in sane_connectors[region_name]: if dest_region not in explored_regions: explore_region(dest_region) @@ -1619,11 +1626,13 @@ def validate_layout(world, player): break # check if entrances in region could be used to access region if world.shuffle[player] != 'vanilla': + # TODO: For District ER, we need to check if there is a dropdown or connector that is able to connect for entrance in [e for e in unreachable_regions[region_name].exits if e.spot_type == 'Entrance']: - if (entrance.name == 'Links House' and (world.mode[player] == 'inverted' or not world.shufflelinks[player] or world.shuffle[player] in ['dungeonssimple', 'dungeonsfull', 'lite', 'lean'])) \ - or (entrance.name == 'Big Bomb Shop' and (world.mode[player] != 'inverted' or not world.shufflelinks[player] or world.shuffle[player] in ['dungeonssimple', 'dungeonsfull', 'lite', 'lean'])) \ - or (entrance.name == 'Ganons Tower' and (world.mode[player] != 'inverted' and not world.shuffle_ganon[player])) \ - or (entrance.name in ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] and world.shuffle[player] not in ['insanity']) \ + if (entrance.name == 'Links House' and ((not world.is_bombshop_start(player) and not world.shufflelinks[player]) or world.shuffle[player] in ['dungeonssimple', 'dungeonsfull', 'lite', 'lean'])) \ + or (entrance.name == 'Big Bomb Shop' and ((world.is_bombshop_start(player) and not world.shufflelinks[player]) or world.shuffle[player] in ['dungeonssimple', 'dungeonsfull', 'lite', 'lean'])) \ + or (entrance.name == 'Ganons Tower' and (not world.is_atgt_swapped(player) and not world.shuffle_ganon[player])) \ + or (entrance.name == 'Agahnims Tower' and (world.is_atgt_swapped(player) and not world.shuffle_ganon[player])) \ + or (entrance.name in ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] and world.shuffle[player] not in ['district', 'insanity']) \ or (entrance.name == 'Tavern North' and not world.shuffletavern[player]): continue # these are fixed entrances and cannot be used for gaining access to region if entrance.name not in drop_entrances \ diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9cbf5b6b..cad049a6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -111,9 +111,6 @@ These are now independent of retro mode and have three options: None, Random, an * 1.4.0.1v * Key logic: Vanilla key logic fixes. Statically set some HC logic and PoD front door -* 1.4.0.0v - * Generation: fix for bunny walk logic taking up too much memory - * Key Logic: Partial is now the new default * 1.3.0.9v * Ganonhunt: playthrough no longer collects crystals * Vanilla Fill: Uncle weapon is always a sword, medallions for Mire/TR will be vanilla @@ -123,7 +120,10 @@ These are now independent of retro mode and have three options: None, Random, an * MW Progression Balancing: Change to be percentage based instead of raw count. (80% threshold) * Take anys: Good Bee cave chosen as take any should no longer prevent generation * Money balancing: Fixed generation issue - 1.2.0.22u + 1.2.0.23u + * Generation: fix for bunny walk logic taking up too much memory + * Key Logic: Partial is now the new default +* 1.2.0.22u * Flute can't be activated in rain state (except glitched modes) (Thanks codemann!) * ER: Minor fix for Link's House on DM in Insanity (escape cave should not be re-used) * Logic issues: diff --git a/Rom.py b/Rom.py index 2a5c1c38..d47fa98b 100644 --- a/Rom.py +++ b/Rom.py @@ -799,7 +799,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): owFlags |= 0x0200 - # setting spriteID to D8, a placeholder sprite we use to inform ROM to spawn a dynamic item + # setting spriteID to D9, a placeholder sprite we use to inform ROM to spawn a dynamic item #for address in bonk_addresses: for address in [b for b in bonk_addresses if b != 0x4D0AE]: # temp fix for screen 1A murahdahla sprite replacement rom.write_byte(address, 0xD9) @@ -1549,7 +1549,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # b - Big Key # a - Small Key # - enable_menu_map_check = world.overworld_map[player] != 'default' and world.shuffle[player] != 'none' + enable_menu_map_check = world.overworld_map[player] != 'default' and world.shuffle[player] != 'vanilla' 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) @@ -1667,7 +1667,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # rom.write_byte(snes_to_pc(0x0DB730), 0x08) # allows chickens to travel across water # allow smith into multi-entrance caves in appropriate shuffles - if world.shuffle[player] in ['restricted', 'full', 'lite', 'lean', 'swapped', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): + if world.shuffle[player] in ['restricted', 'full', 'lite', 'lean', 'district', 'swapped', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): rom.write_byte(0x18004C, 0x01) # set correct flag for hera basement item @@ -2142,7 +2142,7 @@ def write_strings(rom, world, player, team): tt.removeUnwantedText() # Let's keep this guy's text accurate to the shuffle setting. - if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple']: + if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple', 'lite', 'lean']: tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.' tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.' @@ -2193,7 +2193,7 @@ def write_strings(rom, world, player, team): # Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones. if world.shuffle[player] not in ['lite', 'lean']: entrances_to_hint.update(InconvenientOtherEntrances) - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lite', 'lean', 'swapped']: + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lite', 'lean', 'district']: hint_count = 0 elif world.shuffle[player] in ['simple', 'restricted']: hint_count = 2 @@ -2227,7 +2227,7 @@ def write_strings(rom, world, player, team): entrances_to_hint.update(OtherEntrances) if world.mode[player] != 'inverted': entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'}) - if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lite', 'lean']: + if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lite', 'lean', 'district']: if world.shufflelinks[player]: entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'}) entrances_to_hint.update({'Links House': 'The hero\'s old residence'}) @@ -2244,7 +2244,7 @@ def write_strings(rom, world, player, team): entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'}) else: entrances_to_hint.update({'Pyramid Entrance': 'The pyramid ledge'}) - hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'swapped'] else 0 + hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 0 hint_count -= 2 if world.shuffle[player] not in ['simple', 'restricted'] else 0 for entrance in all_entrances: if entrance.name in entrances_to_hint: @@ -2263,7 +2263,7 @@ def write_strings(rom, world, player, team): 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', 'swapped'] else 5 + hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 5 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: @@ -2338,7 +2338,7 @@ def write_strings(rom, world, player, team): if world.bigkeyshuffle[player]: items_to_hint.extend(BigKeys) random.shuffle(items_to_hint) - hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'swapped'] else 8 + hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 8 hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0 hint_count += 1 if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player] else 0 while hint_count > 0 and len(items_to_hint) > 0: @@ -2382,11 +2382,11 @@ def write_strings(rom, world, player, team): choices.clear() choices.append(location_item) if hint_type == 'foolish': - if district.dungeons and world.shuffle[player] != 'vanilla': + if district.dungeons and world.shuffle[player] not in ['vanilla', 'district']: choices.extend(district.dungeons) hint_type = 'dungeon_path' elif district.access_points and world.shuffle[player] not in ['vanilla', 'dungeonssimple', - 'dungeonsfull']: + 'dungeonsfull', 'district']: choices.extend([x.hint_text for x in district.access_points]) hint_type = 'connector' if hint_type == 'foolish': @@ -2732,10 +2732,6 @@ def set_inverted_mode(world, player, rom, inverted_buffer): write_int16s(rom, snes_to_pc(0x1BB810), [0x00BE, 0x00C0, 0x013E]) # update pyramid hole entrance write_int16s(rom, snes_to_pc(0x1BB836), [0x001B, 0x001B, 0x001B]) - write_int16(rom, snes_to_pc(0x308300), 0x0140) # add extra pyramid hole - write_int16(rom, snes_to_pc(0x308320), 0x001B) - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: - rom.write_byte(snes_to_pc(0x308340), 0x7B) rom.write_byte(snes_to_pc(0x00DB9D), 0x1A) # make retreat bat gfx available in HC area rom.write_byte(snes_to_pc(0x00DC09), 0x1A) diff --git a/Rules.py b/Rules.py index ca69aa3b..10a6860b 100644 --- a/Rules.py +++ b/Rules.py @@ -1067,7 +1067,7 @@ def ow_inverted_rules(world, player): else: set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has_beam_sword(player)) # barrier gets removed after killing agahnim, rule for that added later set_rule(world.get_entrance('GT Approach', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) - set_rule(world.get_entrance('GT Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'lite', 'lean', 'swapped', 'crossed', 'insanity')) + set_rule(world.get_entrance('GT Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'lite', 'lean', 'district', 'swapped', 'crossed', 'insanity')) if world.is_tile_swapped(0x03, player): set_rule(world.get_entrance('Spectacle Rock Approach', player), lambda state: world.logic[player] in ['noglitches', 'minorglitches'] and state.has_Pearl(player)) @@ -1742,16 +1742,19 @@ def set_bunny_rules(world, player, inverted): # for each such entrance a new option is added that consist of: # a) being able to reach it, and # b) being able to access all entrances from there to `region` - queue = deque([(region, [], {region})]) + queue = deque([(region, [], {region}, [region])]) seen_sets = set([frozenset({region})]) while queue: - (current, path, seen) = queue.popleft() + (current, path, seen, region_path) = queue.popleft() for entrance in current.entrances: + if entrance.door and entrance.door.blocked: + continue new_region = entrance.parent_region new_seen = seen.union({new_region}) if new_region.type in (RegionType.Cave, RegionType.Dungeon) and new_seen in seen_sets: continue new_path = path + [entrance.access_rule] + new_region_path = region_path + [new_region] seen_sets.add(frozenset(new_seen)) if not is_link(new_region): if world.logic[player] in ['owglitches', 'hybridglitches']: @@ -1796,7 +1799,7 @@ def set_bunny_rules(world, player, inverted): continue if is_bunny(new_region): # todo: if not owg or hmg and entrance is in bunny_impassible_doors, then skip this nonsense? - queue.append((new_region, new_path, new_seen)) + queue.append((new_region, new_path, new_seen, new_region_path)) else: # we have reached pure light world, so we have a new possible option possible_options.append(path_to_access_rule(new_path, entrance)) @@ -1838,10 +1841,11 @@ def set_bunny_rules(world, player, inverted): continue add_rule(location, get_rule_to_add(region, location)) - for ent_name in bunny_pocket_entrances: - bunny_exit = world.get_entrance(ent_name, player) - if bunny_exit.connected_region and is_bunny(bunny_exit.parent_region) and not can_bunny_pocket_to(world, ent_name, player): - add_rule(bunny_exit, lambda state: state.has_Pearl(player)) + if world.logic[player] in ['owglitches', 'hybridglitches']: + for ent_name in bunny_pocket_entrances: + bunny_exit = world.get_entrance(ent_name, player) + if bunny_exit.connected_region and is_bunny(bunny_exit.parent_region) and not can_bunny_pocket_to(world, ent_name, player): + add_rule(bunny_exit, lambda state: state.has_Pearl(player)) drop_dungeon_entrances = { diff --git a/TestSuite.py b/TestSuite.py index 9c2f29d0..a1b18ab3 100644 --- a/TestSuite.py +++ b/TestSuite.py @@ -51,6 +51,7 @@ def main(args=None): test("Full ", "--shuffle full") test("Lite ", "--shuffle lite") test("Lean ", "--shuffle lean") + test("District ", "--shuffle district") test("Swapped ", "--shuffle swapped") test("Crossed ", "--shuffle crossed") test("Insanity ", "--shuffle insanity") diff --git a/TestSuiteStat.py b/TestSuiteStat.py index 882f9ffb..574e3fb4 100644 --- a/TestSuiteStat.py +++ b/TestSuiteStat.py @@ -14,7 +14,7 @@ ALL_SETTINGS = { 'mode': ['open', 'standard', 'inverted'], 'goal': ['ganon', 'pedestal', 'triforcehunt', 'trinity', 'crystals', 'dungeons'], 'swords': ['random', 'swordless', 'assured'], - 'shuffle': ['vanilla','simple','restricted','full','dungeonssimple','dungeonsfull','lite','lean','swapped','crossed','insanity'], + 'shuffle': ['vanilla','simple','restricted','full','dungeonssimple','dungeonsfull','lite','lean','district','swapped','crossed','insanity'], 'shufflelinks': [True, False], 'shuffleganon': [True, False], 'door_shuffle': ['vanilla', 'basic', 'crossed'], @@ -39,7 +39,7 @@ SETTINGS = { 'goal': ['ganon'], 'swords': ['random'], 'shuffle': ['vanilla', - 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted', 'full', 'lite', 'lean', 'swapped', 'crossed', 'insanity' + 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted', 'full', 'lite', 'lean', 'district', 'swapped', 'crossed', 'insanity' ], 'shufflelinks': [True, False], 'shuffleganon': [True, False], diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 09e897f7..ebe1181b 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -209,6 +209,7 @@ "full", "lite", "lean", + "district", "swapped", "crossed", "insanity", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 3ad7f887..db988393 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -212,6 +212,7 @@ " item locations are shuffled in separate pools. Non-item", " locations remain vanilla. Connectors are same-world.", "Lean: Same as Lite, except connectors can travel cross worlds.", + "District: Entrances are shuffled within their overworld districts.", "Crossed: Mix cave and dungeon entrances freely while allowing", " caves to cross between worlds.", "Swapped: Same as Crossed, but entrances switch places in pairs.", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index e23c02cd..02bb4490 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -181,6 +181,7 @@ "randomizer.entrance.entranceshuffle.restricted": "Restricted", "randomizer.entrance.entranceshuffle.full": "Full", "randomizer.entrance.entranceshuffle.lean": "Lean", + "randomizer.entrance.entranceshuffle.district": "District", "randomizer.entrance.entranceshuffle.swapped": "Swapped", "randomizer.entrance.entranceshuffle.crossed": "Crossed", "randomizer.entrance.entranceshuffle.insanity": "Insanity", diff --git a/resources/app/gui/randomize/entrando/widgets.json b/resources/app/gui/randomize/entrando/widgets.json index c325ea26..809dbac5 100644 --- a/resources/app/gui/randomize/entrando/widgets.json +++ b/resources/app/gui/randomize/entrando/widgets.json @@ -9,6 +9,7 @@ "full", "lite", "lean", + "district", "swapped", "crossed", "insanity", diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 1bdd8950..e30e7141 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -129,11 +129,11 @@ def create_item_pool_config(world): groups = LocationGroup('Major').locs(init_set) if world.bigkeyshuffle[player]: groups.locations.extend(mode_grouping['Big Keys']) - if world.dropshuffle[player] != 'none': + if world.dropshuffle[player]: groups.locations.extend(mode_grouping['Big Key Drops']) if world.keyshuffle[player] != 'none': groups.locations.extend(mode_grouping['Small Keys']) - if world.dropshuffle[player] != 'none': + if world.dropshuffle[player]: groups.locations.extend(mode_grouping['Key Drops']) if world.pottery[player] not in ['none', 'cave']: groups.locations.extend(mode_grouping['Pot Keys']) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 95a3e5dd..e9c8f8d0 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -13,6 +13,8 @@ class EntrancePool(object): self.inverted = False self.coupled = True self.swapped = False + self.assumed_loose_caves = False + self.keep_drops_together = True self.default_map = {} self.one_way_map = {} self.skull_handled = False @@ -54,6 +56,7 @@ def link_entrances_new(world, player): avail_pool.entrances = set(i_drop_map.keys()).union(i_entrance_map.keys()).union(i_single_ent_map.keys()) avail_pool.exits = set(i_entrance_map.values()).union(i_drop_map.values()).union(i_single_ent_map.values()) avail_pool.inverted = world.mode[player] == 'inverted' + avail_pool.assumed_loose_caves = world.shuffle[player] == 'district' inverted_substitution(avail_pool, avail_pool.entrances, True, True) inverted_substitution(avail_pool, avail_pool.exits, False, True) avail_pool.original_entrances.update(avail_pool.entrances) @@ -93,6 +96,8 @@ def link_entrances_new(world, player): raise RuntimeError(f'Shuffle mode {mode} is not yet supported') mode_cfg = copy.deepcopy(modes[mode]) avail_pool.swapped = mode_cfg['undefined'] == 'swap' + avail_pool.keep_drops_together = mode_cfg['keep_drops_together'] == 'on' if 'keep_drops_together' in mode_cfg else True + avail_pool.coupled = mode_cfg['decoupled'] != 'on' if 'decoupled' in mode_cfg else True if avail_pool.is_standard(): do_standard_connections(avail_pool) pool_list = mode_cfg['pools'] if 'pools' in mode_cfg else {} @@ -106,8 +111,7 @@ def link_entrances_new(world, player): connect_random(holes, targets, avail_pool) elif special_shuffle == 'normal_drops': cross_world = mode_cfg['cross_world'] == 'on' if 'cross_world' in mode_cfg else False - keep_together = mode_cfg['keep_drops_together'] == 'on' if 'keep_drops_together' in mode_cfg else True - do_holes_and_linked_drops(set(avail_pool.entrances), set(avail_pool.exits), avail_pool, cross_world, keep_together) + do_holes_and_linked_drops(set(avail_pool.entrances), set(avail_pool.exits), avail_pool, cross_world) elif special_shuffle == 'fixed_shuffle': do_fixed_shuffle(avail_pool, pool['entrances']) elif special_shuffle == 'same_world': @@ -126,10 +130,18 @@ def link_entrances_new(world, player): do_limited_shuffle_exclude_drops(pool, avail_pool, False) elif special_shuffle == 'vanilla': do_vanilla_connect(pool, avail_pool) + elif special_shuffle == 'district': + drops = [] + world_limiter = LW_Entrances if pool['condition'] == 'lightworld' else DW_Entrances + entrances = [e for e in pool['entrances'] if e in world_limiter] + if 'drops' in pool: + drops = [e for e in pool['drops'] if combine_linked_drop_map[e] in world_limiter] + entrances, exits = find_entrances_and_exits(avail_pool, entrances+drops) + do_main_shuffle(entrances, exits, avail_pool, mode_cfg) elif special_shuffle == 'skull': entrances, exits = find_entrances_and_exits(avail_pool, pool['entrances']) rem_ent = None - if avail_pool.world.shuffle[avail_pool.player] in ['dungeons-simple', 'simple', 'restricted'] \ + if avail_pool.world.shuffle[avail_pool.player] in ['dungeonssimple', 'simple', 'restricted'] \ and not avail_pool.world.is_tile_swapped(0x00, avail_pool.player): rem_ent = random.choice(['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']) entrances.remove(rem_ent) @@ -149,6 +161,8 @@ def link_entrances_new(world, player): do_vanilla_connections(avail_pool) elif undefined_behavior in ['shuffle', 'swap']: do_main_shuffle(set(avail_pool.entrances), set(avail_pool.exits), avail_pool, mode_cfg) + elif undefined_behavior == 'error': + assert len(avail_pool.entrances)+len(avail_pool.exits) == 0, 'Not all entrances were placed in their districts' # afterward @@ -191,10 +205,8 @@ def do_vanilla_connections(avail_pool): def do_main_shuffle(entrances, exits, avail, mode_def): cross_world = mode_def['cross_world'] == 'on' if 'cross_world' in mode_def else False - avail.coupled = mode_def['decoupled'] != 'on' if 'decoupled' in mode_def else True # drops and holes - keep_together = mode_def['keep_drops_together'] == 'on' if 'keep_drops_together' in mode_def else True - do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_together) + do_holes_and_linked_drops(entrances, exits, avail, cross_world) if not avail.coupled: avail.decoupled_entrances.extend(entrances) @@ -310,7 +322,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def): return not avail.is_standard() or x != 'Bonk Fairy (Light)' # old man S&Q cave - if not cross_world: + if not cross_world and not avail.assumed_loose_caves: #TODO: Add Swapped ER support for this # OM Cave entrance in lw/dw if cross_world off if 'Old Man Cave Exit (West)' in rem_exits: @@ -382,7 +394,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def): def do_old_man_cave_exit(entrances, exits, avail, cross_world): if 'Old Man Cave Exit (East)' in exits: from EntranceShuffle import build_accessible_region_list - if not avail.world.is_tile_swapped(0x03, avail.player): + if not avail.world.is_tile_swapped(0x03, avail.player) or avail.world.shuffle[avail.player] == 'district': region_name = 'West Death Mountain (Top)' else: region_name = 'West Dark Death Mountain (Top)' @@ -420,11 +432,14 @@ def do_blacksmith(entrances, exits, avail): if avail.world.logic[avail.player] in ['noglitches', 'minorglitches'] and (avail.world.is_tile_swapped(0x29, avail.player) == avail.inverted): assumed_inventory.append('Titans Mitts') + blacksmith_options = list() if not avail.world.is_bombshop_start(avail.player): - links_region = avail.world.get_entrance('Links House Exit', avail.player).connected_region.name + links_region = avail.world.get_entrance('Links House Exit', avail.player).connected_region else: - links_region = avail.world.get_entrance('Big Bomb Shop Exit', avail.player).connected_region.name - blacksmith_options = list(get_accessible_entrances(links_region, avail, assumed_inventory, False, True, True)) + links_region = avail.world.get_entrance('Big Bomb Shop Exit', avail.player).connected_region + if links_region is not None: + links_region = links_region.name + blacksmith_options = list(get_accessible_entrances(links_region, avail, assumed_inventory, False, True, True)) if avail.inverted: dark_sanc = avail.world.get_entrance('Dark Sanctuary Hint Exit', avail.player).connected_region.name @@ -440,6 +455,9 @@ def do_blacksmith(entrances, exits, avail): blacksmith_options = [e for e in blacksmith_options if e not in Forbidden_Swap_Entrances] blacksmith_options = [x for x in blacksmith_options if x in entrances] + if avail.world.shuffle[avail.player] == 'district' and not len(blacksmith_options): + blacksmith_options = [e for e in entrances if e not in Forbidden_Swap_Entrances or not avail.swapped] + assert len(blacksmith_options), 'No available entrances left to place Blacksmith' blacksmith_choice = random.choice(blacksmith_options) connect_entrance(blacksmith_choice, 'Blacksmiths Hut', avail) @@ -454,11 +472,21 @@ def do_blacksmith(entrances, exits, avail): def do_standard_connections(avail): - connect_two_way('Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', avail) - # cannot move uncle cave - connect_two_way('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', avail) - connect_entrance('Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', avail) + std_exits = ['Hyrule Castle Exit (South)', 'Hyrule Castle Secret Entrance Exit'] + if not avail.keep_drops_together: + random.shuffle(std_exits) connect_two_way('Links House', 'Links House Exit', avail) + connect_entrance('Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', avail) + if avail.coupled: + connect_two_way('Hyrule Castle Entrance (South)', std_exits[0], avail) + # cannot move uncle cave + connect_two_way('Hyrule Castle Secret Entrance Stairs', std_exits[1], avail) + else: + connect_entrance('Hyrule Castle Entrance (South)', std_exits[0], avail) + connect_entrance('Hyrule Castle Secret Entrance Stairs', std_exits[1], avail) + random.shuffle(std_exits) + connect_exit(std_exits[0], 'Hyrule Castle Entrance (South)', avail) + connect_exit(std_exits[1], 'Hyrule Castle Secret Entrance Stairs', avail) def remove_from_list(t_list, removals): @@ -466,7 +494,7 @@ def remove_from_list(t_list, removals): t_list.remove(r) -def do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_together): +def do_holes_and_linked_drops(entrances, exits, avail, cross_world): holes_to_shuffle = [x for x in entrances if x in drop_map] if not avail.world.shuffle_ganon: @@ -483,7 +511,7 @@ def do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_togethe remove_from_list(entrances, ['Pyramid Hole', 'Pyramid Entrance']) remove_from_list(exits, ['Pyramid', 'Pyramid Exit']) - if not keep_together: + if not avail.keep_drops_together: targets = [avail.one_way_map[x] for x in holes_to_shuffle] connect_random(holes_to_shuffle, targets, avail) remove_from_list(entrances, holes_to_shuffle) @@ -567,6 +595,8 @@ def do_dark_sanc(entrances, exits, avail): forbidden.extend(Forbidden_Swap_Entrances) if not avail.world.is_bombshop_start(avail.player): forbidden.append('Links House') + else: + forbidden.append('Big Bomb Shop') if avail.world.owShuffle[avail.player] == 'vanilla': choices = [e for e in avail.world.districts[avail.player]['Northwest Dark World'].entrances if e not in forbidden and e in entrances] else: @@ -602,7 +632,7 @@ def do_links_house(entrances, exits, avail, cross_world): forbidden.append('Mimic Cave') if avail.world.is_bombshop_start(avail.player) and (avail.inverted == avail.world.is_tile_swapped(0x03, avail.player)): forbidden.extend(['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']) - if avail.inverted: + if avail.inverted and avail.world.shuffle[avail.player] != 'district': dark_sanc_region = avail.world.get_entrance('Dark Sanctuary Hint Exit', avail.player).connected_region.name forbidden.extend(get_nearby_entrances(avail, dark_sanc_region)) else: @@ -644,7 +674,7 @@ def do_links_house(entrances, exits, avail, cross_world): sanc_spawn_can_be_dark = (not avail.inverted and avail.world.doorShuffle[avail.player] in ['partitioned', 'crossed'] and avail.world.intensity[avail.player] >= 3) - if cross_world and not sanc_spawn_can_be_dark: + if (cross_world and not sanc_spawn_can_be_dark) or avail.world.shuffle[avail.player] == 'district': possible = [e for e in entrance_pool if e not in forbidden] else: world_list = LW_Entrances if not avail.inverted else DW_Entrances @@ -676,7 +706,7 @@ def do_links_house(entrances, exits, avail, cross_world): return if avail.world.shuffle[avail.player] in ['lite', 'lean']: rem_exits = [e for e in avail.exits if e in Connector_Exit_Set and e not in Dungeon_Exit_Set] - multi_exit_caves = figure_out_connectors(rem_exits) + multi_exit_caves = figure_out_connectors(rem_exits, avail) if cross_world: possible_dm_exits = [e for e in avail.entrances if e not in entrances and e in LH_DM_Connector_List] possible_exits = [e for e in avail.entrances if e not in entrances and e not in dm_spots] @@ -685,7 +715,7 @@ def do_links_house(entrances, exits, avail, cross_world): possible_dm_exits = [e for e in avail.entrances if e not in entrances and e in LH_DM_Connector_List and e in world_list] possible_exits = [e for e in avail.entrances if e not in entrances and e not in dm_spots and e in world_list] else: - multi_exit_caves = figure_out_connectors(exits) + multi_exit_caves = figure_out_connectors(exits, avail) entrance_pool = entrances if avail.coupled else avail.decoupled_entrances if cross_world: possible_dm_exits = [e for e in entrances if e in LH_DM_Connector_List] @@ -832,12 +862,22 @@ def get_accessible_entrances(start_region, avail, assumed_inventory=[], cross_wo return found_entrances -def figure_out_connectors(exits): +def figure_out_connectors(exits, avail): multi_exit_caves = [] - for item in Connector_List: + cave_list = list(Connector_List) + if avail.assumed_loose_caves: + sw_list = ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'] + random.shuffle(sw_list) + cave_list.extend([sw_list]) + cave_list.extend([[entrance_map[e]] for e in linked_drop_map.values() if 'Inverted ' not in e]) + for item in cave_list: if all(x in exits for x in item): remove_from_list(exits, item) multi_exit_caves.append(list(item)) + elif avail.assumed_loose_caves and any(x in exits for x in item): + remaining = [i for i in item if i in exits] + remove_from_list(exits, remaining) + multi_exit_caves.append(list(remaining)) return multi_exit_caves @@ -1015,7 +1055,7 @@ def figure_out_must_exits_same_world(entrances, exits, avail): for x in entrances: lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x) - multi_exit_caves = figure_out_connectors(exits) + multi_exit_caves = figure_out_connectors(exits, avail) must_exit_lw, must_exit_dw = must_exits_helper(avail) must_exit_lw = must_exit_filter(avail, must_exit_lw, lw_entrances) @@ -1025,7 +1065,7 @@ def figure_out_must_exits_same_world(entrances, exits, avail): def figure_out_must_exits_cross_world(entrances, exits, avail): - multi_exit_caves = figure_out_connectors(exits) + multi_exit_caves = figure_out_connectors(exits, avail) must_exit_lw, must_exit_dw = must_exits_helper(avail) must_exit = must_exit_filter(avail, must_exit_lw + must_exit_dw, entrances) @@ -1148,9 +1188,12 @@ def do_fixed_shuffle(avail, entrance_list): new_x = 'Agahnims Tower Exit' elif x == 'Agahnims Tower Exit': new_x = 'Ganons Tower Exit' + if avail.world.is_bombshop_start(avail.player): + if x == 'Links House Exit': + new_x = 'Big Bomb Shop' + elif x == 'Big Bomb Shop': + new_x = 'Links House Exit' lw_exits.add(new_x) - if avail.world.shufflelinks[avail.player] or avail.world.shuffle[avail.player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: - lw_exits.update({'Big Bomb Shop'} if avail.world.is_bombshop_start(avail.player) else {'Links House Exit'}) filtered_choices = {i: opt for i, opt in choices.items() if all(t in lw_exits for t in opt[2])} _, choice = random.choice(list(filtered_choices.items())) else: @@ -1361,7 +1404,8 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): # find multi exit cave candidates = [] for candidate in cave_options: - if not isinstance(candidate, str) and len(candidate) > 1 and (candidate in used_caves + allow_single = avail.assumed_loose_caves or len(candidate) > 1 + if not isinstance(candidate, str) and allow_single and (candidate in used_caves or len(candidate) < len(entrances) - required_entrances): if not avail.swapped or (combine_map[exit] not in candidate and not any(e for e in must_exit if combine_map[e] in candidate)): #maybe someday allow these, but we need to disallow mutual locks in Swapped candidates.append(candidate) @@ -1395,6 +1439,10 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): if entrance in invalid_connections: for exit2 in invalid_connections[entrance]: invalid_connections[exit2] = invalid_connections[exit2].union(invalid_connections[exit]).union(invalid_cave_connections[tuple(cave)]) + elif len(cave) == 1 and avail.assumed_loose_caves: + #TODO: keep track of caves we use for must exits that are unaccounted here + # the other exits of the cave should NOT be used to satisfy must-exit later + pass elif cave[-1] == 'Spectacle Rock Cave Exit': # Spectacle rock only has one exit cave_entrances = [] for cave_exit in rnd_cave[:-1]: @@ -1519,14 +1567,12 @@ def find_entrances_and_exits(avail_pool, entrance_pool): entrances, targets = [], [] inverted_substitution(avail_pool, entrance_pool, True) for item in entrance_pool: - if item == 'Ganons Tower' and not avail_pool.world.shuffle_ganon[avail_pool.player]: - continue if item in avail_pool.entrances: entrances.append(item) - if item in entrance_map and entrance_map[item] in avail_pool.exits: - targets.append(entrance_map[item]) - elif item in single_entrance_map and single_entrance_map[item] in avail_pool.exits: - targets.append(single_entrance_map[item]) + if item in avail_pool.default_map and avail_pool.default_map[item] in avail_pool.exits: + targets.append(avail_pool.default_map[item]) + elif item in avail_pool.one_way_map and avail_pool.one_way_map[item] in avail_pool.exits: + targets.append(avail_pool.one_way_map[item]) return entrances, targets @@ -2064,6 +2110,157 @@ modes = { }, } }, + 'district': { + 'undefined': 'error', + 'keep_drops_together': 'off', + 'cross_world': 'off', + 'pools': { + 'northwest_hyrule': { + 'special': 'district', + 'condition': 'lightworld', + 'drops': ['Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave', 'North Fairy Cave Drop', + + 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (East)', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'], + 'entrances': ['Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Sanctuary', 'North Fairy Cave', + 'Lost Woods Gamble', 'Lumberjack House', 'Old Man Cave (West)', 'Death Mountain Return Cave (West)', + 'Fortune Teller (Light)', 'Bonk Rock Cave', 'Graveyard Cave', 'Kings Grave', + + 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section', 'Dark Lumberjack Shop', + 'Bumper Cave (Bottom)', 'Bumper Cave (Top)', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', + 'Red Shield Shop'] + }, + 'northwest_dark_world': { + 'special': 'district', + 'condition': 'darkworld', + 'drops': ['Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (East)', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole', + + 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave', 'North Fairy Cave Drop', + 'Kakariko Well Drop', 'Bat Cave Drop'], + 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section', 'Dark Lumberjack Shop', + 'Bumper Cave (Bottom)', 'Bumper Cave (Top)', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', + 'Chest Game', 'Thieves Town', 'C-Shaped House', 'Dark World Shop', 'Brewery', + 'Red Shield Shop', 'Hammer Peg Cave', + + 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Sanctuary', 'North Fairy Cave', + 'Kakariko Well Cave', 'Bat Cave Cave', 'Lost Woods Gamble', 'Lumberjack House', 'Fortune Teller (Light)', + 'Old Man Cave (West)', 'Death Mountain Return Cave (West)', 'Bonk Rock Cave', 'Graveyard Cave', + 'Kings Grave', 'Blinds Hideout', 'Elder House (West)', 'Elder House (East)', 'Snitch Lady (West)', + 'Snitch Lady (East)', 'Chicken House', 'Sick Kids House', 'Bush Covered House', 'Light World Bomb Hut', + 'Kakariko Shop', 'Tavern North', 'Tavern (Front)', 'Blacksmiths Hut'] + }, + 'central_hyrule': { + 'special': 'district', + 'condition': 'lightworld', + 'drops': ['Hyrule Castle Secret Entrance Drop', 'Inverted Pyramid Hole', + + 'Pyramid Hole'], + 'entrances': ['Hyrule Castle Secret Entrance Stairs', 'Inverted Pyramid Entrance', 'Agahnims Tower', + 'Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (South)', + 'Bonk Fairy (Light)', 'Links House', 'Cave 45', 'Light Hype Fairy', 'Dam', + + 'Pyramid Entrance', 'Pyramid Fairy', 'Bonk Fairy (Dark)', 'Big Bomb Shop', 'Hype Cave', 'Swamp Palace'] + }, + 'kakariko': { + 'special': 'district', + 'condition': 'lightworld', + 'drops': ['Kakariko Well Drop', 'Bat Cave Drop'], + 'entrances': ['Kakariko Well Cave', 'Bat Cave Cave', 'Blinds Hideout', 'Elder House (West)', 'Elder House (East)', + 'Snitch Lady (West)', 'Snitch Lady (East)', 'Chicken House', 'Sick Kids House', 'Bush Covered House', + 'Light World Bomb Hut', 'Kakariko Shop', 'Tavern North', 'Tavern (Front)', 'Blacksmiths Hut', + 'Two Brothers House (West)', 'Two Brothers House (East)', 'Library', 'Kakariko Gamble Game', + + 'Chest Game', 'Thieves Town', 'C-Shaped House', 'Dark World Shop', 'Brewery', + 'Hammer Peg Cave', 'Archery Game'] + }, + 'eastern_hyrule': { + 'special': 'district', + 'condition': 'lightworld', + 'entrances': ['Waterfall of Wishing', 'Potion Shop', 'Sahasrahlas Hut', 'Eastern Palace', 'Lake Hylia Fairy', + 'Long Fairy Cave', + + 'Dark Potion Shop', 'Palace of Darkness Hint', 'Palace of Darkness', 'Dark Lake Hylia Fairy', + 'East Dark World Hint'] + }, + 'lake_hylia': { + 'special': 'district', + 'condition': 'lightworld', + 'entrances': ['Lake Hylia Fortune Teller', 'Lake Hylia Shop', 'Capacity Upgrade', 'Mini Moldorm Cave', + 'Ice Rod Cave', 'Good Bee Cave', '20 Rupee Cave', + + 'Dark Lake Hylia Shop', 'Ice Palace', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint', + 'Dark Lake Hylia Ledge Spike Cave'] + }, + 'desert': { + 'special': 'district', + 'condition': 'lightworld', + 'entrances': ['Desert Palace Entrance (North)', 'Desert Palace Entrance (West)', 'Desert Palace Entrance (South)', + 'Desert Palace Entrance (East)', 'Checkerboard Cave', 'Aginahs Cave', 'Desert Fairy', '50 Rupee Cave', + + 'Mire Shed', 'Misery Mire', 'Mire Fairy', 'Mire Hint'] + }, + 'death_mountain': { + 'special': 'district', + 'condition': 'lightworld', + 'entrances': ['Tower of Hera', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Spectacle Rock Cave', + 'Death Mountain Return Cave (East)', 'Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', + 'Spiral Cave', 'Spiral Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Fairy Ascension Cave (Bottom)', + 'Mimic Cave', 'Hookshot Fairy', 'Paradox Cave (Top)', 'Paradox Cave (Middle)', 'Paradox Cave (Bottom)', + + 'Ganons Tower', 'Dark Death Mountain Fairy', 'Spike Cave', 'Superbunny Cave (Bottom)', 'Superbunny Cave (Top)', + 'Dark Death Mountain Shop', 'Hookshot Cave', 'Hookshot Cave Back Entrance', + 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)', 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock'] + }, + 'dark_death_mountain': { + 'special': 'district', + 'condition': 'darkworld', + 'entrances': ['Ganons Tower', 'Dark Death Mountain Fairy', 'Spike Cave', 'Superbunny Cave (Bottom)', 'Superbunny Cave (Top)', + 'Dark Death Mountain Shop', 'Hookshot Cave', 'Hookshot Cave Back Entrance', + 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)', 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock', + + 'Tower of Hera', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Spectacle Rock Cave', + 'Death Mountain Return Cave (East)', 'Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', + 'Spiral Cave', 'Spiral Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Fairy Ascension Cave (Bottom)', + 'Mimic Cave', 'Hookshot Fairy', 'Paradox Cave (Top)', 'Paradox Cave (Middle)', 'Paradox Cave (Bottom)'] + }, + 'south_dark_world': { + 'special': 'district', + 'condition': 'darkworld', + 'entrances': ['Archery Game', 'Bonk Fairy (Dark)', 'Big Bomb Shop', 'Hype Cave', 'Dark Lake Hylia Shop', 'Ice Palace', + 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Spike Cave', + 'Swamp Palace', + + 'Two Brothers House (West)', 'Two Brothers House (East)', 'Library', 'Kakariko Gamble Game', + 'Bonk Fairy (Light)', 'Links House', 'Cave 45', 'Desert Fairy', '50 Rupee Cave', 'Dam', + 'Light Hype Fairy', 'Lake Hylia Fortune Teller', 'Lake Hylia Shop', 'Capacity Upgrade', + 'Mini Moldorm Cave', 'Ice Rod Cave', 'Good Bee Cave', '20 Rupee Cave'] + }, + 'east_dark_world': { + 'special': 'district', + 'condition': 'darkworld', + 'drops': ['Pyramid Hole', + + 'Hyrule Castle Secret Entrance Drop', 'Inverted Pyramid Hole'], + 'entrances': ['Pyramid Entrance', 'Pyramid Fairy', 'Dark Potion Shop', 'Palace of Darkness Hint', 'Palace of Darkness', + 'Dark Lake Hylia Fairy', 'East Dark World Hint', + + 'Hyrule Castle Secret Entrance Stairs', 'Inverted Pyramid Entrance', 'Waterfall of Wishing', 'Potion Shop', + 'Agahnims Tower', 'Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', + 'Hyrule Castle Entrance (South)', 'Sahasrahlas Hut', 'Eastern Palace', 'Lake Hylia Fairy', 'Long Fairy Cave'] + }, + 'mire': { + 'special': 'district', + 'condition': 'darkworld', + 'entrances': ['Mire Shed', 'Misery Mire', 'Mire Fairy', 'Mire Hint', + + 'Desert Palace Entrance (North)', 'Desert Palace Entrance (West)', 'Desert Palace Entrance (South)', + 'Desert Palace Entrance (East)', 'Checkerboard Cave', 'Aginahs Cave'] + } + } + }, 'swapped': { 'undefined': 'swap', 'keep_drops_together': 'on', @@ -2125,7 +2322,7 @@ drop_map = { } linked_drop_map = { - 'Hyrule Castle Secret Entrance Drop': 'Hyrule Castle Secret Entrance Stairs', + 'Hyrule Castle Secret Entrance Drop': 'Hyrule Castle Secret Entrance Stairs', 'Kakariko Well Drop': 'Kakariko Well Cave', 'Bat Cave Drop': 'Bat Cave Cave', 'North Fairy Cave Drop': 'North Fairy Cave', @@ -2136,6 +2333,13 @@ linked_drop_map = { 'Inverted Pyramid Hole': 'Inverted Pyramid Entrance' } +sw_linked_drop_map = { + 'Skull Woods Second Section Hole': 'Skull Woods Second Section Door (West)', + 'Skull Woods First Section Hole (North)': 'Skull Woods First Section Door', + 'Skull Woods First Section Hole (West)': 'Skull Woods First Section Door', + 'Skull Woods First Section Hole (East)': 'Skull Woods First Section Door' +} + entrance_map = { 'Desert Palace Entrance (South)': 'Desert Palace Exit (South)', 'Desert Palace Entrance (West)': 'Desert Palace Exit (West)', @@ -2169,7 +2373,7 @@ entrance_map = { 'Links House': 'Links House Exit', - 'Hyrule Castle Secret Entrance Stairs': 'Hyrule Castle Secret Entrance Exit', + 'Hyrule Castle Secret Entrance Stairs': 'Hyrule Castle Secret Entrance Exit', 'Kakariko Well Cave': 'Kakariko Well Exit', 'Bat Cave Cave': 'Bat Cave Exit', 'North Fairy Cave': 'North Fairy Cave Exit', @@ -2244,6 +2448,7 @@ single_entrance_map = { } combine_map = {**entrance_map, **single_entrance_map, **drop_map} +combine_linked_drop_map = {**linked_drop_map, **sw_linked_drop_map} LW_Entrances = [] DW_Entrances = []