From b93a287b36e5688e8bc28318598b0ba67dfdd057 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 10 Oct 2024 15:16:24 -0600 Subject: [PATCH 01/31] feat: spoiler/playthrough support in meta section of customizer --- source/classes/CustomSettings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 51fa4c2e..37bb0206 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -61,6 +61,8 @@ class CustomSettings(object): args.outputname = get_setting(meta['name'], args.outputname) args.bps = get_setting(meta['bps'], args.bps) args.suppress_rom = get_setting(meta['suppress_rom'], args.suppress_rom) + args.skip_playthrough = get_setting(meta['skip_playthrough'], args.skip_playthrough) + args.spoiler = get_setting(meta['spoiler'], args.spoiler) args.names = get_setting(meta['names'], args.names) args.race = get_setting(meta['race'], args.race) args.notes = get_setting(meta['user_notes'], args.notes) From 7de6671477cd1fba665aa19f009a9eb7bc836a31 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 11 Nov 2024 08:19:31 -0700 Subject: [PATCH 02/31] fix: accessibility bug --- DoorShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 14433143..e22a6822 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -2321,7 +2321,7 @@ def validate_trap_layout(proposal, builder, start_regions, paths, world, player) if bk_special_loc: if not state.found_forced_bk(): return False - if world.accessibility[player] != 'beatable': + if world.accessibility[player] != 'none': 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 From 82a04dabd736e64cf6f29d42ebe24f2e7aab2f07 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 12 Nov 2024 14:07:43 -0700 Subject: [PATCH 03/31] fix: standard+enemizer rules --- Rules.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Rules.py b/Rules.py index 87742ad1..fcd475fa 100644 --- a/Rules.py +++ b/Rules.py @@ -1533,16 +1533,28 @@ def standard_rules(world, player): add_rule(world.get_location('Secret Passage', player), lambda state: standard_escape_rule(state)) escape_builder = world.dungeon_layouts[player]['Hyrule Castle'] + room_map = world.data_tables[player].uw_enemy_table.room_map + stats = world.data_tables[player].enemy_stats for region in escape_builder.master_sector.regions: - for loc in region.locations: - add_rule(loc, lambda state: standard_escape_rule(state)) if region.name in std_kill_rooms: - for ent in std_kill_rooms[region.name][0]: - add_rule(world.get_entrance(ent, player), lambda state: standard_escape_rule(state)) - for ent in std_kill_rooms[region.name][1]: + entrances, trap_ables, room_id, enemy_list = std_kill_rooms[region.name] + rule = get_challenge_rule(world, player, room_map, stats, room_id, enemy_list, region) + for ent in entrances: entrance = world.get_entrance(ent, player) - if entrance.door.trapped: - add_rule(entrance, lambda state: standard_escape_rule(state)) + if not entrance.door or not entrance.door.entranceFlag: + add_rule_new(entrance, rule) + for ent in trap_ables: + entrance = world.get_entrance(ent, player) + if entrance.door.trapped and not entrance.door.entranceFlag: + add_rule_new(entrance, rule) + else: + for loc in region.locations: + if loc.name in kill_chests: + locations, room_id, enemy_list = kill_chests[loc.name] + rule = get_challenge_rule(world, player, room_map, stats, room_id, enemy_list, region) + add_rule_new(world.get_location(loc, player), rule) + else: + add_rule(loc, lambda state: standard_escape_rule(state)) set_rule(world.get_location('Zelda Pickup', player), lambda state: state.has('Big Key (Escape)', player)) set_rule(world.get_entrance('Hyrule Castle Tapestry Backwards', player), lambda state: state.has('Zelda Herself', player)) From a2075ec9925eacd23998a17cf5880286ff3897e9 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 12 Nov 2024 14:10:28 -0700 Subject: [PATCH 04/31] fix: inconsistent treatment of beemizer --- BaseClasses.py | 2 +- CLI.py | 2 +- ItemList.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 0d63917c..6846ac2d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -129,7 +129,7 @@ class World(object): set_player_attr('enemy_health', 'default') set_player_attr('enemy_damage', 'default') set_player_attr('any_enemy_logic', 'allow_all') - set_player_attr('beemizer', 0) + set_player_attr('beemizer', '0') set_player_attr('escape_assist', []) set_player_attr('crystals_needed_for_ganon', 7) set_player_attr('crystals_needed_for_gt', 7) diff --git a/CLI.py b/CLI.py index cae0feb8..af94e810 100644 --- a/CLI.py +++ b/CLI.py @@ -270,7 +270,7 @@ def parse_settings(): "seed": "", "count": 1, "startinventory": "", - "beemizer": 0, + 'beemizer': '0', "remote_items": False, "race": False, "customitemarray": { diff --git a/ItemList.py b/ItemList.py index 1df02b2a..be1f744c 100644 --- a/ItemList.py +++ b/ItemList.py @@ -459,7 +459,7 @@ def generate_itempool(world, player): '3': {'trap': 50, 'bee': 50}, '4': {'trap': 100}} def beemizer(item): - if world.beemizer[item.player] and not item.advancement and not item.priority and not item.type: + if world.beemizer[item.player] != '0' and not item.advancement and not item.priority and not item.type: choice = random.choices(list(beeweights[world.beemizer[item.player]].keys()), weights=list(beeweights[world.beemizer[item.player]].values()))[0] return item if not choice else ItemFactory("Bee Trap", player) if choice == 'trap' else ItemFactory("Bee", player) return item From 718ef718f464aa201475f101c93ca4135973b2ef Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 2 Dec 2024 10:43:07 -0700 Subject: [PATCH 05/31] fix: broken compare --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 6846ac2d..928594f1 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2406,7 +2406,7 @@ class Item(object): return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})' def __eq__(self, other): - return self.name == other.name and self.player == other.player + return other is not None and self.name == other.name and self.player == other.player # have 6 address that need to be filled From 514dfcc0c804c6d4552230e7a7238a98e3519ef9 Mon Sep 17 00:00:00 2001 From: KrisDavie Date: Sat, 12 Oct 2024 16:37:22 +0200 Subject: [PATCH 06/31] Fix world copy in HMG/NL --- Main.py | 6 +++++- UnderworldGlitchRules.py | 41 +++++++++++++++++++++++++--------------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/Main.py b/Main.py index af8ba0ab..bd249668 100644 --- a/Main.py +++ b/Main.py @@ -27,7 +27,7 @@ from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dunge from Fill import dungeon_tracking from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, fill_specific_items -from UnderworldGlitchRules import create_hybridmajor_connections, create_hybridmajor_connectors +from UnderworldGlitchRules import create_hybridmajor_connections, create_hybridmajor_connectors, get_hybridmajor_connector_entrances from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config, verify_item_pool_config @@ -557,6 +557,8 @@ def copy_world(world): # connect copied world copied_locations = {(loc.name, loc.player): loc for loc in ret.get_locations()} # caches all locations + hmg_entrances = get_hybridmajor_connector_entrances() + for region in world.regions: copied_region = ret.get_region(region.name, region.player) copied_region.is_light_world = region.is_light_world @@ -566,6 +568,8 @@ def copy_world(world): for location in copied_region.locations: location.parent_region = copied_region for entrance in region.entrances: + if entrance.name in hmg_entrances: + continue ret.get_entrance(entrance.name, entrance.player).connect(copied_region) # fill locations diff --git a/UnderworldGlitchRules.py b/UnderworldGlitchRules.py index 67593631..4544b2ed 100644 --- a/UnderworldGlitchRules.py +++ b/UnderworldGlitchRules.py @@ -6,12 +6,11 @@ kikiskip_spots = [ ("Kiki Skip", "Spectacle Rock Cave (Bottom)", "Palace of Darkness Portal") ] -mireheraswamp_spots = [ - ("Mire to Hera Clip", "Mire Torches Top", "Hera Portal"), - ("Hera to Swamp Clip", "Mire Torches Top", "Swamp Portal"), -] +mirehera_spots = [("Mire to Hera Clip", "Mire Torches Top", "Hera Portal")] -icepalace_spots = [("Ice Lobby Clip", "Ice Portal", "Ice Bomb Drop")] +heraswamp_spots = [("Hera to Swamp Clip", "Mire Torches Top", "Swamp Portal")] + +icepalace_spots = [("Ice Lobby Clip", "Ice Portal", "Ice Bomb Drop - Top")] thievesdesert_spots = [ ("Thieves to Desert West Clip", "Thieves Attic", "Desert West Portal"), @@ -32,14 +31,12 @@ paradox_spots = [ kikiskip_connectors = [ ("Kiki Skip Connector", "Spectacle Rock Cave (Bottom)", "Palace of Darkness Exit") ] - - -mireheraswamp_connectors = [ - ("Mire to Hera Connector", "Mire Torches Top", "Tower of Hera Exit"), - ("Mire to Swamp Connector", "Mire Torches Top", "Swamp Palace Exit"), +mirehera_connectors = [ + ("Mire to Hera Connector", "Mire Torches Top", "Tower of Hera Exit") +] +heraswamp_connectors = [ + ("Mire to Swamp Connector", "Mire Torches Top", "Swamp Palace Exit") ] - - thievesdesert_connectors = [ ("Thieves to Desert West Connector", "Thieves Attic", "Desert Palace Exit (West)"), ( @@ -49,7 +46,6 @@ thievesdesert_connectors = [ ), ("Thieves to Desert East Connector", "Thieves Attic", "Desert Palace Exit (East)"), ] - specrock_connectors = [ ( "Spec Rock Top Connector", @@ -68,7 +64,8 @@ specrock_connectors = [ def create_hybridmajor_connections(world, player): for spots in [ kikiskip_spots, - mireheraswamp_spots, + mirehera_spots, + heraswamp_spots, icepalace_spots, thievesdesert_spots, specrock_spots, @@ -81,7 +78,8 @@ def create_hybridmajor_connections(world, player): def create_hybridmajor_connectors(world, player): for connectors in [ kikiskip_connectors, - mireheraswamp_connectors, + mirehera_connectors, + heraswamp_connectors, thievesdesert_connectors, specrock_connectors, ]: @@ -96,6 +94,19 @@ def create_hybridmajor_connectors(world, player): create_no_logic_connections(player, world, new_connectors) +def get_hybridmajor_connector_entrances(): + connectors = [] + for connector in ( + kikiskip_connectors + + mirehera_connectors + + heraswamp_connectors + + thievesdesert_connectors + + specrock_connectors + ): + connectors.append(connector[0]) + return set(connectors) + + # For some entrances, we need to fake having pearl, because we're in fake DW/LW. # This creates a copy of the input state that has Moon Pearl. def fake_pearl_state(state, player): From 5dc51d4a2cab0e03858c59077d9bfec3c62f8ad5 Mon Sep 17 00:00:00 2001 From: KrisDavie Date: Sat, 12 Oct 2024 16:38:27 +0200 Subject: [PATCH 07/31] Actually check if we can leave IP bomb drop --- UnderworldGlitchRules.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/UnderworldGlitchRules.py b/UnderworldGlitchRules.py index 4544b2ed..a03797b0 100644 --- a/UnderworldGlitchRules.py +++ b/UnderworldGlitchRules.py @@ -1,6 +1,8 @@ -from BaseClasses import Entrance +from BaseClasses import Entrance, DoorType +from DoorShuffle import connect_simple_door import Rules from OverworldGlitchRules import create_no_logic_connections +from Doors import create_door kikiskip_spots = [ ("Kiki Skip", "Spectacle Rock Cave (Bottom)", "Palace of Darkness Portal") @@ -73,6 +75,17 @@ def create_hybridmajor_connections(world, player): ]: create_no_logic_connections(player, world, spots) + # Add the new Ice path (back of bomb drop to front) to the world and model it properly + clip_door = create_door(player, "Ice Bomb Drop Clip", DoorType.Logical) + world.doors += [clip_door] + world.initialize_doors([clip_door]) + + ice_bomb_top_reg = world.get_region("Ice Bomb Drop - Top", player) + ice_bomb_top_reg.exits.append( + Entrance(player, "Ice Bomb Drop Clip", ice_bomb_top_reg) + ) + connect_simple_door(world, "Ice Bomb Drop Clip", "Ice Bomb Drop", player) + # Turn dungeons into connectors def create_hybridmajor_connectors(world, player): From 81a8b3a0f555ac890e0ab6c0c694f6f0b4de2fad Mon Sep 17 00:00:00 2001 From: KrisDavie Date: Sat, 12 Oct 2024 16:42:14 +0200 Subject: [PATCH 08/31] HMG PoD Region fix + reformatting * Create a region in HMG when PoD entrance isn't pre-opened that limits access to the overworld unless already available * Change rule generations to loop * Check for bomb and dash clips properly --- OverworldShuffle.py | 3 + Regions.py | 15 +++- UnderworldGlitchRules.py | 164 +++++++++++++++++++++++++++------------ 3 files changed, 130 insertions(+), 52 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 303aeed2..768588d3 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -15,6 +15,9 @@ def link_overworld(world, player): for exitname, regionname in inverted_mandatory_connections: connect_simple(world, exitname, regionname, player) + if world.logic[player] in ['hybridglitches', 'nologic'] and not world.fix_palaceofdarkness_exit[player]: + connect_two_way(world, 'Palace of Darkness HMG Exit', 'Palace of Darkness HMG Entrance', player) + for forward_edge, back_edge in default_connections: connect_two_way(world, forward_edge, back_edge, player) diff --git a/Regions.py b/Regions.py index 6cb44847..e82dd74b 100644 --- a/Regions.py +++ b/Regions.py @@ -191,8 +191,19 @@ def create_regions(world, player): create_dw_region(player, 'Broken Bridge Area', None, ['Broken Bridge Hammer Rock (South)', 'Broken Bridge Water Drop', 'Broken Bridge SW']), create_dw_region(player, 'Broken Bridge Northeast', None, ['Broken Bridge Hammer Rock (North)', 'Broken Bridge Hookshot Gap', 'Broken Bridge Northeast Water Drop', 'Broken Bridge NE']), create_dw_region(player, 'Broken Bridge West', None, ['Broken Bridge West Water Drop', 'Broken Bridge NW']), - create_dw_region(player, 'Broken Bridge Water', None, ['Broken Bridge NC'], 'Dark World', Terrain.Water), - create_dw_region(player, 'Palace of Darkness Area', None, ['Palace of Darkness Hint', 'Palace of Darkness', 'Palace of Darkness SW', 'Palace of Darkness SE']), + create_dw_region(player, 'Broken Bridge Water', None, ['Broken Bridge NC'], 'Dark World', Terrain.Water) + ] + if world.logic[player] in ['hybridglitches', 'nologic'] and not world.fix_palaceofdarkness_exit[player]: + world.regions += [ + create_dw_region(player, 'Palace of Darkness HMG Area', None, ['Palace of Darkness', 'Palace of Darkness HMG Exit']), + create_dw_region(player, 'Palace of Darkness Area', None, ['Palace of Darkness Hint', 'Palace of Darkness SW', 'Palace of Darkness SE', 'Palace of Darkness HMG Entrance']) + ] + else: + world.regions += [ + create_dw_region(player, 'Palace of Darkness Area', None, ['Palace of Darkness Hint', 'Palace of Darkness', 'Palace of Darkness SW', 'Palace of Darkness SE']) + ] + + world.regions += [ create_dw_region(player, 'Hammer Pegs Area', ['Dark Blacksmith Ruins'], ['Hammer Peg Cave', 'Peg Area Rocks (East)']), create_dw_region(player, 'Hammer Pegs Entry', None, ['Peg Area Rocks (West)', 'Hammer Pegs WS']), create_dw_region(player, 'Dark Dunes Area', None, ['Dark Dunes NW', 'Dark Dunes WN', 'Dark Dunes SC']), diff --git a/UnderworldGlitchRules.py b/UnderworldGlitchRules.py index a03797b0..0f4389df 100644 --- a/UnderworldGlitchRules.py +++ b/UnderworldGlitchRules.py @@ -1,3 +1,4 @@ +import functools from BaseClasses import Entrance, DoorType from DoorShuffle import connect_simple_door import Rules @@ -133,19 +134,45 @@ def fake_pearl_state(state, player): # Sets the rules on where we can actually go using this clip. # Behavior differs based on what type of ER shuffle we're playing. def dungeon_reentry_rules( - world, player, clip: Entrance, dungeon_region: str, dungeon_exit: str + world, + player, + clip: Entrance, + dungeon_region: str, ): fix_dungeon_exits = world.fix_palaceofdarkness_exit[player] fix_fake_worlds = world.fix_fake_world[player] + all_clips = [ + x[0] + for x in kikiskip_spots + + mirehera_spots + + heraswamp_spots + + icepalace_spots + + thievesdesert_spots + + specrock_spots + + paradox_spots + + kikiskip_connectors + + mirehera_connectors + + heraswamp_connectors + + thievesdesert_connectors + + specrock_connectors + ] + dungeon_entrance = [ r for r in world.get_region(dungeon_region, player).entrances - if r.name != clip.name + if r.name not in all_clips ][0] - if ( - not fix_dungeon_exits - ): # vanilla, simple, restricted, dungeonssimple; should never have fake worlds fix + + dungeon_exit = [ + r + for r in world.get_region(dungeon_region, player).exits + if r.name not in all_clips + ][0] + + + if not fix_dungeon_exits: + # vanilla, simple, restricted, dungeonssimple; should never have fake worlds fix # Dungeons are only shuffled among themselves. We need to check SW, MM, and AT because they can't be reentered trivially. # entrance doesn't exist until you fire rod it from the other side @@ -196,32 +223,85 @@ def dungeon_reentry_rules( def underworld_glitches_rules(world, player): - # Ice Palace Entrance Clip, needs bombs or cane of somaria to exit bomb drop room - Rules.add_rule( - world.get_entrance("Ice Bomb Drop SE", player), - lambda state: state.can_dash_clip(world.get_region("Ice Lobby", player), player) - and (state.can_use_bombs(player) or state.has("Cane of Somaria", player)), - combine="or", - ) - - # Kiki Skip - kks = world.get_entrance("Kiki Skip", player) - Rules.set_rule(kks, lambda state: state.can_bomb_clip(kks.parent_region, player)) - dungeon_reentry_rules( - world, player, kks, "Palace of Darkness Portal", "Palace of Darkness Exit" - ) - - # Mire -> Hera -> Swamp def mire_clip(state): - return state.can_reach( - "Mire Torches Top", "Region", player - ) and state.can_dash_clip(world.get_region("Mire Torches Top", player), player) - - def hera_clip(state): - return state.can_reach("Hera 4F", "Region", player) and state.can_dash_clip( - world.get_region("Hera 4F", player), player + torches = world.get_region("Mire Torches Top", player) + return state.can_dash_clip(torches, player) or ( + state.can_bomb_clip(torches, player) and state.has_fire_source(player) ) + def hera_clip(state): + hera = world.get_region("Hera 4F", player) + return state.can_bomb_clip(hera, player) or state.can_dash_clip(hera, player) + + # We use these plus functool.partial because lambdas don't work in loops properly. + def bomb_clip(state, region, player): + return state.can_bomb_clip(region, player) + + def dash_clip(state, region, player): + return state.can_dash_clip(region, player) + + if not world.fix_palaceofdarkness_exit[player]: + Rules.set_rule( + world.get_entrance("Palace of Darkness HMG Exit", player), + lambda state: state.can_reach(world.get_entrance("Palace of Darkness", player), player), + ) + + + # Bomb clips + for clip in ( + kikiskip_spots + + icepalace_spots + + thievesdesert_spots + + specrock_spots + + kikiskip_connectors + + thievesdesert_connectors + + specrock_connectors + ): + region = world.get_region(clip[1], player) + Rules.set_rule( + world.get_entrance(clip[0], player), + functools.partial(bomb_clip, region=region, player=player), + ) + # Dash clips + for clip in icepalace_spots: + region = world.get_region(clip[1], player) + Rules.add_rule( + world.get_entrance(clip[0], player), + functools.partial(dash_clip, region=region, player=player), + combine="or", + ) + + for spot in kikiskip_spots + thievesdesert_spots: + dungeon_reentry_rules( + world, + player, + world.get_entrance(spot[0], player), + spot[2], + ) + + for connector in kikiskip_connectors + thievesdesert_connectors: + dungeon_reentry_rules( + world, + player, + world.get_entrance(connector[0], player), + world.get_entrance(connector[2], player).parent_region.name, + ) + + for clip in mirehera_spots + mirehera_connectors: + Rules.set_rule( + world.get_entrance(clip[0], player), + lambda state: mire_clip(state), + ) + + # Need to be able to escape by hitting the switch from the back + Rules.set_rule( + world.get_entrance("Ice Bomb Drop Clip", player), + lambda state: ( + state.can_use_bombs(player) or state.has("Cane of Somaria", player) + ), + ) + + # Allow mire big key to be used in Hera Rules.add_rule( world.get_entrance("Hera Startile Corner NW", player), lambda state: mire_clip(state) and state.has("Big Key (Misery Mire)", player), @@ -232,20 +312,10 @@ def underworld_glitches_rules(world, player): lambda state: mire_clip(state) and state.has("Big Key (Misery Mire)", player), combine="or", ) - - mire_to_hera = world.get_entrance("Mire to Hera Clip", player) - mire_to_swamp = world.get_entrance("Hera to Swamp Clip", player) - Rules.set_rule(mire_to_hera, mire_clip) + # This uses the mire clip because it's always expected to come from mire Rules.set_rule( - mire_to_swamp, lambda state: mire_clip(state) and state.has("Flippers", player) - ) - - # Using the entrances for various ER types. Hera -> Swamp never matters because you can only logically traverse with the mire keys - dungeon_reentry_rules( - world, player, mire_to_hera, "Hera Lobby", "Tower of Hera Exit" - ) - dungeon_reentry_rules( - world, player, mire_to_swamp, "Swamp Lobby", "Swamp Palace Exit" + world.get_entrance("Hera to Swamp Clip", player), + lambda state: mire_clip(state) and state.has("Flippers", player), ) # We need to set _all_ swamp doors to be openable with mire keys, otherwise the small key can't be behind them - 6 keys because of Pots # Flippers required for all of these doors to prevent locks when flooding @@ -269,7 +339,6 @@ def underworld_glitches_rules(world, player): and state.has("Flippers", player), combine="or", ) - # Rules.add_rule(world.get_entrance(door, player), lambda state: mire_clip(state) and state.has('Flippers', player), combine="or") Rules.add_rule( world.get_location("Trench 1 Switch", player), @@ -322,21 +391,16 @@ def underworld_glitches_rules(world, player): lambda state: mirrorless_moat_rule(state), combine="or", ) + desert_exits = ["West", "South", "East"] - for desert_exit in ["East", "South", "West"]: + for desert_exit in desert_exits: Rules.add_rule( world.get_entrance(f"Thieves to Desert {desert_exit} Connector", player), lambda state: state.can_dash_clip( world.get_region("Thieves Attic", player), player ), ) - dungeon_reentry_rules( - world, - player, - world.get_entrance(f"Thieves to Desert {desert_exit} Connector", player), - f"Desert {desert_exit} Portal", - f"Desert Palace Exit ({desert_exit})", - ) + # Collecting left chests in Paradox Cave using a dash clip -> dash citrus, 1f right, teleport up paradox_left_chests = [ From 8aba0f66fc74708edcea707534ab74a24dc32d37 Mon Sep 17 00:00:00 2001 From: KrisDavie Date: Sat, 12 Oct 2024 16:43:09 +0200 Subject: [PATCH 09/31] Tests update --- test/suite/hmg/flippers_wraps.yaml | 2 - .../hmg/inverted_inaccessible_desert.yaml | 1 - test/suite/hmg/inverted_moon_pearl_locs.yaml | 3 ++ test/suite/hmg/moon_pearl_locs.yaml | 4 +- test/suite/hmg/no_east_dw_from_kikiskip.yaml | 49 +++++++++++++++++++ test/suite/hmg/pearlless_sw.yaml | 8 +++ 6 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 test/suite/hmg/no_east_dw_from_kikiskip.yaml diff --git a/test/suite/hmg/flippers_wraps.yaml b/test/suite/hmg/flippers_wraps.yaml index 3418b4ec..9971a38e 100644 --- a/test/suite/hmg/flippers_wraps.yaml +++ b/test/suite/hmg/flippers_wraps.yaml @@ -19,8 +19,6 @@ advanced_placements: locations: Link's House: True Magic Bat: False -advanced_placements: - 1: - type: Verification item: Progressive Glove locations: diff --git a/test/suite/hmg/inverted_inaccessible_desert.yaml b/test/suite/hmg/inverted_inaccessible_desert.yaml index bb8ad8aa..5e59ca63 100644 --- a/test/suite/hmg/inverted_inaccessible_desert.yaml +++ b/test/suite/hmg/inverted_inaccessible_desert.yaml @@ -5,7 +5,6 @@ settings: logic: hybridglitches mode: inverted shuffle: crossed - start_inventory: 1: - Pegasus Boots diff --git a/test/suite/hmg/inverted_moon_pearl_locs.yaml b/test/suite/hmg/inverted_moon_pearl_locs.yaml index 9ae1a9e1..23ec40fe 100644 --- a/test/suite/hmg/inverted_moon_pearl_locs.yaml +++ b/test/suite/hmg/inverted_moon_pearl_locs.yaml @@ -15,6 +15,9 @@ start_inventory: - Ether - Quake - Bombos + - Big Key (Tower of Hera) + - Big Key (Desert Palace) + - Big Key (Eastern Palace) advanced_placements: 1: - type: Verification diff --git a/test/suite/hmg/moon_pearl_locs.yaml b/test/suite/hmg/moon_pearl_locs.yaml index a18d53d1..13a37f92 100644 --- a/test/suite/hmg/moon_pearl_locs.yaml +++ b/test/suite/hmg/moon_pearl_locs.yaml @@ -3,6 +3,7 @@ meta: settings: 1: logic: hybridglitches + shuffle: vanilla start_inventory: 1: - Pegasus Boots @@ -10,9 +11,8 @@ start_inventory: - Fire Rod - Book of Mudora - Progressive Sword - - Progressive Sword - Lamp - - Magic Mirror + - Hammer - Ether - Quake - Bombos diff --git a/test/suite/hmg/no_east_dw_from_kikiskip.yaml b/test/suite/hmg/no_east_dw_from_kikiskip.yaml new file mode 100644 index 00000000..af5ac2ce --- /dev/null +++ b/test/suite/hmg/no_east_dw_from_kikiskip.yaml @@ -0,0 +1,49 @@ +meta: + players: 1 +settings: + 1: + logic: hybridglitches + shuffle: vanilla +start_inventory: + 1: + - Pegasus Boots + - Flippers + - Fire Rod + - Book of Mudora + - Progressive Sword + - Lamp + - Hammer + - Ether + - Quake + - Bombos + - Bombs (10) +placements: + 1: + # Put mirror in PF so we can't DMD + Pyramid Fairy - Right: Magic Mirror + # Lock all swords and cape behind pearl so we can't do aga 1 for pyramid access + Thieves' Town - Big Chest: Progressive Sword + Thieves' Town - Attic: Progressive Sword + Thieves' Town - Blind's Cell: Progressive Sword + Thieves' Town - Map Chest: Cape +advanced_placements: + 1: + - type: Verification + item: Moon Pearl + locations: + Palace of Darkness - Shooter Room: True + Palace of Darkness - The Arena - Bridge: True + Palace of Darkness - Stalfos Basement: True + Palace of Darkness - Big Key Chest: True + Palace of Darkness - The Arena - Ledge: True + Palace of Darkness - Map Chest: True + Palace of Darkness - Compass Chest: True + Palace of Darkness - Dark Basement - Left: True + Palace of Darkness - Dark Basement - Right: True + Palace of Darkness - Dark Maze - Top: True + Palace of Darkness - Dark Maze - Bottom: True + Palace of Darkness - Big Chest: True + Palace of Darkness - Harmless Hellway: True + Palace of Darkness - Boss: True + Pyramid Fairy - Left: False + Pyramid: False diff --git a/test/suite/hmg/pearlless_sw.yaml b/test/suite/hmg/pearlless_sw.yaml index c26ce1d9..3b097028 100644 --- a/test/suite/hmg/pearlless_sw.yaml +++ b/test/suite/hmg/pearlless_sw.yaml @@ -9,6 +9,7 @@ start_inventory: - Flippers - Pegasus Boots - Progressive Sword + - Progressive Sword - Hammer - Progressive Glove - Progressive Glove @@ -17,9 +18,16 @@ start_inventory: - Bottle - Magic Mirror - Lamp + - Beat Agahnim 1 + - Cane of Somaria + - Hookshot + - Quake + - Ether + - Bombos advanced_placements: 1: - type: Verification item: Moon Pearl locations: Skull Woods - Bridge Room: True + Skull Woods - Boss: True From 76834a477a25a3b73e76081e344f916199a63bd6 Mon Sep 17 00:00:00 2001 From: KrisDavie Date: Thu, 12 Dec 2024 20:44:26 +0100 Subject: [PATCH 10/31] Don't ever check playthough when all seeds are no logic --- Main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Main.py b/Main.py index bd249668..4d806e96 100644 --- a/Main.py +++ b/Main.py @@ -426,6 +426,10 @@ def main(args, seed=None, fish=None): if 'debug' in world.spoiler.settings: world.spoiler.extras(output_path(f'{outfilebase}_Spoiler.txt')) + player_logics = set(world.logic.values()) + if len(player_logics) == 1 and 'nologic' in player_logics: + args.skip_playthrough = True + if not args.skip_playthrough: logger.info(world.fish.translate("cli","cli","calc.playthrough")) create_playthrough(world) From 1c33f793e0895149f8c6e9d7cac53fee8e9a999d Mon Sep 17 00:00:00 2001 From: KrisDavie Date: Thu, 12 Dec 2024 20:56:59 +0100 Subject: [PATCH 11/31] SP Bosstest for MP --- test/suite/hmg/moon_pearl_locs.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/suite/hmg/moon_pearl_locs.yaml b/test/suite/hmg/moon_pearl_locs.yaml index 13a37f92..b2d418b1 100644 --- a/test/suite/hmg/moon_pearl_locs.yaml +++ b/test/suite/hmg/moon_pearl_locs.yaml @@ -41,6 +41,7 @@ advanced_placements: C-Shaped House: True Pyramid Fairy - Left: True Swamp Palace - Entrance: False + Swamp Palace - Boss: False Thieves' Town - Map Chest: False From 252a99f0d2c6718e916b72854e1d8b3b30c1a8bd Mon Sep 17 00:00:00 2001 From: KrisDavie Date: Mon, 16 Dec 2024 00:06:54 +0100 Subject: [PATCH 12/31] Revamp HMG logic --- Main.py | 32 +++-- OverworldGlitchRules.py | 14 ++- OverworldShuffle.py | 3 - Regions.py | 15 +-- UnderworldGlitchRules.py | 111 +++--------------- .../hmg/inverted_inaccessible_desert.yaml | 4 + test/suite/hmg/pod_mp.yaml | 13 ++ 7 files changed, 71 insertions(+), 121 deletions(-) create mode 100644 test/suite/hmg/pod_mp.yaml diff --git a/Main.py b/Main.py index 4d806e96..fcb50142 100644 --- a/Main.py +++ b/Main.py @@ -27,7 +27,7 @@ from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dunge from Fill import dungeon_tracking from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, fill_specific_items -from UnderworldGlitchRules import create_hybridmajor_connections, create_hybridmajor_connectors, get_hybridmajor_connector_entrances +from UnderworldGlitchRules import create_hybridmajor_connections, get_hybridmajor_connection_entrances from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config, verify_item_pool_config @@ -222,8 +222,6 @@ def main(args, seed=None, fish=None): world.data_tables[player] = init_data_tables(world, player) place_bosses(world, player) randomize_enemies(world, player) - if world.logic[player] in ('nologic', 'hybridglitches'): - create_hybridmajor_connections(world, player) adjust_locations(world, player) if world.customizer and world.customizer.get_start_inventory(): @@ -264,8 +262,6 @@ def main(args, seed=None, fish=None): link_overworld(world, player) create_dynamic_exits(world, player) link_entrances_new(world, player) - if world.logic[player] in ('nologic', 'hybridglitches'): - create_hybridmajor_connectors(world, player) logger.info(world.fish.translate("cli", "cli", "shuffling.prep")) for player in range(1, world.players + 1): @@ -285,6 +281,8 @@ def main(args, seed=None, fish=None): logger.info(world.fish.translate("cli", "cli", "generating.itempool")) for player in range(1, world.players + 1): + if world.logic[player] in ('nologic', 'hybridglitches'): + create_hybridmajor_connections(world, player) generate_itempool(world, player) verify_item_pool_config(world) @@ -533,8 +531,6 @@ def copy_world(world): create_dungeons(ret, player) if world.logic[player] in ('owglitches', 'hybridglitches', 'nologic'): create_owg_connections(ret, player) - if world.logic[player] in ('nologic', 'hybridglitches'): - create_hybridmajor_connections(ret, player) # there are region references here they must be migrated to preserve integrity @@ -561,7 +557,10 @@ def copy_world(world): # connect copied world copied_locations = {(loc.name, loc.player): loc for loc in ret.get_locations()} # caches all locations - hmg_entrances = get_hybridmajor_connector_entrances() + + # We have to skip these for now. They require both the rest of the entrances _and_ the dungeon portals to be copied first + # We will connect them later + hmg_entrances = get_hybridmajor_connection_entrances() for region in world.regions: copied_region = ret.get_region(region.name, region.player) @@ -621,7 +620,22 @@ def copy_world(world): for player in range(1, world.players + 1): if world.logic[player] in ('nologic', 'hybridglitches'): - create_hybridmajor_connectors(ret, player) + create_hybridmajor_connections(ret, player) + + for region in world.regions: + copied_region = ret.get_region(region.name, region.player) + copied_region.is_light_world = region.is_light_world + copied_region.is_dark_world = region.is_dark_world + copied_region.dungeon = region.dungeon + copied_region.locations = [copied_locations[(location.name, location.player)] for location in region.locations] + for location in copied_region.locations: + location.parent_region = copied_region + for entrance in region.entrances: + if entrance.name not in hmg_entrances: + continue + ret.get_entrance(entrance.name, entrance.player).connect(copied_region) + + for player in range(1, world.players + 1): set_rules(ret, player) return ret diff --git a/OverworldGlitchRules.py b/OverworldGlitchRules.py index 97883dc6..55e9d896 100644 --- a/OverworldGlitchRules.py +++ b/OverworldGlitchRules.py @@ -2,7 +2,7 @@ Helper functions to deliver entrance/exit/region sets to OWG rules. """ -from BaseClasses import Entrance +from BaseClasses import Entrance, Region # Cave regions that superbunny can get through - but only with a sword. sword_required_superbunny_mirror_regions = ["Spiral Cave (Top)"] @@ -283,10 +283,18 @@ def add_alternate_rule(entrance, rule): entrance.access_rule = lambda state: old_rule(state) or rule(state) -def create_no_logic_connections(player, world, connections): +def create_no_logic_connections(player, world, connections, connect_external=False): for entrance, parent_region, target_region, *rule_override in connections: parent = world.get_region(parent_region, player) - target = world.get_region(target_region, player) + + if isinstance(target_region, Region): + target_region = target_region.name + + if connect_external and target_region.endswith(" Portal"): + target = world.get_portal(target_region[:-7], player).find_portal_entrance().parent_region + else: + target = world.get_region(target_region, player) + connection = Entrance(player, entrance, parent) parent.exits.append(connection) connection.connect(target) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 768588d3..303aeed2 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -15,9 +15,6 @@ def link_overworld(world, player): for exitname, regionname in inverted_mandatory_connections: connect_simple(world, exitname, regionname, player) - if world.logic[player] in ['hybridglitches', 'nologic'] and not world.fix_palaceofdarkness_exit[player]: - connect_two_way(world, 'Palace of Darkness HMG Exit', 'Palace of Darkness HMG Entrance', player) - for forward_edge, back_edge in default_connections: connect_two_way(world, forward_edge, back_edge, player) diff --git a/Regions.py b/Regions.py index e82dd74b..6cb44847 100644 --- a/Regions.py +++ b/Regions.py @@ -191,19 +191,8 @@ def create_regions(world, player): create_dw_region(player, 'Broken Bridge Area', None, ['Broken Bridge Hammer Rock (South)', 'Broken Bridge Water Drop', 'Broken Bridge SW']), create_dw_region(player, 'Broken Bridge Northeast', None, ['Broken Bridge Hammer Rock (North)', 'Broken Bridge Hookshot Gap', 'Broken Bridge Northeast Water Drop', 'Broken Bridge NE']), create_dw_region(player, 'Broken Bridge West', None, ['Broken Bridge West Water Drop', 'Broken Bridge NW']), - create_dw_region(player, 'Broken Bridge Water', None, ['Broken Bridge NC'], 'Dark World', Terrain.Water) - ] - if world.logic[player] in ['hybridglitches', 'nologic'] and not world.fix_palaceofdarkness_exit[player]: - world.regions += [ - create_dw_region(player, 'Palace of Darkness HMG Area', None, ['Palace of Darkness', 'Palace of Darkness HMG Exit']), - create_dw_region(player, 'Palace of Darkness Area', None, ['Palace of Darkness Hint', 'Palace of Darkness SW', 'Palace of Darkness SE', 'Palace of Darkness HMG Entrance']) - ] - else: - world.regions += [ - create_dw_region(player, 'Palace of Darkness Area', None, ['Palace of Darkness Hint', 'Palace of Darkness', 'Palace of Darkness SW', 'Palace of Darkness SE']) - ] - - world.regions += [ + create_dw_region(player, 'Broken Bridge Water', None, ['Broken Bridge NC'], 'Dark World', Terrain.Water), + create_dw_region(player, 'Palace of Darkness Area', None, ['Palace of Darkness Hint', 'Palace of Darkness', 'Palace of Darkness SW', 'Palace of Darkness SE']), create_dw_region(player, 'Hammer Pegs Area', ['Dark Blacksmith Ruins'], ['Hammer Peg Cave', 'Peg Area Rocks (East)']), create_dw_region(player, 'Hammer Pegs Entry', None, ['Peg Area Rocks (West)', 'Hammer Pegs WS']), create_dw_region(player, 'Dark Dunes Area', None, ['Dark Dunes NW', 'Dark Dunes WN', 'Dark Dunes SC']), diff --git a/UnderworldGlitchRules.py b/UnderworldGlitchRules.py index 0f4389df..beb95ed9 100644 --- a/UnderworldGlitchRules.py +++ b/UnderworldGlitchRules.py @@ -29,42 +29,10 @@ paradox_spots = [ ("Paradox Front Teleport", "Paradox Cave Front", "Paradox Cave Chest Area") ] - -# We need to make connectors at a separate time from the connections, because of how dungeons are linked to regions -kikiskip_connectors = [ - ("Kiki Skip Connector", "Spectacle Rock Cave (Bottom)", "Palace of Darkness Exit") -] -mirehera_connectors = [ - ("Mire to Hera Connector", "Mire Torches Top", "Tower of Hera Exit") -] -heraswamp_connectors = [ - ("Mire to Swamp Connector", "Mire Torches Top", "Swamp Palace Exit") -] -thievesdesert_connectors = [ - ("Thieves to Desert West Connector", "Thieves Attic", "Desert Palace Exit (West)"), - ( - "Thieves to Desert South Connector", - "Thieves Attic", - "Desert Palace Exit (South)", - ), - ("Thieves to Desert East Connector", "Thieves Attic", "Desert Palace Exit (East)"), -] -specrock_connectors = [ - ( - "Spec Rock Top Connector", - "Spectacle Rock Cave (Peak)", - "Spectacle Rock Cave Exit (Top)", - ), - ( - "Spec Rock Exit Connector", - "Spectacle Rock Cave (Peak)", - "Spectacle Rock Cave Exit", - ), -] - - # Create connections between dungeons/locations def create_hybridmajor_connections(world, player): + fix_fake_worlds = world.fix_fake_world[player] + for spots in [ kikiskip_spots, mirehera_spots, @@ -74,7 +42,7 @@ def create_hybridmajor_connections(world, player): specrock_spots, paradox_spots, ]: - create_no_logic_connections(player, world, spots) + create_no_logic_connections(player, world, spots, connect_external=fix_fake_worlds) # Add the new Ice path (back of bomb drop to front) to the world and model it properly clip_door = create_door(player, "Ice Bomb Drop Clip", DoorType.Logical) @@ -87,39 +55,20 @@ def create_hybridmajor_connections(world, player): ) connect_simple_door(world, "Ice Bomb Drop Clip", "Ice Bomb Drop", player) - -# Turn dungeons into connectors -def create_hybridmajor_connectors(world, player): - for connectors in [ - kikiskip_connectors, - mirehera_connectors, - heraswamp_connectors, - thievesdesert_connectors, - specrock_connectors, - ]: - new_connectors = [ - ( - connector[0], - connector[1], - world.get_entrance(connector[2], player).connected_region, - ) - for connector in connectors - ] - create_no_logic_connections(player, world, new_connectors) - - -def get_hybridmajor_connector_entrances(): - connectors = [] +def get_hybridmajor_connection_entrances(): + connections = [] for connector in ( - kikiskip_connectors - + mirehera_connectors - + heraswamp_connectors - + thievesdesert_connectors - + specrock_connectors - ): - connectors.append(connector[0]) - return set(connectors) - + kikiskip_spots + + mirehera_spots + + heraswamp_spots + + icepalace_spots + + thievesdesert_spots + + specrock_spots + + paradox_spots + ): + connections.append(connector[0]) + connections.append('Ice Bomb Drop Clip') + return set(connections) # For some entrances, we need to fake having pearl, because we're in fake DW/LW. # This creates a copy of the input state that has Moon Pearl. @@ -151,11 +100,6 @@ def dungeon_reentry_rules( + thievesdesert_spots + specrock_spots + paradox_spots - + kikiskip_connectors - + mirehera_connectors - + heraswamp_connectors - + thievesdesert_connectors - + specrock_connectors ] dungeon_entrance = [ @@ -239,23 +183,12 @@ def underworld_glitches_rules(world, player): def dash_clip(state, region, player): return state.can_dash_clip(region, player) - - if not world.fix_palaceofdarkness_exit[player]: - Rules.set_rule( - world.get_entrance("Palace of Darkness HMG Exit", player), - lambda state: state.can_reach(world.get_entrance("Palace of Darkness", player), player), - ) - - # Bomb clips for clip in ( kikiskip_spots + icepalace_spots + thievesdesert_spots + specrock_spots - + kikiskip_connectors - + thievesdesert_connectors - + specrock_connectors ): region = world.get_region(clip[1], player) Rules.set_rule( @@ -279,15 +212,7 @@ def underworld_glitches_rules(world, player): spot[2], ) - for connector in kikiskip_connectors + thievesdesert_connectors: - dungeon_reentry_rules( - world, - player, - world.get_entrance(connector[0], player), - world.get_entrance(connector[2], player).parent_region.name, - ) - - for clip in mirehera_spots + mirehera_connectors: + for clip in mirehera_spots: Rules.set_rule( world.get_entrance(clip[0], player), lambda state: mire_clip(state), @@ -395,7 +320,7 @@ def underworld_glitches_rules(world, player): for desert_exit in desert_exits: Rules.add_rule( - world.get_entrance(f"Thieves to Desert {desert_exit} Connector", player), + world.get_entrance(f"Thieves to Desert {desert_exit} Clip", player), lambda state: state.can_dash_clip( world.get_region("Thieves Attic", player), player ), diff --git a/test/suite/hmg/inverted_inaccessible_desert.yaml b/test/suite/hmg/inverted_inaccessible_desert.yaml index 5e59ca63..2a55777d 100644 --- a/test/suite/hmg/inverted_inaccessible_desert.yaml +++ b/test/suite/hmg/inverted_inaccessible_desert.yaml @@ -14,6 +14,8 @@ start_inventory: placements: 1: Desert Palace - Boss: Moon Pearl + Desert Palace - Prize: Green Pendant + Sahasrahla: Magic Mirror entrances: 1: two-way: @@ -22,4 +24,6 @@ entrances: Thieves Town: Thieves Town Exit Hyrule Castle Entrance (East): Desert Palace Exit (South) Hyrule Castle Entrance (West): Desert Palace Exit (North) + entrances: + Agahnims Tower: Pyramid Fairy diff --git a/test/suite/hmg/pod_mp.yaml b/test/suite/hmg/pod_mp.yaml new file mode 100644 index 00000000..19de4b63 --- /dev/null +++ b/test/suite/hmg/pod_mp.yaml @@ -0,0 +1,13 @@ +meta: + players: 1 +settings: + 1: + logic: hybridglitches + mode: open + shuffle: vanilla +start_inventory: + 1: + - Pegasus Boots +placements: + 1: + Palace of Darkness - Shooter Room: Moon Pearl \ No newline at end of file From 8ec25d8476b67fbf482e71e4ec8a0ea236fdc45a Mon Sep 17 00:00:00 2001 From: KrisDavie Date: Mon, 16 Dec 2024 15:22:18 +0100 Subject: [PATCH 13/31] Trim down unnecessary parts of loop --- Main.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Main.py b/Main.py index fcb50142..ea73d590 100644 --- a/Main.py +++ b/Main.py @@ -624,12 +624,6 @@ def copy_world(world): for region in world.regions: copied_region = ret.get_region(region.name, region.player) - copied_region.is_light_world = region.is_light_world - copied_region.is_dark_world = region.is_dark_world - copied_region.dungeon = region.dungeon - copied_region.locations = [copied_locations[(location.name, location.player)] for location in region.locations] - for location in copied_region.locations: - location.parent_region = copied_region for entrance in region.entrances: if entrance.name not in hmg_entrances: continue From fb6727c5279f86605f242b637f1b156c779d052b Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 21 Oct 2024 13:47:14 -0400 Subject: [PATCH 14/31] Fix NoneType object error when BK in starting inventory ZenArcane has a mode that starts you with all the big keys, maps, compasses, and enough universal small keys to open all doors. The item pool is updated to remove them, but generation fails due to NoneType item variables. This changes how current_amount is determined to prevent a NoneType variable access and allows for generation to continue. I haven't noticed any adverse side-effects to this, so I decided to PR this upstream. --- ItemList.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ItemList.py b/ItemList.py index be1f744c..3a74d106 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1280,7 +1280,7 @@ def make_customizer_pool(world, player): or (d_item.map and not world.mapshuffle[player])): d_name = d_item.dungeon dungeon = world.get_dungeon(d_name, player) - current_amount = 1 if d_item == dungeon.big_key or d_item in dungeon.dungeon_items else 0 + current_amount = 1 if dungeon.big_key and (d_item == dungeon.big_key or d_item in dungeon.dungeon_items) else 0 additional_amount = amount - current_amount possible_fit = min(additional_amount, len(dungeon_locations[d_name])-dungeon_count[d_name]) if possible_fit > 0: @@ -1291,7 +1291,7 @@ def make_customizer_pool(world, player): pool.extend([item_name] * amount) else: dungeon = world.get_dungeon(d_item.dungeon, player) - current_amount = 1 if d_item == dungeon.big_key or d_item in dungeon.dungeon_items else 0 + current_amount = 1 if dungeon.big_key and (d_item == dungeon.big_key or d_item in dungeon.dungeon_items) else 0 additional_amount = amount - current_amount dungeon.dungeon_items.extend([d_item] * additional_amount) else: From 338e949213daa89e9da9a5907aca65c01022b248 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 17 Dec 2024 15:38:59 -0700 Subject: [PATCH 15/31] feat: update docs and version --- CHANGELOG.md | 8 ++++++++ Main.py | 2 +- RELEASENOTES.md | 15 ++++++--------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b209701..e1bd83c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ Changelog archive +* 1.4.6 + - Restores original Sanc & Quit behavior, if Aga1 is not dead, then no quick pyramid warp + - Fixed problem with Lite/Lean shuffling some fairy caves unnecessarily + - The 300 Rupees item is now marked as priority, so it will not be used as GT junk fill (this matches a change in the VT randomizer) + - Murahdahla is now logically accessible in inverted mode as a bunny. May affect some beatable-only TFH seeds by allowing Moon Pearl to be inaccessible. + - Fixed an issue around PreferredLocationGroups in customizer which would not work with pre-activated Ocraina or priority items. + - Minor fix on triforce text + - Enemy bans for poor placements * 1.4.5 - Logic: Added appropriate enemy logic to GT Mimics 1 and 2 rooms - Logic: Added appropriate enemy logic to Mire 2 room. Note this does change the default logical strats, due to how enemy kill logic works. diff --git a/Main.py b/Main.py index ea73d590..b78eb325 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.4.6' +version_number = '1.4.7' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6dedb16d..7f3f17bb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,11 +1,8 @@ # Patch Notes -1.4.6 - -- Restores original Sanc & Quit behavior, if Aga1 is not dead, then no quick pyramid warp -- Fixed problem with Lite/Lean shuffling some fairy caves unnecessarily -- The 300 Rupees item is now marked as priority, so it will not be used as GT junk fill (this matches a change in the VT randomizer) -- Murahdahla is now logically accessible in inverted mode as a bunny. May affect some beatable-only TFH seeds by allowing Moon Pearl to be inaccessible. -- Fixed an issue around PreferredLocationGroups in customizer which would not work with pre-activated Ocraina or priority items. -- Minor fix on triforce text -- Enemy bans for poor placements +* 1.4.7 + - Fixed generation error with Big Key in starting inventory (thanks Cody!) + - HMG/NL logic fixes by Muffins + - "Beatable" or "accessibility: none" can now use randomized trap doors to seal off entire parts of dungeons + - Logic error with enemizer and standard should use new enemy logic rules + - Fixed a bug with the inconsistent treatment of the beemizer setting From 4f1f61bc6889114959aa5d2a332ab08ddc321e01 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 17 Dec 2024 16:12:49 -0700 Subject: [PATCH 16/31] fix: simple shuffle blacksmith --- RELEASENOTES.md | 1 + Rom.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7f3f17bb..75092b3d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,3 +6,4 @@ - "Beatable" or "accessibility: none" can now use randomized trap doors to seal off entire parts of dungeons - Logic error with enemizer and standard should use new enemy logic rules - Fixed a bug with the inconsistent treatment of the beemizer setting + - Fixed an issue with returning Blacksmith in Simple shuffle (when blacksmith is at Link's House) diff --git a/Rom.py b/Rom.py index a3060fcf..1d2b8864 100644 --- a/Rom.py +++ b/Rom.py @@ -1361,7 +1361,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): rom.write_bytes(0x02F539, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA] if world.powder_patch_required[player] else [0xAD, 0xBF, 0x0A, 0xF0, 0x4F]) # 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', 'simple', 'full', 'lite', 'lean', 'swapped', 'crossed', 'insanity']: rom.write_byte(0x18004C, 0x01) # set correct flag for hera basement item From 01f3f6ad50b73521ba07ce21a4de9331258047f4 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 17 Dec 2024 16:17:55 -0700 Subject: [PATCH 17/31] fix: dark sanc at tavern north fix: gt exit point for all players --- Plando.py | 2 +- RELEASENOTES.md | 1 + Rom.py | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Plando.py b/Plando.py index b2da5ba0..c65b6d02 100755 --- a/Plando.py +++ b/Plando.py @@ -132,7 +132,7 @@ def fill_world(world, plando, text_patches): world.fix_trock_exit = {1: trfstr.strip().lower() == 'true'} elif line.startswith('!fix_gtower_exit'): _, gtfstr = line.split(':', 1) - world.fix_gtower_exit = gtfstr.strip().lower() == 'true' + world.fix_gtower_exit = {1: gtfstr.strip().lower() == 'true'} elif line.startswith('!fix_pod_exit'): _, podestr = line.split(':', 1) world.fix_palaceofdarkness_exit = {1: podestr.strip().lower() == 'true'} diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 75092b3d..eb374b5b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,3 +7,4 @@ - Logic error with enemizer and standard should use new enemy logic rules - Fixed a bug with the inconsistent treatment of the beemizer setting - Fixed an issue with returning Blacksmith in Simple shuffle (when blacksmith is at Link's House) + - Fixed an issue with dark sanc spawn at tavern north door (thanks Codemann!) diff --git a/Rom.py b/Rom.py index 1d2b8864..a3654c8c 100644 --- a/Rom.py +++ b/Rom.py @@ -520,7 +520,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): write_int16(rom, 0x15DB5 + 2 * offset, 0x0640) elif room_id == 0x00d6 and world.fix_trock_exit[player]: write_int16(rom, 0x15DB5 + 2 * offset, 0x0134) - elif room_id == 0x000c and world.fix_gtower_exit: # fix ganons tower exit point + elif room_id == 0x000c and world.fix_gtower_exit[player]: # fix ganons tower exit point write_int16(rom, 0x15DB5 + 2 * offset, 0x00A4) else: write_int16(rom, 0x15DB5 + 2 * offset, link_y) @@ -2512,6 +2512,8 @@ def patch_shuffled_dark_sanc(world, rom, player): dark_sanc = world.get_region('Dark Sanctuary Hint', player) dark_sanc_entrance = str([i for i in dark_sanc.entrances if i.parent_region.name != 'Menu'][0].name) room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = door_addresses[dark_sanc_entrance][1] + if dark_sanc_entrance == 'Tavern North': + link_y -= 0x10 # rom code assumes south-facing doors and adds $10 to the y-coordinate door_index = door_addresses[str(dark_sanc_entrance)][0] rom.write_byte(0x180241, 0x01) From 96f646b1abfee44fcbaa0b33b9be115999fbfe54 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 19 Dec 2024 15:00:34 -0700 Subject: [PATCH 18/31] fix: prevent enemizer crash with walking zora fix: various enemy bans --- RELEASENOTES.md | 4 +++- source/enemizer/SpriteSheets.py | 2 +- source/enemizer/enemy_deny.yaml | 29 +++++++++++++++++++---------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index eb374b5b..ec16bd30 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,8 +3,10 @@ * 1.4.7 - Fixed generation error with Big Key in starting inventory (thanks Cody!) - HMG/NL logic fixes by Muffins + - Enemizer: Disabled Wlkaing Zora in the UW due to crash with Swamola (they ignore a lot of collison anyway) - "Beatable" or "accessibility: none" can now use randomized trap doors to seal off entire parts of dungeons - Logic error with enemizer and standard should use new enemy logic rules - Fixed a bug with the inconsistent treatment of the beemizer setting - Fixed an issue with returning Blacksmith in Simple shuffle (when blacksmith is at Link's House) - - Fixed an issue with dark sanc spawn at tavern north door (thanks Codemann!) + - Fixed an issue with dark sanctuary spawn at tavern north door (thanks Codemann!) + - Various enemy bans for the last couple months diff --git a/source/enemizer/SpriteSheets.py b/source/enemizer/SpriteSheets.py index 4d89a29e..b1b24064 100644 --- a/source/enemizer/SpriteSheets.py +++ b/source/enemizer/SpriteSheets.py @@ -237,7 +237,7 @@ def init_sprite_requirements(): SpriteRequirement(EnemySprite.ArmosKnight).exalt().sub_group(3, 0x1d), SpriteRequirement(EnemySprite.Lanmolas).exalt().sub_group(3, 0x31), SpriteRequirement(EnemySprite.FireballZora).immerse().no_drop().sub_group(2, [0xc, 0x18]), # .uw_skip() test - SpriteRequirement(EnemySprite.Zora).sub_group(2, 0xc).sub_group(3, 0x44), # .uw_skip() test + SpriteRequirement(EnemySprite.Zora).sub_group(2, 0xc).sub_group(3, 0x44).uw_skip(), SpriteRequirement(EnemySprite.DesertStatue).affix().sub_group(2, 0x12), SpriteRequirement(EnemySprite.Crab).sub_group(2, 0xc), SpriteRequirement(EnemySprite.LostWoodsBird).affix().sub_group(2, 0x37).sub_group(3, 0x36), diff --git a/source/enemizer/enemy_deny.yaml b/source/enemizer/enemy_deny.yaml index 301abda2..069bb668 100644 --- a/source/enemizer/enemy_deny.yaml +++ b/source/enemizer/enemy_deny.yaml @@ -17,9 +17,9 @@ UwGeneralDeny: - [ 0x000e, 0, [ "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft" ] ] #"Ice Palace - Entrance - Freezor" - [ 0x000e, 1, [ "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft" ] ] #"Ice Palace - Bari Key - Top Bari" - [ 0x000e, 2, [ "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft" ] ] #"Ice Palace - Bari Key - Middle Bari" - - [ 0x0016, 0, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper", "GreenMimic", "RedMimic", "Pikit"] ] #"Swamp Palace - Pool - Zol 1" - - [ 0x0016, 1, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper", "GreenMimic", "RedMimic", "Pikit" ] ] #"Swamp Palace - Pool - Zol 2" - - [ 0x0016, 2, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper", "GreenMimic", "RedMimic", "Pikit" ] ] #"Swamp Palace - Pool - Blue Bari" + - [ 0x0016, 0, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper", "GreenMimic", "RedMimic", "Pikit", "Deadrock"] ] #"Swamp Palace - Pool - Zol 1" + - [ 0x0016, 1, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper", "GreenMimic", "RedMimic", "Pikit", "Deadrock" ] ] #"Swamp Palace - Pool - Zol 2" + - [ 0x0016, 2, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper", "GreenMimic", "RedMimic", "Pikit", "Deadrock" ] ] #"Swamp Palace - Pool - Blue Bari" - [ 0x0016, 3, [ "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Swamp Palace - Pool - Zol 3" - [ 0x0017, 5, [ "Beamos", "AntiFairyCircle", "SpikeBlock", "Bumper" ] ] #"Tower Of Hera - Bumper Room - Fire Bar (Clockwise)" - [ 0x0019, 0, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Palace of Darkness - Dark Maze - Kodongo 1" @@ -43,7 +43,7 @@ UwGeneralDeny: - [ 0x0024, 6, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots - [ 0x0026, 1, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper", "Statue" ] ] #"Swamp Palace - Big Spoon - Red Bari 1" - [ 0x0026, 8, [ "AntiFairyCircle", "Bumper", "Statue" ] ] #"Swamp Palace - Big Spoon - Red Bari 3" - - [ 0x0026, 9, [ "RollerHorizontalRight", "Statue" ] ] #"Swamp Palace - Big Spoon - Kyameron" + - [0x0026, 9, ["RollerHorizontalRight", "Statue", "BigSpike"]] #"Swamp Palace - Big Spoon - Kyameron" - [ 0x0026, 10, [ "Statue" ] ] # multiple push statues in this room can cause issues - [ 0x0026, 11, [ "Statue" ] ] # multiple push statues in this room can cause issues - [ 0x0027, 0, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalLeft", "FirebarCW" ] ] #"Tower Of Hera - Petting Zoo - Mini Moldorm 1" @@ -84,6 +84,7 @@ UwGeneralDeny: - [ 0x0039, 4, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "FirebarCW", "FirebarCCW" ] ] #"Skull Woods - Play Pen - Spike Trap 1" - [0x0039, 5, ["RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "Bumper"]] #"Skull Woods - Play Pen - Hardhat Beetle" - [ 0x0039, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "FirebarCW", "FirebarCCW" ] ] #"Skull Woods - Play Pen - Spike Trap 2" + - [0x003a, 1, ["RollerVerticalUp"]] - [ 0x003b, 1, [ "Bumper" ]] - [ 0x003b, 4, ["RollerVerticalUp", "RollerVerticalDown"]] - [ 0x003c, 0, ["BigSpike"]] @@ -92,7 +93,7 @@ UwGeneralDeny: - [ 0x003d, 9, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ganon's Tower - Torches 2 - Spark (Counterclockwise)" - [ 0x003d, 10, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ganon's Tower - Torches 2 - Spark (Clockwise) 1" - [ 0x003d, 12, [ "AntiFairyCircle", "Bumper" ] ] #"Ganon's Tower - Torches 2 - Bunny Beam" - - [ 0x003d, 13, [ "AntiFairyCircle", "Bumper" ] ] #"Ganon's Tower - Torches 2 - Antifairy" + - [0x003d, 13, ["AntiFairyCircle", "Bumper", "RollerHorizontalLeft", "RollerHorizontalRight"]] #"Ganon's Tower - Torches 2 - Antifairy" - [0x003e, 8, ["Wizzrobe"]] - [0x003e, 9, ["Wizzrobe", "GreenMimic", "RedMimic", "RedEyegoreMimic", "GreenEyegoreMimic"]] # drops a key by default - [0x003e, 10, ["Wizzrobe"]] @@ -171,7 +172,7 @@ UwGeneralDeny: - [0x0059, 5, ["RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper"]] - [ 0x0059, 9, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Skull Woods - Bridge Room - Gibdo 1" - [ 0x005e, 3, [ "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "SpikeBlock", "Bumper" ] ] #"Ice Palace - Pit Trap - Big Spike Trap" - - [ 0x005e, 4, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Ice Palace - Pit Trap - Fire Bar (Clockwise)" + - [0x005e, 4, ["RollerVerticalUp", "RollerVerticalDown", "SpikeBlock"]] #"Ice Palace - Pit Trap - Fire Bar (Clockwise)" - [ 0x005f, 0, [ "RollerVerticalDown", "RollerHorizontalLeft" ] ] #"Ice Palace - Bari University - Blue Bari 1" - [ 0x005f, 1, [ "RollerVerticalDown", "RollerHorizontalRight" ] ] #"Ice Palace - Bari University - Blue Bari 2" - [ 0x0060, 0, [ "RollerVerticalUp", "RollerHorizontalLeft", "AntiFairyCircle", "BigSpike", "Bumper", "Beamos", "SpikeBlock" ] ] #"Hyrule Castle - West - Blue Guard" @@ -198,6 +199,7 @@ UwGeneralDeny: - [ 0x006a, 5, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Palace of Darkness - Dark Alley - Terrorpin 4" - [ 0x006b, 7, [ "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "Bumper" ] ] #"Ganon's Tower - Mimics 1 - Spike Trap 1" - [ 0x0071, 0, [ "RollerHorizontalLeft" ] ] #"Hyrule Castle - Basement Trap - Green Guard" + - [0x0072, 1, ["RollerHorizontalLeft", "RollerHorizontalRight"]] - [ 0x0074, 0, [ "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Desert Palace - North Hallway - Red Devalant 1" - [ 0x0074, 1, [ "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Desert Palace - North Hallway - Red Devalant 2" - [ 0x0074, 4, [ "AntiFairyCircle", "Bumper" ] ] #"Desert Palace - North Hallway - Leever 1" @@ -275,6 +277,7 @@ UwGeneralDeny: - [ 0x009c, 3, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ganon's Tower - Invisible Floor Maze - Hardhat Beetle 3" - [ 0x009c, 4, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ganon's Tower - Invisible Floor Maze - Hardhat Beetle 4" - [ 0x009c, 5, [ "RollerVerticalUp", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ganon's Tower - Invisible Floor Maze - Hardhat Beetle 5" + - [0x009c, 6, ["AntiFairyCircle", "Bumper"]] - [0x009d, 2, ["AntiFairyCircle"]] - [ 0x009d, 3, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ganon's Tower - Compass Room - Gibdo 2" - [ 0x009d, 6, [ "RollerHorizontalLeft", "RollerHorizontalRight" ] ] #"Ganon's Tower - Compass Room - Blue Bari 1" @@ -414,13 +417,17 @@ UwGeneralDeny: - [ 0x00f1, 4, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight" ] ] #"Old Man Maze - Keese 5" - [ 0x00f1, 5, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight" ] ] #"Old Man Maze - Keese 6" - [0x00fd, 0, ["Bumper", "AntiFairyCircle"]] - - [ 0x0107, 1, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle"]] - - [ 0x0107, 2, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle"]] + - [0x0107, 1, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle"]] + - [0x0107, 2, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle"]] - [0x010b, 6, ["RollerHorizontalRight"]] - [0x010c, 4, ["AntiFairyCircle"]] - [0x010c, 5, ["AntiFairyCircle"]] - [0x010c, 6, ["StalfosKnight", "Geldman", "Blob", "Stal", "Wizzrobe"]] - [0x010c, 7, ["StalfosKnight", "Geldman", "Blob", "Stal", "Wizzrobe"]] + - [0x011e, 0, ["RollerVerticalDown"]] + - [0x011e, 1, ["RollerVerticalDown"]] + - [0x011e, 2, ["RollerVerticalDown"]] + - [0x011e, 3, ["RollerVerticalDown"]] OwGeneralDeny: - [0x03, 2, ["Gibo"]] # OldMan eating Gibo - [0x03, 4, ["Gibo"]] # OldMan eating Gibo @@ -428,12 +435,14 @@ OwGeneralDeny: - [0x03, 6, ["Gibo"]] # OldMan eating Gibo - [0x03, 8, ["Gibo"]] # OldMan eating Gibo - [0x03, 9, ["Gibo"]] # OldMan eating Gibo - - [0x03, 10, ["Gibo"]] # OldMan eating Gibo + - [0x03, 10, ["Gibo", "Beamos"]] # OldMan eating Gibo - [0x05, 10, ["Bumper", "AntiFairyCircle"]] # Blocks path - [0x05, 11, ["Bumper", "AntiFairyCircle"]] # Blocks path to portal - - [0x07, 3, ["Bumper", "AntiFairyCircle"]] # Blocks path to ladder + - [0x07, 3, ["Bumper", "AntiFairyCircle", "RollerHorizontalRight", "RollerHorizontalLeft"]] # Blocks path to ladder + - [0x07, 4, ["RollerHorizontalLeft"]] # Blocks path to ladder - [0x1e, 3, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle"]] # forbid a beamos here - [0x35, 8, ["RollerVerticalUp", "RollerVerticalDown"]] # blocks the dock + - [0x37, 5, ["RollerVerticalUp"]] # combines with a roller above to make the way impassable - [0x40, 0, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle", "Thief"]] - [0x40, 7, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle", "Thief"]] - [0x40, 13, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle", "Thief"]] From 17e48e4e189aa4cc6d3108bcac9680bd60d39110 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 19 Dec 2024 15:18:09 -0700 Subject: [PATCH 19/31] fix: prevent crash from bush spawning mimics hopefully --- RELEASENOTES.md | 7 ++++--- source/enemizer/Enemizer.py | 3 ++- source/enemizer/SpriteSheets.py | 9 +++++++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ec16bd30..4da7420d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,10 +3,11 @@ * 1.4.7 - Fixed generation error with Big Key in starting inventory (thanks Cody!) - HMG/NL logic fixes by Muffins - - Enemizer: Disabled Wlkaing Zora in the UW due to crash with Swamola (they ignore a lot of collison anyway) - - "Beatable" or "accessibility: none" can now use randomized trap doors to seal off entire parts of dungeons + - Enemizer: Disabled Walking Zora in the UW due to crash with Swamola (they ignore a lot of collison anyway) + - Enemizer: Banned new Mimics from being the randomized bush sprite due to crash + - "Beatable" or "accessibility: none" can now use randomized trap doors to seal off entire parts of dungeons (was intended, bug prevented the logic skip) - Logic error with enemizer and standard should use new enemy logic rules - Fixed a bug with the inconsistent treatment of the beemizer setting - Fixed an issue with returning Blacksmith in Simple shuffle (when blacksmith is at Link's House) - Fixed an issue with dark sanctuary spawn at tavern north door (thanks Codemann!) - - Various enemy bans for the last couple months + - Various enemy bans for the last few months diff --git a/source/enemizer/Enemizer.py b/source/enemizer/Enemizer.py index 021b7f03..4a7a61c8 100644 --- a/source/enemizer/Enemizer.py +++ b/source/enemizer/Enemizer.py @@ -408,8 +408,9 @@ def randomize_overworld_enemies(data_tables, custom_ow): chosen = random.choices(candidate_sprites, weight, k=1)[0] sprite.kind = chosen.sprite # randomize the bush sprite per area + bush_candidates = [x for x in candidate_sprites if x.bush_valid] weight = [data_tables.ow_weights[r.sprite] for r in candidate_sprites] - bush_sprite_choice = random.choices(candidate_sprites, weight, k=1)[0] + bush_sprite_choice = random.choices(bush_candidates, weight, k=1)[0] data_tables.bush_sprite_table[area_id] = bush_sprite_choice diff --git a/source/enemizer/SpriteSheets.py b/source/enemizer/SpriteSheets.py index b1b24064..bb021088 100644 --- a/source/enemizer/SpriteSheets.py +++ b/source/enemizer/SpriteSheets.py @@ -21,6 +21,7 @@ class SpriteRequirement: self.uw_valid = True self.can_randomize = True self.water_phobic = False + self.bush_valid = True self.groups = [] self.sub_groups = defaultdict(list) @@ -94,6 +95,10 @@ class SpriteRequirement: self.uw_valid = False return self + def no_bush(self): + self.bush_valid = False + return self + def good_for_uw_water(self): return self.water_only and not self.static and not self.dont_use and self.uw_valid @@ -366,8 +371,8 @@ def init_sprite_requirements(): SpriteRequirement(EnemySprite.MagicShopAssistant).affix().sub_group(0, 0x4b).sub_group(3, 0x5a), SpriteRequirement(EnemySprite.SomariaPlatform).affix().sub_group(2, 0x27), SpriteRequirement(EnemySprite.CastleMantle).affix().sub_group(0, 0x5d), - SpriteRequirement(EnemySprite.GreenMimic).sub_group(1, 0x2c), - SpriteRequirement(EnemySprite.RedMimic).sub_group(1, 0x2c), + SpriteRequirement(EnemySprite.GreenMimic).sub_group(1, 0x2c).no_bush(), + SpriteRequirement(EnemySprite.RedMimic).sub_group(1, 0x2c).no_bush(), SpriteRequirement(EnemySprite.MedallionTablet).affix().sub_group(2, 0x12), # overlord requirements - encapsulated mostly in the required sheets From 804e69380396b239f4190f7c5582dbe74be4ec14 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 19 Dec 2024 16:30:40 -0700 Subject: [PATCH 20/31] fix: problem with enemizer bush enemies --- RELEASENOTES.md | 1 + source/enemizer/Enemizer.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4da7420d..d72daa83 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,7 @@ - Fixed generation error with Big Key in starting inventory (thanks Cody!) - HMG/NL logic fixes by Muffins - Enemizer: Disabled Walking Zora in the UW due to crash with Swamola (they ignore a lot of collison anyway) + - Enemizer: Fixed an issue with enemizer bush sprites - Enemizer: Banned new Mimics from being the randomized bush sprite due to crash - "Beatable" or "accessibility: none" can now use randomized trap doors to seal off entire parts of dungeons (was intended, bug prevented the logic skip) - Logic error with enemizer and standard should use new enemy logic rules diff --git a/source/enemizer/Enemizer.py b/source/enemizer/Enemizer.py index 4a7a61c8..90c7ef34 100644 --- a/source/enemizer/Enemizer.py +++ b/source/enemizer/Enemizer.py @@ -409,7 +409,7 @@ def randomize_overworld_enemies(data_tables, custom_ow): sprite.kind = chosen.sprite # randomize the bush sprite per area bush_candidates = [x for x in candidate_sprites if x.bush_valid] - weight = [data_tables.ow_weights[r.sprite] for r in candidate_sprites] + weight = [data_tables.ow_weights[r.sprite] for r in bush_candidates] bush_sprite_choice = random.choices(bush_candidates, weight, k=1)[0] data_tables.bush_sprite_table[area_id] = bush_sprite_choice @@ -533,5 +533,5 @@ def write_enemy_shuffle_settings(world, player, rom): for idx, pair in enumerate(tile_pattern): rom.write_byte(snes_to_pc(0x09BA2A + idx), (pair[0] + 3) * 16) rom.write_byte(snes_to_pc(0x09BA40 + idx), (pair[1] + 4) * 16) - if world.enemy_shuffle[player] == 'random': + if world.enemy_shuffle[player] == 'shuffled': rom.write_byte(snes_to_pc(0x368100), 1) # randomize bushes From 46e52682e5a6e6a4eba380c89da5f5b1133dd372 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 20 Dec 2024 09:49:41 -0700 Subject: [PATCH 21/31] fix: backwards compatibility with old beemizer settings --- Main.py | 2 +- docs/avianart/pots_n_bones.yaml | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 docs/avianart/pots_n_bones.yaml diff --git a/Main.py b/Main.py index b78eb325..d4e56327 100644 --- a/Main.py +++ b/Main.py @@ -117,7 +117,7 @@ def main(args, seed=None, fish=None): world.enemy_health = args.enemy_health.copy() world.enemy_damage = args.enemy_damage.copy() world.any_enemy_logic = args.any_enemy_logic.copy() - world.beemizer = args.beemizer.copy() + world.beemizer = {player: str(args.beemizer[player]) for player in range(1, world.players + 1)} 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.trap_door_mode = args.trap_door_mode.copy() diff --git a/docs/avianart/pots_n_bones.yaml b/docs/avianart/pots_n_bones.yaml new file mode 100644 index 00000000..e4e80a02 --- /dev/null +++ b/docs/avianart/pots_n_bones.yaml @@ -0,0 +1,24 @@ +meta: + bps: on + user_notes: An attempt to replicate the Avianart Pots and Bones settings +settings: + 1: + goal: triforcehunt + mode: standard + swords: assured + hints: on + shuffle: crossed + keysanity: on + + pottery: lottery + dropshuffle: underworld + + triforce_goal: 216 + triforce_pool: 469 + + beemizer: 0 +start_inventory: + 1: + - Pegasus Boots + - Lamp + - Red Boomerang From fe2a01b38419f51cba1d1ca3932cfa48ca017713 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 20 Dec 2024 09:57:44 -0700 Subject: [PATCH 22/31] fix: bump version --- Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Main.py b/Main.py index d4e56327..695d1559 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.4.7' +version_number = '1.4.7.1' version_branch = '-u' __version__ = f'{version_number}{version_branch}' From f1c681bb1f5af02373a7a3902e8e6544ec4bfd88 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 21 Dec 2024 18:49:03 -0600 Subject: [PATCH 23/31] Fix issue with smith getting deleted on s+q due to bomb walls --- OverworldShuffle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 625e43d2..6663bdf7 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -1404,6 +1404,9 @@ def can_reach_smith(world, player): if world.logic[player] in ['noglitches', 'minorglitches'] and not world.is_tile_swapped(0x29, player): blank_state.collect(ItemFactory('Titans Mitts', player), True) blank_state.collect(ItemFactory('Moon Pearl', player), True) + if not world.bombbag[player]: + blank_state.collect(ItemFactory('Farmable Bombs', player), True) + blank_state.collect(ItemFactory('Farmable Rupees', player), True) found = False explored_regions = list() From 507fbeece902de59df949ed08afd7b56ee18f74b Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 21 Dec 2024 18:57:28 -0600 Subject: [PATCH 24/31] Fix minor issue with shuffle_ganon option missing player check --- BaseClasses.py | 2 +- source/overworld/EntranceShuffle2.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index e74c9ac5..8bfdc544 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -71,7 +71,6 @@ class World(object): self.fix_palaceofdarkness_exit = {} self.fix_trock_exit = {} self.shuffle_ganon = shuffle_ganon - self.fix_gtower_exit = self.shuffle_ganon self.custom = custom self.customitemarray = customitemarray self.can_take_damage = True @@ -133,6 +132,7 @@ class World(object): set_player_attr('fix_skullwoods_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'] or self.doorShuffle[player] not in ['vanilla']) set_player_attr('fix_palaceofdarkness_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) set_player_attr('fix_trock_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) + set_player_attr('fix_gtower_exit', self.shuffle_ganon[player] > 0) set_player_attr('can_access_trock_eyebridge', None) set_player_attr('can_access_trock_front', None) set_player_attr('can_access_trock_big_chest', None) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 994f5f50..c65c4fb8 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -504,7 +504,7 @@ def remove_from_list(t_list, removals): 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: + if not avail.world.shuffle_ganon[avail.player]: if avail.world.is_tile_swapped(0x1b, avail.player) and 'Inverted Pyramid Hole' in holes_to_shuffle: connect_entrance('Inverted Pyramid Hole', 'Pyramid', avail) connect_two_way('Pyramid Entrance', 'Pyramid Exit', avail) From d2f4271a8749a43de802bfbb52c680e76d05d23c Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 23 Dec 2024 10:16:58 -0700 Subject: [PATCH 25/31] fix: make shuffleganon per player --- BaseClasses.py | 2 +- CLI.py | 4 ++-- Main.py | 2 +- RELEASENOTES.md | 4 ++++ Utils.py | 14 +++++++++----- source/overworld/EntranceShuffle2.py | 2 +- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 928594f1..9ac4a9cb 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -61,7 +61,6 @@ class World(object): self.fix_palaceofdarkness_exit = {} self.fix_trock_exit = {} self.shuffle_ganon = shuffle_ganon - self.fix_gtower_exit = self.shuffle_ganon self.custom = custom self.customitemarray = customitemarray self.can_take_damage = True @@ -109,6 +108,7 @@ class World(object): set_player_attr('fix_skullwoods_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'] or self.doorShuffle[player] not in ['vanilla']) set_player_attr('fix_palaceofdarkness_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) set_player_attr('fix_trock_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) + set_player_attr('fix_gtower_exit', self.shuffle_ganon[player] > 0) set_player_attr('can_access_trock_eyebridge', None) set_player_attr('can_access_trock_front', None) set_player_attr('can_access_trock_big_chest', None) diff --git a/CLI.py b/CLI.py index af94e810..4efc85b6 100644 --- a/CLI.py +++ b/CLI.py @@ -132,8 +132,8 @@ def parse_cli(argv, no_defaults=False): 'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', - 'usestartinventory', 'bombbag', 'overworld_map', 'restrict_boss_items', 'triforce_max_difference', - 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', + 'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items', + 'triforce_max_difference', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern', 'skullwoods', 'linked_drops', 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', diff --git a/Main.py b/Main.py index 695d1559..bc2efd8f 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.4.7.1' +version_number = '1.4.7.2' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d72daa83..75f76720 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,9 @@ # Patch Notes +* 1.4.7.2 + - Fixed an issue with shuffle_ganon/fix_gtower_exit causing a generation failure +* 1.4.7.1 + - Fixed an issue with the repaired "beemizer" setting not being backwards compatible * 1.4.7 - Fixed generation error with Big Key in starting inventory (thanks Cody!) - HMG/NL logic fixes by Muffins diff --git a/Utils.py b/Utils.py index 6d52d5f0..5b2d7f1b 100644 --- a/Utils.py +++ b/Utils.py @@ -325,6 +325,10 @@ def update_deprecated_args(args): if args: argVars = vars(args) truthy = [1, True, "True", "true"] + if "multi" in argVars: + players = int(args.multi) + else: + players = 1 # Hints default to FALSE # Don't do: Yes # Do: No @@ -362,11 +366,11 @@ def update_deprecated_args(args): # Don't do: Yes # Do: No if "no_shuffleganon" in argVars: - args.shuffleganon = not args.no_shuffleganon in truthy - # Don't do: No - # Do: Yes - if "shuffleganon" in argVars: - args.no_shuffleganon = not args.shuffleganon in truthy + if isinstance(args.shuffleganon, dict): + for player in range(1, players + 1): + args.shuffleganon[player] = not args.no_shuffleganon in truthy + else: + args.shuffleganon = not args.no_shuffleganon in truthy # Playthrough defaults to TRUE # Don't do: Yes diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 40ead938..a033a112 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -181,7 +181,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def): avail.decoupled_entrances.extend(entrances) avail.decoupled_exits.extend(exits) - if not avail.world.shuffle_ganon: + if not avail.world.shuffle_ganon[avail.player]: if avail.world.is_atgt_swapped(avail.player) and 'Agahnims Tower' in entrances: connect_two_way('Agahnims Tower', 'Ganons Tower Exit', avail) entrances.remove('Agahnims Tower') From e5f38b2d016abadb8fe23866ec75b7f4df36639e Mon Sep 17 00:00:00 2001 From: KrisDavie Date: Mon, 23 Dec 2024 15:55:44 +0100 Subject: [PATCH 26/31] Separate OWG and HMG connection code Split making HMG entrances and connecting them Correctly link IP lobby (not portal) --- Main.py | 22 +++++++------- OverworldGlitchRules.py | 14 ++------- UnderworldGlitchRules.py | 63 ++++++++++++++++++++++------------------ 3 files changed, 49 insertions(+), 50 deletions(-) diff --git a/Main.py b/Main.py index bc2efd8f..a36d34e7 100644 --- a/Main.py +++ b/Main.py @@ -27,7 +27,7 @@ from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dunge from Fill import dungeon_tracking from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, fill_specific_items -from UnderworldGlitchRules import create_hybridmajor_connections, get_hybridmajor_connection_entrances +from UnderworldGlitchRules import connect_hmg_entrances_regions, create_hmg_entrances_regions from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config, verify_item_pool_config @@ -282,7 +282,8 @@ def main(args, seed=None, fish=None): for player in range(1, world.players + 1): if world.logic[player] in ('nologic', 'hybridglitches'): - create_hybridmajor_connections(world, player) + create_hmg_entrances_regions(world, player) + connect_hmg_entrances_regions(world, player) generate_itempool(world, player) verify_item_pool_config(world) @@ -544,6 +545,10 @@ def copy_world(world): connection = Entrance(player, 'Uncle S&Q', parent) parent.exits.append(connection) connection.connect(target) + # This makes the regions for HMG only + # we'll connect them later after all other connections are made (OW <=> UW, Portals) + if world.logic[player] in ('nologic', 'hybridglitches'): + create_hmg_entrances_regions(ret, player) # copy bosses for dungeon in world.dungeons: @@ -560,7 +565,6 @@ def copy_world(world): # We have to skip these for now. They require both the rest of the entrances _and_ the dungeon portals to be copied first # We will connect them later - hmg_entrances = get_hybridmajor_connection_entrances() for region in world.regions: copied_region = ret.get_region(region.name, region.player) @@ -571,8 +575,6 @@ def copy_world(world): for location in copied_region.locations: location.parent_region = copied_region for entrance in region.entrances: - if entrance.name in hmg_entrances: - continue ret.get_entrance(entrance.name, entrance.player).connect(copied_region) # fill locations @@ -620,15 +622,15 @@ def copy_world(world): for player in range(1, world.players + 1): if world.logic[player] in ('nologic', 'hybridglitches'): - create_hybridmajor_connections(ret, player) + connect_hmg_entrances_regions(ret, player) for region in world.regions: copied_region = ret.get_region(region.name, region.player) for entrance in region.entrances: - if entrance.name not in hmg_entrances: - continue - ret.get_entrance(entrance.name, entrance.player).connect(copied_region) - + ent = ret.get_entrance(entrance.name, entrance.player) + if (ent.connected_region != copied_region): + ent.connect(copied_region) + for player in range(1, world.players + 1): set_rules(ret, player) diff --git a/OverworldGlitchRules.py b/OverworldGlitchRules.py index 55e9d896..b9abef8d 100644 --- a/OverworldGlitchRules.py +++ b/OverworldGlitchRules.py @@ -282,20 +282,12 @@ def add_alternate_rule(entrance, rule): old_rule = entrance.access_rule entrance.access_rule = lambda state: old_rule(state) or rule(state) - -def create_no_logic_connections(player, world, connections, connect_external=False): +def create_no_logic_connections(player, world, connections): for entrance, parent_region, target_region, *rule_override in connections: parent = world.get_region(parent_region, player) - - if isinstance(target_region, Region): - target_region = target_region.name - - if connect_external and target_region.endswith(" Portal"): - target = world.get_portal(target_region[:-7], player).find_portal_entrance().parent_region - else: - target = world.get_region(target_region, player) - + target = world.get_region(target_region, player) connection = Entrance(player, entrance, parent) + connection.spot_type = 'OWG' parent.exits.append(connection) connection.connect(target) diff --git a/UnderworldGlitchRules.py b/UnderworldGlitchRules.py index beb95ed9..623a1dcc 100644 --- a/UnderworldGlitchRules.py +++ b/UnderworldGlitchRules.py @@ -1,9 +1,7 @@ import functools -from BaseClasses import Entrance, DoorType +from BaseClasses import Entrance, DoorType, Door from DoorShuffle import connect_simple_door import Rules -from OverworldGlitchRules import create_no_logic_connections -from Doors import create_door kikiskip_spots = [ ("Kiki Skip", "Spectacle Rock Cave (Bottom)", "Palace of Darkness Portal") @@ -13,7 +11,7 @@ mirehera_spots = [("Mire to Hera Clip", "Mire Torches Top", "Hera Portal")] heraswamp_spots = [("Hera to Swamp Clip", "Mire Torches Top", "Swamp Portal")] -icepalace_spots = [("Ice Lobby Clip", "Ice Portal", "Ice Bomb Drop - Top")] +icepalace_spots = [("Ice Lobby Clip", "Ice Lobby", "Ice Bomb Drop - Top")] thievesdesert_spots = [ ("Thieves to Desert West Clip", "Thieves Attic", "Desert West Portal"), @@ -29,10 +27,7 @@ paradox_spots = [ ("Paradox Front Teleport", "Paradox Cave Front", "Paradox Cave Chest Area") ] -# Create connections between dungeons/locations -def create_hybridmajor_connections(world, player): - fix_fake_worlds = world.fix_fake_world[player] - +def create_hmg_entrances_regions(world, player): for spots in [ kikiskip_spots, mirehera_spots, @@ -42,34 +37,44 @@ def create_hybridmajor_connections(world, player): specrock_spots, paradox_spots, ]: - create_no_logic_connections(player, world, spots, connect_external=fix_fake_worlds) + for entrance, parent_region, _, *_ in spots: + parent = world.get_region(parent_region, player) + connection = Entrance(player, entrance, parent) + connection.spot_type = 'HMG' + if connection not in parent.exits: + parent.exits.append(connection) + + ip_bomb_top_reg = world.get_region("Ice Bomb Drop - Top", player) + ip_clip_entrance = Entrance(player, "Ice Bomb Drop Clip", ip_bomb_top_reg) + ip_bomb_top_reg.exits.append(ip_clip_entrance) + + +def connect_hmg_entrances_regions(world, player): + for spots in [ + kikiskip_spots, + mirehera_spots, + heraswamp_spots, + icepalace_spots, + thievesdesert_spots, + specrock_spots, + paradox_spots, + ]: + for entrance, _, target_region, *_ in spots: + connection = world.get_entrance(entrance, player) + if world.fix_fake_world[player] and target_region.endswith(" Portal"): + target = world.get_portal(target_region[:-7], player).find_portal_entrance().parent_region + else: + target = world.get_region(target_region, player) + connection.connect(target) # Add the new Ice path (back of bomb drop to front) to the world and model it properly - clip_door = create_door(player, "Ice Bomb Drop Clip", DoorType.Logical) + ip_clip_entrance = world.get_entrance('Ice Bomb Drop Clip', 1) + clip_door = Door(player, "Ice Bomb Drop Clip", DoorType.Logical, ip_clip_entrance) world.doors += [clip_door] world.initialize_doors([clip_door]) - ice_bomb_top_reg = world.get_region("Ice Bomb Drop - Top", player) - ice_bomb_top_reg.exits.append( - Entrance(player, "Ice Bomb Drop Clip", ice_bomb_top_reg) - ) connect_simple_door(world, "Ice Bomb Drop Clip", "Ice Bomb Drop", player) -def get_hybridmajor_connection_entrances(): - connections = [] - for connector in ( - kikiskip_spots - + mirehera_spots - + heraswamp_spots - + icepalace_spots - + thievesdesert_spots - + specrock_spots - + paradox_spots - ): - connections.append(connector[0]) - connections.append('Ice Bomb Drop Clip') - return set(connections) - # For some entrances, we need to fake having pearl, because we're in fake DW/LW. # This creates a copy of the input state that has Moon Pearl. def fake_pearl_state(state, player): From 897f248c25e948ac8a25918b427528fb0b93b053 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 23 Dec 2024 15:00:58 -0700 Subject: [PATCH 27/31] doc: update notes --- RELEASENOTES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 75f76720..602fb94d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,7 +1,8 @@ # Patch Notes * 1.4.7.2 - - Fixed an issue with shuffle_ganon/fix_gtower_exit causing a generation failure + - Fixed an issue with shuffle_ganon/fix_gtower_exit causing a generation failure + - More HMG fixes by Muffins * 1.4.7.1 - Fixed an issue with the repaired "beemizer" setting not being backwards compatible * 1.4.7 From e3e227f3d5c4f5826001db1c24fc9f978f742cee Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 24 Dec 2024 11:39:27 -0600 Subject: [PATCH 28/31] Baserom update to allow textbox suppression for "this dungeon" items --- Rom.py | 2 +- data/base2current.bps | Bin 133996 -> 134005 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index ff3a316d..7a1cd7ca 100644 --- a/Rom.py +++ b/Rom.py @@ -43,7 +43,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'a51679f0e31ee29f59247545d705992a' +RANDOMIZERBASEHASH = '48d4747c281935f6f5803f13864d8d69' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 680045bc2d2f2419af0062d94da0c5ca14e74799..1eeb15c85371f3849b98e00ad4174d7d682cf5dc 100644 GIT binary patch delta 1993 zcmWmEYgAKL76zUI+>xeM&|nI_dk1`^WmI*&iS=Y z?5q=ajRJJyj}Q$h4Db~or27lh%uGSH0OVNWk`9}(&t*A8GTyE!0K=Fg{zyDEM%y#V zas?g}e+V9UO^onwrqGQKAjOT69+*Of`(p@3p+_1xaj^#hAL4f&fzW|ZJ(h5lcxeD2 zW=g|FTgGVB6jLJA0x0l5o@>F5*TG1Z@4we;4z77 z@lP}%RJ~;c@81GYd_F`U~lLo{Aske2TJcGG$aMB5ZKp|NBWcVh9_6RlXnVzyQ{zh2 z7%CQ@gGpv&@y%I~h2QK7g+yj(SA`JfGdU$8t`NrT)vg0i?i?t0|CUCwsYxuWQk`665Oi$qtkf zYl*%3GviA035#}^XPejVTt+O>of*0|4BWs|&ub?$h;Th-T`U!+QawRsC?@g8i%AfH zUfr8SX*X!&cKl0s2K))HbccvH^^sDOj>^H`y1#^GEV@)4J9V8VUys@8fY)D0GPZ-R?Cp}x>1I+AA=5CLnHv(4U`QGQHD@HV}Jf&e&V@aprSw=G?@&gR_`(FXtZ41DyLgMOjW1W;yFh*A9Ly za|kk5jF=tNR-(4jF{m}aGJ2BLuLiZKuvCKq&syR<6EmHLdSZKP4UaHMmg5Uco@C?J zM~3{eC~&K-=rWz^Y|-ENsiUd05hFJ^Tf8?o%{s2oa7jO|G|CH{ zdY|((2Rqg-BWnv=d2Ku^B=pML#&3?!B`Du_u_0q=85M#P))a`uq`tYZ z8$a(;#AJ^iJ)(b+jQYE0nk+xm-KDD}HnF+UqW$3*mv@<~soADl|8_pFQcLZ_p1x4< z$DjM6MHL;oXGc)huLwMHm#z#Xj;aa6iwZ98Cdl5(OXS}=bmUjKq(8`|rXx{DR$+1f zMzEr--v$Jp81RMZcz$3N*Ef&Lo7GeI>6-gzZLYt@fsNn%I=}p&&YjGeIY9{tQfk(j z3d1I<8;69Nd&Y9|jnR7zS=%LS77%$gjik&Nj^@iBiK7%m1MjeD9y%`101bLx`7+u= zF%SVz37a5UCNCJMdO6Oern9Pr{obQv^ zT+et2hkYD;|H8tCtuw4Q6L8~T1SH_m!ARlk5+a)E9+U#4wp{%OaDV5})u(b;j^?rH zkje~?ofAlcH4iKzp9{W3HlzRNAvb>M7Ie4Ifi;ZUUI2d5fU^E%hrgL5yKFw@lGj>? zxLt^$%-^49d$@0h@2c;$20AuTV#a%71He4o^ZUC-)&H)S<&L$s3k`gJI~W9lHs6O# z%o{bpW2SgA`8|;=AzX$hVHsvu5;$SPaU>dv? zHQl3CO;oZU_{rbJHa)G1x6)!v7ZYTaX&0sOgIHL}Hu!-nfE(4!Jx>s_|MG`4flG+D z!SscUof8Dn0?42W6c7Z#?0yA27C{=dJ`6&@lR6j%c?scL>?ac!w2@Z)^i_2w(dCGp z-0WG?5*?2<-X&j=n&wqIDC)D$6M}Zov{nqKYT6{MEF8q{ff>n$n*v_@Twax+-G^Uk znXfJMBE0zLq9{3=5D%wAl*{%VHi&o*H&fxh!*^ZAl}$Ej;kdHZ+ZNPbM}uk|JCzL~ z0{3Yd69(HO;-zW77aO$-k^!dJ;?+<9u!_2t3vvAYk?h-CU;`nLy|4$uCDN!}^|fxT zl3^K-`s;M+nHF|(o6IeRaPVVGOCiox5*+s53)_M^Ly6DX?!&M~04bDj1tdTo`$+{@ zJRy~GISwlR&U`lVILP=uvZ9BE!e)NDW&O0O@@@A{Q7Sgm2wCBxu!KVx!8Y|nyqC+C Y{6|!=lD%bv$|Uh)^}@@yaACs#0k=(8JOBUy delta 2036 zcmWmDdsq`!769-$dB6Y$LI{WfO&A0f0YkwTDx#uAMMd$6RH44mxLub*+4=}G(TKoi zL%4=PL(Nn~8c{=?-Hrl=msDv*1J<&j(3Q_>jde?vbQQn#+wqV4{my;;aqd0GU5Wo% zi8tH>s6jtKG|XZ_pajveJBQ-f$-;F)@Ir+isgQ|UJ<>tR`g<+{aG(r)E4TfwiyNfdAb zrAZ>iTOTsSC|fAe0?5%HzFXlZw9YRU(h=nscpfdfs6IRHyg zRZOnr#cjsrx@aL%%myN!mBgxr;K3%vJr=?c8$R=y2m)A1Vmq#w(`VTcV62%GReV)X zWZYDUD*r_pzqjP!T5XIigD;0s7%_t1>1O;25-3Gq=JJ3GCHPzIaay_x1eN^<^ z8(eI6>G^PoXY((caOg%JToPPIOSvyZUf-v6qbJdg=P@F5oA=ddxAYRO_u<3E>eX!N9GS?4y%cGx^wO7gJTUM z8(Q2-BAJ+DGU;q}e#qskOhf_)?~qomw>2VFDd$bf|rmX>LOmxv#bY zBdz&s&}&Dg_o_XQ3|SY)iY-%7R?Di*dve^Tw|kjO8>|;D*XTQbtUq(P8p%I)*Br`m z+jacH=|=sq%A(!o*314`akPHv8p@nqi`8K*?BR8kzFlouNa_RoEa%D+NIKwsczWgP zV%i5m>mt6zmb50oN9eDu%9#20%1-EiOGf(sOJ^K+%leseZzpcAc4+UG@Nu5qM9;9A zgKYxLsHJzH8?E7dZ~xL7EiS6pJv)LXv?&)H>1T`~WLXkv_^pT!KP9PVVo<)Jco3>|5*2vZ|U>RB`=G9i>MFZ9Cu}sK3n_SkR}j-%c-bN{!P< zLzlqmOIwf-Iodb!pBvb*D{1rrQ}N(yr{}L~a0<@8$|^ak^Pv_@eoc!=N@CYpS46xv zRo@b61}r7inB~xB%G@A2Dq$mHcxES+L~PIx^$ar0*3}V|mB>-D$oZkW@vpqMT$U_phwOmR5q2ez)&9#9TfrViy*lMgjf2SG6u)Pnj z2)%L5phGPCCVvMR8m5NU)uv8k{nF;VbP3ZWAF1 zd;R&kaX#zeJJUdI$jDB5603OqF#v&%ya|U`^yM49bn(Mlo5&y#*uWqZ)*TsSXN@HR z^s@zTlLN&ERedOfYm~zG05bZx48kFWyC4Ij01R|uAPm4#Ixq;-u$EpH1n<;nfFbyS ziVDaVVw+s=uc?)G$(-(;{+g`!dv$t-h-+bRl!}v7xiyQ<9}lsxkE@DajsI)%rU_ZIuiV862JgVL~5l Date: Tue, 24 Dec 2024 11:49:24 -0600 Subject: [PATCH 29/31] Implemented District/Nearby Dungeon Item Shuffle --- BaseClasses.py | 85 ++++++++++++------- CLI.py | 9 +- DoorShuffle.py | 8 +- DungeonGenerator.py | 14 +-- Fill.py | 57 ++++++++++--- ItemList.py | 23 ++--- KeyDoorShuffle.py | 18 ++-- README.md | 8 ++ Rom.py | 58 +++++++------ Rules.py | 2 +- mystery_example.yml | 17 ++-- resources/app/cli/args.json | 22 +++-- resources/app/gui/lang/en.json | 11 +++ .../app/gui/randomize/dungeon/keysanity.json | 37 +++++++- .../app/gui/randomize/dungeon/widgets.json | 1 + source/classes/CustomSettings.py | 9 +- source/dungeon/DungeonStitcher.py | 4 +- source/gui/randomize/dungeon.py | 14 ++- source/item/District.py | 6 ++ source/item/FillUtil.py | 23 ++--- source/tools/MysteryUtils.py | 17 +++- 21 files changed, 295 insertions(+), 148 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 8869ed53..98f20269 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -139,10 +139,10 @@ class World(object): set_player_attr('can_access_trock_middle', None) set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'hybridglitches', 'nologic'] or shuffle[player] in ['lean', 'swapped', 'crossed', 'insanity']) - set_player_attr('mapshuffle', False) - set_player_attr('compassshuffle', False) + set_player_attr('mapshuffle', 'none') + set_player_attr('compassshuffle', 'none') set_player_attr('keyshuffle', 'none') - set_player_attr('bigkeyshuffle', False) + set_player_attr('bigkeyshuffle', 'none') set_player_attr('prizeshuffle', 'none') set_player_attr('restrict_boss_items', 'none') set_player_attr('bombbag', False) @@ -477,7 +477,7 @@ class World(object): item.world = self if ((item.prize and self.prizeshuffle[item.player] != 'none') or (item.smallkey and self.keyshuffle[item.player] != 'none') - or (item.bigkey and self.bigkeyshuffle[item.player])): + or (item.bigkey and self.bigkeyshuffle[item.player] != 'none')): item.advancement = True self.precollected_items.append(item) self.state.collect(item, True) @@ -1100,7 +1100,7 @@ class CollectionState(object): new_locations = True while new_locations: reachable_events = [location for location in locations if location.event and - (not key_only or (self.world.keyshuffle[location.item.player] == 'none' and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey)) + (not key_only or (self.world.keyshuffle[location.item.player] in ['none', 'district'] and location.item.smallkey) or (self.world.bigkeyshuffle[location.item.player] in ['none', 'district'] and location.item.bigkey)) and location.can_reach(self)] reachable_events = self._do_not_flood_the_keys(reachable_events) new_locations = False @@ -1577,6 +1577,7 @@ class Region(object): self.exits = [] self.locations = [] self.dungeon = None + self.districts = [] self.shop = None self.world = None self.is_light_world = False # will be set aftermaking connections. @@ -1607,10 +1608,16 @@ class Region(object): return False def can_fill(self, item): + if item.is_near_dungeon_item(self.world): + item_dungeon = self.world.get_dungeon(item.dungeon, self.player) if item.dungeon else item.dungeon_object + ret = (self.dungeon and self.dungeon.is_dungeon_item(item)) + ret = ret or (len(self.districts) and item_dungeon and len([d for d in self.districts if d in item_dungeon.districts])) + return ret and item.player == self.player + inside_dungeon_item = ((item.smallkey and self.world.keyshuffle[item.player] == 'none') - or (item.bigkey and not self.world.bigkeyshuffle[item.player]) - or (item.map and not self.world.mapshuffle[item.player]) - or (item.compass and not self.world.compassshuffle[item.player]) + or (item.bigkey and self.world.bigkeyshuffle[item.player] == 'none') + or (item.map and self.world.mapshuffle[item.player] == 'none') + or (item.compass and self.world.compassshuffle[item.player] == 'none') or (item.prize and self.world.prizeshuffle[item.player] == 'dungeon')) # not all small keys to escape must be in escape # sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)' @@ -1859,6 +1866,7 @@ class Dungeon(object): def __init__(self, name, regions, big_key, small_keys, dungeon_items, player, dungeon_id): self.name = name self.regions = regions + self.districts = [] self.prize = None self.big_key = big_key self.small_keys = small_keys @@ -1887,8 +1895,8 @@ class Dungeon(object): return self.dungeon_items + self.keys + ([self.prize] if self.prize else []) def is_dungeon_item(self, item): - if item.prize: - return item.player == self.player and self.prize is None and self.name not in ['Hyrule Castle', 'Agahnims Tower', 'Ganons Tower'] + if item.prize and item.dungeon is None: + return item.player == self.player and self.name not in ['Hyrule Castle', 'Agahnims Tower', 'Ganons Tower'] else: return item.player == self.player and item.name in [dungeon_item.name for dungeon_item in self.all_items] @@ -2757,6 +2765,7 @@ class Item(object): self.code = code self.price = price self.location = None + self.dungeon_object = None self.world = None self.player = player @@ -2792,9 +2801,16 @@ class Item(object): def is_inside_dungeon_item(self, world): return ((self.prize and world.prizeshuffle[self.player] in ['none', 'dungeon']) or (self.smallkey and world.keyshuffle[self.player] == 'none') - or (self.bigkey and not world.bigkeyshuffle[self.player]) - or (self.compass and not world.compassshuffle[self.player]) - or (self.map and not world.mapshuffle[self.player])) + or (self.bigkey and world.bigkeyshuffle[self.player] == 'none') + or (self.compass and world.compassshuffle[self.player] == 'none') + or (self.map and world.mapshuffle[self.player] == 'none')) + + def is_near_dungeon_item(self, world): + return ((self.prize and world.prizeshuffle[self.player] == 'district') + or (self.smallkey and world.keyshuffle[self.player] == 'district') + or (self.bigkey and world.bigkeyshuffle[self.player] == 'district') + or (self.compass and world.compassshuffle[self.player] == 'district') + or (self.map and world.mapshuffle[self.player] == 'district')) def get_map_location(self): if self.location: @@ -3266,10 +3282,10 @@ class Spoiler(object): outfile.write('Pyramid Hole Pre-opened:'.ljust(line_width) + '%s\n' % self.metadata['open_pyramid'][player]) outfile.write('Overworld Map:'.ljust(line_width) + '%s\n' % self.metadata['overworld_map'][player]) outfile.write('\n') - outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['mapshuffle'][player])) - outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['compassshuffle'][player])) + outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['mapshuffle'][player]) + outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['compassshuffle'][player]) outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['keyshuffle'][player]) - outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['bigkeyshuffle'][player])) + outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['bigkeyshuffle'][player]) outfile.write('Prize Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['prizeshuffle'][player]) outfile.write('Key Logic Algorithm:'.ljust(line_width) + '%s\n' % self.metadata['key_logic'][player]) outfile.write('\n') @@ -3627,9 +3643,11 @@ counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3} # byte 6: LCCC CPAA (shuffle links, crystals ganon, pyramid, access access_mode = {"items": 0, "locations": 1, "none": 2} -# byte 7: B?MC DDPP (big, ?, maps, compass, door_type, prize shuffle) -door_type_mode = {'original': 0, 'big': 1, 'all': 2, 'chaos': 3} -prizeshuffle_mode = {'none': 0, 'dungeon': 1, 'wild': 3} +# byte 7: MMCC SSBB (maps, compass, small, big) +mapshuffle_mode = {'none': 0, 'off': 0, 'district': 2, 'wild': 3, 'on': 3} +compassshuffle_mode = {'none': 0, 'off': 0, 'district': 2, 'wild': 3, 'on': 3} +keyshuffle_mode = {'none': 0, 'off': 0, 'universal': 1, 'district': 2, 'wild': 3, 'on': 3} +bigkeyshuffle_mode = {'none': 0, 'off': 0, 'district': 2, 'wild': 3, 'on': 3} # byte 8: HHHD DPEE (enemy_health, enemy_dmg, potshuffle, enemies) e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4} @@ -3651,11 +3669,11 @@ orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "unrestricted": 4} # byte 12: KMB? FF?? (keep similar, mixed/tile flip, bonk drops, flute spots) flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2} -# byte 13: FBBB TTSS (flute_mode, bow_mode, take_any, small_key_mode) +# byte 13: FBBB TTPP (flute_mode, bow_mode, take_any, prize shuffle) flute_mode = {'normal': 0, 'active': 1} bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} # reserved 8 modes? take_any_mode = {'none': 0, 'random': 1, 'fixed': 2} -keyshuffle_mode = {'none': 0, 'off': 0, 'wild': 1, 'on': 1, 'universal': 2} +prizeshuffle_mode = {'none': 0, 'dungeon': 1, 'district': 2, 'wild': 3} # additions # byte 14: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo) @@ -3663,9 +3681,10 @@ overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2} trap_door_mode = {'vanilla': 0, 'optional': 1, 'boss': 2, 'oneway': 3} key_logic_algo = {'dangerous': 0, 'partial': 1, 'strict': 2} -# byte 15: SSDD ???? (skullwoods, linked_drops, 4 free bytes) +# byte 15: SSLL ??DD (skullwoods, linked_drops, 2 free bytes, door_type) skullwoods_mode = {'original': 0, 'restricted': 1, 'loose': 2, 'followlinked': 3} linked_drops_mode = {'unset': 0, 'linked': 1, 'independent': 2} +door_type_mode = {'original': 0, 'big': 1, 'all': 2, 'chaos': 3} # sfx_shuffle and other adjust items does not affect settings code @@ -3700,9 +3719,8 @@ class Settings(object): (0x80 if w.shufflelinks[p] else 0) | ((8 if w.crystals_ganon_orig[p] == "random" else int(w.crystals_ganon_orig[p])) << 3) | (0x4 if w.is_pyramid_open(p) else 0) | access_mode[w.accessibility[p]], - (0x80 if w.bigkeyshuffle[p] else 0) - | (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0) - | (door_type_mode[w.door_type_mode[p]] << 2) | prizeshuffle_mode[w.prizeshuffle[p]], + (mapshuffle_mode[w.mapshuffle[p]] << 6) | (compassshuffle_mode[w.compassshuffle[p]] << 4) + | (keyshuffle_mode[w.keyshuffle[p]] << 2) | (bigkeyshuffle_mode[w.bigkeyshuffle[p]]), (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0) | (enemy_mode[w.enemy_shuffle[p]]), @@ -3718,12 +3736,13 @@ class Settings(object): | (0x20 if w.shuffle_bonk_drops[p] else 0) | (flutespot_mode[w.owFluteShuffle[p]] << 4), (flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4 - | take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]), + | take_any_mode[w.take_any[p]] << 2 | prizeshuffle_mode[w.prizeshuffle[p]]), ((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 5 | trap_door_mode[w.trap_door_mode[p]] << 3 | key_logic_algo[w.key_logic_algorithm[p]]), - (skullwoods_mode[w.skullwoods[p]] << 6 | linked_drops_mode[w.linked_drops[p]] << 4), + (skullwoods_mode[w.skullwoods[p]] << 6 | linked_drops_mode[w.linked_drops[p]] << 4 + | door_type_mode[w.door_type_mode[p]]), ]) return base64.b64encode(code, "+-".encode()).decode() @@ -3776,11 +3795,10 @@ class Settings(object): args.crystals_ganon[p] = "random" if cgan == 8 else cgan args.openpyramid[p] = True if settings[6] & 0x4 else False - args.bigkeyshuffle[p] = True if settings[7] & 0x80 else False - args.mapshuffle[p] = True if settings[7] & 0x20 else False - args.compassshuffle[p] = True if settings[7] & 0x10 else False - args.door_type_mode[p] = r(door_type_mode)[(settings[7] & 0xc) >> 2] - args.prizeshuffle[p] = r(prizeshuffle_mode)[settings[7] & 0x3] + args.mapshuffle[p] = r(mapshuffle_mode)[(settings[7] & 0xC0) >> 6] + args.compassshuffle[p] = r(compassshuffle_mode)[(settings[7] & 0x30) >> 4] + args.keyshuffle[p] = r(keyshuffle_mode)[(settings[7] & 0xC) >> 2] + args.bigkeyshuffle[p] = r(bigkeyshuffle_mode)[settings[7] & 0x3] args.enemy_health[p] = r(e_health)[(settings[8] & 0xE0) >> 5] args.enemy_damage[p] = r(e_dmg)[(settings[8] & 0x18) >> 3] @@ -3806,7 +3824,7 @@ class Settings(object): args.flute_mode[p] = r(flute_mode)[(settings[13] & 0x80) >> 7] args.bow_mode[p] = r(bow_mode)[(settings[13] & 0x70) >> 4] args.take_any[p] = r(take_any_mode)[(settings[13] & 0xC) >> 2] - args.keyshuffle[p] = r(keyshuffle_mode)[settings[13] & 0x3] + args.prizeshuffle[p] = r(prizeshuffle_mode)[settings[13] & 0x3] if len(settings) > 14: args.pseudoboots[p] = True if settings[14] & 0x80 else False @@ -3817,6 +3835,7 @@ class Settings(object): if len(settings) > 15: args.skullwoods[p] = r(skullwoods_mode)[(settings[15] & 0xc0) >> 6] args.linked_drops[p] = r(linked_drops_mode)[(settings[15] & 0x30) >> 4] + args.door_type_mode[p] = r(door_type_mode)[(settings[15] & 0x3)] class KeyRuleType(FastEnum): diff --git a/CLI.py b/CLI.py index 958a8da4..2d4f898e 100644 --- a/CLI.py +++ b/CLI.py @@ -106,8 +106,7 @@ def parse_cli(argv, no_defaults=False): ret = parser.parse_args(argv) if ret.keysanity: - ret.mapshuffle, ret.compassshuffle, ret.bigkeyshuffle = [True] * 3 - ret.keyshuffle = 'wild' + ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = 'wild' * 4 if ret.keydropshuffle: ret.dropshuffle = 'keys' if ret.dropshuffle == 'none' else ret.dropshuffle @@ -222,10 +221,10 @@ def parse_settings(): "pottery": "none", "colorizepots": True, "shufflepots": False, - "mapshuffle": False, - "compassshuffle": False, + "mapshuffle": "none", + "compassshuffle": "none", "keyshuffle": "none", - "bigkeyshuffle": False, + "bigkeyshuffle": "none", "prizeshuffle": "none", "keysanity": False, "door_shuffle": "vanilla", diff --git a/DoorShuffle.py b/DoorShuffle.py index 05dfcb55..71c90884 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -477,7 +477,7 @@ def choose_portals(world, player): allowed = {name: set(group[0]) for group in world.dungeon_pool[player] for name in group[0]} # key drops allow the big key in the right place in Desert Tiles 2 - bk_shuffle = world.bigkeyshuffle[player] or world.pottery[player] not in ['none', 'cave'] + bk_shuffle = world.bigkeyshuffle[player] != 'none' or world.pottery[player] not in ['none', 'cave'] std_flag = world.mode[player] == 'standard' # roast incognito doors world.get_room(0x60, player).delete(5) @@ -2689,12 +2689,12 @@ def calc_used_dungeon_items(builder, world, player): basic_flag = world.doorShuffle[player] == 'basic' base = 0 if basic_flag else 2 # at least 2 items per dungeon, except in basic base = max(count_reserved_locations(world, player, builder.location_set), base) - if not world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] == 'none': if builder.bk_required and not builder.bk_provided: base += 1 - if not world.compassshuffle[player] and (builder.name not in ['Hyrule Castle', 'Agahnims Tower'] or not basic_flag): + if world.compassshuffle[player] == 'none' and (builder.name not in ['Hyrule Castle', 'Agahnims Tower'] or not basic_flag): base += 1 - if not world.mapshuffle[player] and (builder.name != 'Agahnims Tower' or not basic_flag): + if world.mapshuffle[player] == 'none' and (builder.name != 'Agahnims Tower' or not basic_flag): base += 1 if world.prizeshuffle[player] == 'dungeon' and builder.name not in ['Hyrule Castle', 'Agahnims Tower', 'Ganons Tower']: base += 1 diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 2e539bf1..f8297aff 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -231,7 +231,7 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, all_regions, pro start = ExplorationState(dungeon=name) start.big_key_special = bk_special group_flags, door_map = find_bk_groups(name, available_sectors, proposed_map, bk_special) - bk_flag = False if world.bigkeyshuffle[player] and not bk_special else bk_needed + bk_flag = False if world.bigkeyshuffle[player] != 'none' and not bk_special else bk_needed def exception(d): return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS' @@ -436,7 +436,7 @@ def check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, a if len(dungeon.keys()) <= 1 and len(proposed_map.keys()) < len(doors_to_connect): return False # origin has no more hooks, but not all doors have been proposed - if not world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] == 'none': possible_bks = len(dungeon['Origin'].possible_bk_locations) if bk_special and check_for_special(dungeon['Origin'].visited_regions): possible_bks = 1 @@ -470,7 +470,7 @@ def check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, a if len(outstanding_doors[key]) > 0 and len(hangers[key]) == 0 and len(hooks[opp_key]) == 0: return False all_visited = set() - bk_possible = not bk_needed or (world.bigkeyshuffle[player] and not bk_special) + bk_possible = not bk_needed or (world.bigkeyshuffle[player] != 'none' and not bk_special) for piece in dungeon.values(): all_visited.update(piece.visited_regions) if ((not bk_possible and len(piece.possible_bk_locations) > 0) or @@ -544,7 +544,7 @@ def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_re start = ExplorationState(dungeon=name) start.big_key_special = bk_special - bk_flag = False if world.bigkeyshuffle[player] and not bk_special else bk_needed + bk_flag = False if world.bigkeyshuffle[player] != 'none' and not bk_special else bk_needed def exception(d): return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS' @@ -1775,11 +1775,11 @@ def requested_dungeon_items(world, player): num = 0 if world.prizeshuffle[player] == 'dungeon': num += 1 - if not world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] == 'none': num += 1 - if not world.compassshuffle[player]: + if world.compassshuffle[player] == 'none': num += 1 - if not world.mapshuffle[player]: + if world.mapshuffle[player] == 'none': num += 1 return num diff --git a/Fill.py b/Fill.py index 39825197..6654c813 100644 --- a/Fill.py +++ b/Fill.py @@ -45,12 +45,12 @@ def fill_dungeons_restrictive(world, shuffled_locations): for item in world.get_items(): if ((item.prize and world.prizeshuffle[item.player] != 'none') or (item.smallkey and world.keyshuffle[item.player] != 'none') - or (item.bigkey and world.bigkeyshuffle[item.player])): + or (item.bigkey and world.bigkeyshuffle[item.player] != 'none')): item.advancement = True - elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]): + elif (item.map and world.mapshuffle[item.player] not in ['none', 'district']) or (item.compass and world.compassshuffle[item.player] not in ['none', 'district']): item.priority = True - dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)] + dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world) or item.is_near_dungeon_item(world)] bigs, smalls, prizes, others = [], [], [], [] for i in dungeon_items: (bigs if i.bigkey else smalls if i.smallkey else prizes if i.prize else others).append(i) @@ -76,6 +76,19 @@ def fill_dungeons_restrictive(world, shuffled_locations): prizes_copy = prizes.copy() for attempt in range(15): try: + for player in range(1, world.players + 1): + if world.prizeshuffle[player] == 'district': + dungeon_pool = [] + for dungeon in world.dungeons: + from Dungeons import dungeon_table + if dungeon.player == player and dungeon_table[dungeon.name].prize: + dungeon_pool.append(dungeon) + random.shuffle(dungeon_pool) + for item in prizes: + if item.player == player: + dungeon = dungeon_pool.pop() + dungeon.prize = item + item.dungeon_object = dungeon random.shuffle(prizes) random.shuffle(shuffled_locations) prize_state_base = all_state_base.copy() @@ -86,8 +99,7 @@ def fill_dungeons_restrictive(world, shuffled_locations): logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, 14 - attempt) prizes = prizes_copy.copy() for dungeon in world.dungeons: - if world.prizeshuffle[dungeon.player] == 'dungeon': - dungeon.prize = None + dungeon.prize = None for prize in prizes: if prize.location: prize.location.item = None @@ -186,7 +198,8 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl test_state.sweep_for_events() if location.can_fill(test_state, item_to_place, perform_access_check): if valid_key_placement(item_to_place, location, key_pool, test_state, world): - if item_to_place.prize or valid_dungeon_placement(item_to_place, location, world): + if (item_to_place.prize and world.prizeshuffle[item_to_place.player] == 'none') \ + or valid_dungeon_placement(item_to_place, location, world): return location if item_to_place.smallkey or item_to_place.bigkey or item_to_place.prize: location.item = None @@ -203,6 +216,10 @@ def valid_key_placement(item, location, key_pool, collection_state, world): or world.keyshuffle[item.player] == 'universal' or world.logic[item.player] == 'nologic'): return True dungeon = location.parent_region.dungeon + if not dungeon and item.is_near_dungeon_item(world): + check_dungeon = world.get_dungeon(item.dungeon, item.player) if item.dungeon else item.dungeon_object + if len([d for d in location.parent_region.districts if d in check_dungeon.districts]): + dungeon = check_dungeon if dungeon: if dungeon.name not in item.name and (dungeon.name != 'Hyrule Castle' or 'Escape' not in item.name): return True @@ -236,13 +253,22 @@ def valid_reserved_placement(item, location, world): def valid_dungeon_placement(item, location, world): - if location.parent_region.dungeon: - layout = world.dungeon_layouts[location.player][location.parent_region.dungeon.name] + dungeon = location.parent_region.dungeon + if not dungeon and item.is_near_dungeon_item(world): + check_dungeon = world.get_dungeon(item.dungeon, item.player) if item.dungeon else item.dungeon_object + if len([d for d in location.parent_region.districts if d in check_dungeon.districts]): + dungeon = check_dungeon + if dungeon: + layout = world.dungeon_layouts[location.player][dungeon.name] if not is_dungeon_item(item, world) or item.player != location.player: + if item.prize and item.is_near_dungeon_item(world): + return item.dungeon_object == dungeon and layout.free_items > 0 return layout.free_items > 0 + elif item.prize: + return not dungeon.prize and layout.dungeon_items > 0 else: # the second half probably doesn't matter much - should always return true - return item.dungeon == location.parent_region.dungeon.name and layout.dungeon_items > 0 + return item.dungeon == dungeon.name and layout.dungeon_items > 0 return not is_dungeon_item(item, world) @@ -267,14 +293,15 @@ def track_dungeon_items(item, location, world): layout.free_items -= 1 if item.prize: location.parent_region.dungeon.prize = item + item.dungeon_object = location.parent_region.dungeon def is_dungeon_item(item, world): return ((item.prize and world.prizeshuffle[item.player] in ['none', 'dungeon']) or (item.smallkey and world.keyshuffle[item.player] == 'none') - or (item.bigkey and not world.bigkeyshuffle[item.player]) - or (item.compass and not world.compassshuffle[item.player]) - or (item.map and not world.mapshuffle[item.player])) + or (item.bigkey and world.bigkeyshuffle[item.player] == 'none') + or (item.compass and world.compassshuffle[item.player] == 'none') + or (item.map and world.mapshuffle[item.player] == 'none')) def recovery_placement(item_to_place, locations, world, state, base_state, itempool, perform_access_check, attempted, @@ -816,7 +843,8 @@ def balance_multiworld_progression(world): candidate_items = collections.defaultdict(set) while True: for location in balancing_sphere: - if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): + if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) \ + and (world.bigkeyshuffle[location.item.player] != 'none' or not location.item.bigkey): balancing_state.collect(location.item, True, location) player = location.item.player if player in balancing_players and not location.locked and location.player != player: @@ -895,7 +923,8 @@ def balance_multiworld_progression(world): sphere_locations.add(location) for location in sphere_locations: - if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): + if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) \ + and (world.bigkeyshuffle[location.item.player] != 'none' or not location.item.bigkey): state.collect(location.item, True, location) checked_locations |= sphere_locations diff --git a/ItemList.py b/ItemList.py index 6f3b7d73..5727c523 100644 --- a/ItemList.py +++ b/ItemList.py @@ -348,11 +348,11 @@ def generate_itempool(world, player): world.treasure_hunt_icon[player] = 'Triforce Piece' world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player - and ((item.prize and world.prizeshuffle[player] == 'wild') - or (item.smallkey and world.keyshuffle[player] != 'none') - or (item.bigkey and world.bigkeyshuffle[player]) - or (item.map and world.mapshuffle[player]) - or (item.compass and world.compassshuffle[player]))]) + and ((item.prize and world.prizeshuffle[player] not in ['none', 'dungeon', 'district']) + or (item.smallkey and world.keyshuffle[player] not in ['none', 'district']) + or (item.bigkey and world.bigkeyshuffle[player] not in ['none', 'district']) + or (item.map and world.mapshuffle[player] not in ['none', 'district']) + or (item.compass and world.compassshuffle[player] not in ['none', 'district']))]) if world.logic[player] == 'hybridglitches' and world.pottery[player] not in ['none', 'cave']: keys_to_remove = 2 @@ -753,6 +753,7 @@ def fill_prizes(world, attempts=15): fill_restrictive(world, all_state, prize_locs, prizepool, single_player_placement=True) for prize_loc in crystal_locations: prize_loc.parent_region.dungeon.prize = prize_loc.item + prize_loc.item.dungeon_object = prize_loc.parent_region.dungeon except FillError as e: logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts - attempt - 1) for location in empty_crystal_locations: @@ -1434,9 +1435,9 @@ def make_customizer_pool(world, player): or item_name.startswith('Crystal') or item_name.endswith('Pendant')): d_item = ItemFactory(item_name, player) if ((d_item.prize and world.prizeshuffle[player] in ['none', 'dungeon']) - or (d_item.bigkey and not world.bigkeyshuffle[player]) - or (d_item.compass and not world.compassshuffle[player]) - or (d_item.map and not world.mapshuffle[player])): + or (d_item.bigkey and world.bigkeyshuffle[player] == 'none') + or (d_item.compass and world.compassshuffle[player] == 'none') + or (d_item.map and world.mapshuffle[player] == 'none')): d_name = d_item.dungeon dungeon = world.get_dungeon(d_name, player) current_amount = 1 if dungeon.big_key and (d_item == dungeon.big_key or d_item in dungeon.dungeon_items) else 0 @@ -1694,7 +1695,7 @@ def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_ def is_dungeon_item(item, world, player): return (((item.startswith('Crystal') or item.endswith('Pendant')) and world.prizeshuffle[player] in ['none', 'dungeon']) or (item.startswith('Small Key') and world.keyshuffle[player] == 'none') - or (item.startswith('Big Key') and not world.bigkeyshuffle[player]) - or (item.startswith('Compass') and not world.compassshuffle[player]) - or (item.startswith('Map') and not world.mapshuffle[player])) + or (item.startswith('Big Key') and world.bigkeyshuffle[player] == 'none') + or (item.startswith('Compass') and world.compassshuffle[player] == 'none') + or (item.startswith('Map') and world.mapshuffle[player] == 'none')) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index a3c008b1..d5cebf00 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1303,7 +1303,7 @@ def check_rules_deep(original_counter, key_layout, world, player): big_avail = counter.big_key_opened or bk_drop big_maybe_not_found = not counter.big_key_opened and not bk_drop # better named as big_missing? if not key_layout.big_key_special and not big_avail: - if world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] != 'none': big_avail = True else: for location in counter.free_locations: @@ -1430,7 +1430,7 @@ def prize_relevance_sig2(start_regions, d_name, dungeon_entrance, is_atgt_swappe def validate_bk_layout(proposal, builder, start_regions, world, player): bk_special = check_bk_special(builder.master_sector.regions, world, player) - if world.bigkeyshuffle[player] and (world.dropshuffle[player] != 'none' or not bk_special): + if world.bigkeyshuffle[player] != 'none' and (world.dropshuffle[player] != 'none' or not bk_special): return True flat_proposal = flatten_pair_list(proposal) state = ExplorationState(dungeon=builder.name) @@ -1438,7 +1438,7 @@ def validate_bk_layout(proposal, builder, start_regions, world, player): for region in start_regions: dungeon_entrance, portal_door = find_outside_connection(region) prize_relevant_flag = prize_relevance_sig2(start_regions, builder.name, dungeon_entrance, world.is_atgt_swapped(player)) - if prize_relevant_flag and world.prizeshuffle[player] == 'none': + if prize_relevant_flag and world.prizeshuffle[player] in ['none', 'dungeon']: state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance # key_layout.prize_relevant = prize_relevant_flag @@ -1469,7 +1469,7 @@ def validate_key_layout(key_layout, world, player): for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance, world.is_atgt_swapped(player)) - if prize_relevant_flag and world.prizeshuffle[player] == 'none': + if prize_relevant_flag and world.prizeshuffle[player] in ['none', 'dungeon']: state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = prize_relevant_flag @@ -1606,7 +1606,7 @@ def determine_prize_lock(key_layout, world, player): for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance, world.is_atgt_swapped(player)) - if prize_relevant_flag and world.prizeshuffle[player] == 'none': + if prize_relevant_flag and world.prizeshuffle[player] in ['none', 'dungeon']: state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = prize_relevant_flag @@ -1650,13 +1650,13 @@ def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, pla def cnt_avail_big_locations(ttl_locations, state, world, player): - if not world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] == 'none': return max(0, ttl_locations - state.used_locations) if not state.big_key_special else 0 return 1 if not state.big_key_special else 0 def cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player): - if not world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] == 'none': bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0 used_locations = max(0, counter.used_keys - len(counter.key_only_locations)) + bk_adj return max(0, ttl_locations - used_locations) if not layout.big_key_special else 0 @@ -1683,7 +1683,7 @@ def create_key_counters(key_layout, world, player): for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance, world.is_atgt_swapped(player)) - if prize_relevant_flag and world.prizeshuffle[player] == 'none': + if prize_relevant_flag and world.prizeshuffle[player] in ['none', 'dungeon']: state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = prize_relevant_flag @@ -2096,7 +2096,7 @@ def validate_key_placement(key_layout, world, player): bigkey_name = dungeon_bigs[key_layout.sector.name] if world.keyshuffle[player] != 'none': keys_outside = key_layout.max_chests - sum(1 for i in max_counter.free_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) - if world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] != 'none': max_counter = find_max_counter(key_layout) big_key_outside = bigkey_name not in (l.item.name for l in max_counter.free_locations if l.item) for i in world.precollected_items: diff --git a/README.md b/README.md index 0da12e9e..a332ce01 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,10 @@ As far as map trackers, Bonk Locations are supported on `CodeTracker` when the B - 1 8x Bomb Pack - 1 Good Bee +## Nearby Dungeon Items + +This is a new option in addition to the traditional wild vs non-wild (keysanity/non-keysanity) options for all the dungeon item types (maps, compasses, small keys, big keys, prizes). This new option shuffles dungeon items into locations somewhere either within the dungeon that it is assigned to or within the surrounding district of that dungeon. + ## Prize Shuffle A new option has been added to shuffle the 10 dungeon prizes in ways that they haven't been shuffled before. This means that dungeon prizes can be found in other item locations, such as chests or free-standing item locations. This also means that bosses are able to drop a 2nd item in place of the shuffled prize. @@ -280,6 +284,10 @@ This is the normal prize behavior that has been a part of rando up until now. Th This option shuffles the prize into a location somewhere within the dungeon that it is assigned to. +### Nearby + +This option shuffles the prize into a location somewhere either within the dungeon that it is assigned to or within the surrounding district of that dungeon. + ### Randomized This option freely shuffles the prizes throughout the world. While the dungeon prizes can end up anywhere, they still are assigned to a specific dungeon. When you defeat the boss of a certain dungeon, checking the map on the overworld will reveal the location WHERE you can find the prize, an example shown [here](https://zelda.codemann8.com/images/shared/prizemap-all.gif). Finding the map will still reveal WHAT the prize is. If you defeated a boss but haven't collected the map for that dungeon, the prize will be indicated by a red X, example shown [here](https://zelda.codemann8.com/images/shared/prizemap-boss.gif). If you collected a map but haven't defeated the boss yet, the icon indicator on the map will be shown on the top edge (for LW dungeons) or the bottom edge (for DW dungeons), but it will show you WHAT the prize is for that dungeon, an example of that is shown [here](https://zelda.codemann8.com/images/shared/prizemap-map.gif). diff --git a/Rom.py b/Rom.py index 7a1cd7ca..6e0cd76d 100644 --- a/Rom.py +++ b/Rom.py @@ -480,14 +480,14 @@ def patch_rom(world, rom, player, team, is_mystery=False): rom.write_byte(address, value) # patch music - if world.mapshuffle[player]: + if world.mapshuffle[player] != 'none': music = random.choice([0x11, 0x16]) else: music = 0x11 if 'Pendant' in dungeon.prize.name else 0x16 for music_address in dungeon_music_addresses[dungeon.name]: rom.write_byte(music_address, music) - if world.mapshuffle[player]: + if world.mapshuffle[player] != 'none': rom.write_byte(0x155C9, random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle if world.doorShuffle[player] != 'vanilla': @@ -1131,7 +1131,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): ERtimeincrease = 10 else: ERtimeincrease = 20 - if world.keyshuffle[player] != 'none' or world.bigkeyshuffle[player] or world.mapshuffle[player]: + if world.keyshuffle[player] != 'none' or world.bigkeyshuffle[player] != 'none' or world.mapshuffle[player] != 'none': ERtimeincrease = ERtimeincrease + 15 if world.clock_mode == 'none': rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode @@ -1268,34 +1268,36 @@ def patch_rom(world, rom, player, team, is_mystery=False): # Bitfield - enable text box to show with free roaming items # - # --po bmcs + # -tpo bmcs + # t - suppress "this dungeon" textboxes (temporary fix) # p - enabled for non-prize crystals - # o - enabled for outside dungeon items + # o - enabled for outside dungeon items (unused currently?) # b - enabled for inside big keys # m - enabled for inside maps # c - enabled for inside compasses # s - enabled for inside small keys - rom.write_byte(0x18016A, 0x10 | ((0x20 if world.prizeshuffle[player] == 'wild' else 0x00) - | (0x01 if world.keyshuffle[player] == 'wild' else 0x00) - | (0x02 if world.compassshuffle[player] else 0x00) - | (0x04 if world.mapshuffle[player] else 0x00) - | (0x08 if world.bigkeyshuffle[player] else 0x00))) # free roaming item text boxes - rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] else 0x00) # maps showing crystals on overworld + free_item_text = 0x40 if 'district' in [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]] else 0x00 + rom.write_byte(0x18016A, free_item_text | 0x10 | ((0x20 if world.prizeshuffle[player] not in ['none', 'dungeon'] else 0x00) + | (0x01 if world.keyshuffle[player] not in ['none', 'universal'] else 0x00) + | (0x02 if world.compassshuffle[player] != 'none' else 0x00) + | (0x04 if world.mapshuffle[player] != 'none' else 0x00) + | (0x08 if world.bigkeyshuffle[player] != 'none' else 0x00))) # free roaming item text boxes + rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] not in ['none', 'district'] else 0x00) # maps showing crystals on overworld # compasses showing dungeon count - compass_mode = 0x80 if world.compassshuffle[player] else 0x00 + compass_mode = 0x80 if world.compassshuffle[player] not in ['none', 'district'] else 0x00 if world.clock_mode != 'none' or world.dungeon_counters[player] == 'off': pass elif world.dungeon_counters[player] == 'on': compass_mode |= 0x02 # always on - elif (world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player] != 'none' + elif (world.compassshuffle[player] not in ['none', 'district'] or world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player] != 'none' or world.dungeon_counters[player] == 'pickup' or world.pottery[player] not in ['none', 'cave']): compass_mode |= 0x01 # show on pickup if world.overworld_map[player] == 'map': compass_mode |= 0x10 # show icon if map is collected elif world.overworld_map[player] == 'compass': compass_mode |= 0x20 # show icon if compass is collected - if world.prizeshuffle[player] == 'wild': + if world.prizeshuffle[player] not in ['none', 'dungeon', 'district']: compass_mode |= 0x40 # show icon if boss is defeated, hide if collected rom.write_byte(0x18003C, compass_mode) @@ -1331,7 +1333,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): map_index = max(0, dungeon_index - 2) # write out dislocated coords - if map_index >= 0x02 and map_index < 0x18 and (world.overworld_map[player] != 'default' or world.prizeshuffle[player] == 'wild'): + if map_index >= 0x02 and map_index < 0x18 and (world.overworld_map[player] != 'default' or world.prizeshuffle[player] not in ['none', 'dungeon', 'district']): owid_map = [0x1E, 0x30, 0xFF, 0x7B, 0x5E, 0x70, 0x40, 0x75, 0x03, 0x58, 0x47] x_map_position_generic = [0x03c0, 0x0740, 0xff00, 0x03c0, 0x01c0, 0x0bc0, 0x05c0, 0x09c0, 0x0ac0, 0x07c0, 0x0dc0] y_map_position_generic = [0xff00, 0xff00, 0xff00, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0xff00, 0x0fc0, 0x0fc0] @@ -1345,7 +1347,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): write_int16(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+6, y_map_position_generic[idx]) # write out icon coord data - if world.prizeshuffle[player] == 'wild' and dungeon_table[dungeon].prize: + if world.prizeshuffle[player] not in ['none', 'dungeon', 'district'] and dungeon_table[dungeon].prize: dungeon_obj = world.get_dungeon(dungeon, player) entrance = dungeon_obj.prize.get_map_location() coords = get_entrance_coords(entrance) @@ -1385,7 +1387,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): # figure out compass entrances and what world (light/dark) write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6), coords) - if world.prizeshuffle[player] != 'wild' and dungeon_table[dungeon].prize: + if world.prizeshuffle[player] in ['none', 'dungeon', 'district'] and dungeon_table[dungeon].prize: # prize location write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+8, coords) @@ -1424,11 +1426,11 @@ def patch_rom(world, rom, player, team, is_mystery=False): # b - Big Key # a - Small Key # - enable_menu_map_check = (world.overworld_map[player] != 'default' and world.shuffle[player] != 'vanilla') or world.prizeshuffle[player] == 'wild' - rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] == 'wild' else 0x00) - | (0x02 if world.bigkeyshuffle[player] else 0x00) - | (0x04 if world.mapshuffle[player] or enable_menu_map_check else 0x00) - | (0x08 if world.compassshuffle[player] else 0x00) # free roaming items in menu + enable_menu_map_check = (world.overworld_map[player] != 'default' and world.shuffle[player] != 'vanilla') or world.prizeshuffle[player] not in ['none', 'dungeon', 'district'] + rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] not in ['none', 'universal'] else 0x00) + | (0x02 if world.bigkeyshuffle[player] != 'none' else 0x00) + | (0x04 if world.mapshuffle[player] != 'none' or enable_menu_map_check else 0x00) + | (0x08 if world.compassshuffle[player] != 'none' else 0x00) # free roaming items in menu | (0x10 if world.logic[player] == 'nologic' else 0))) # boss icon def get_reveal_bytes(itemName): @@ -2203,11 +2205,11 @@ def write_strings(rom, world, player, team): this_hint = this_hint[0].upper() + this_hint[1:] tt[hint_locations.pop(0)] = this_hint items_to_hint.remove(flute_item) - if world.keyshuffle[player] == 'wild': + if world.keyshuffle[player] not in ['none', 'universal']: items_to_hint.extend(SmallKeys) - if world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] != 'none': items_to_hint.extend(BigKeys) - if world.prizeshuffle[player] == 'wild': + if world.prizeshuffle[player] not in ['none', 'dungeon']: items_to_hint.extend(Prizes) random.shuffle(items_to_hint) hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 8 @@ -2323,10 +2325,14 @@ def write_strings(rom, world, player, team): crystal5 = world.find_items('Crystal 5', player)[0] crystal6 = world.find_items('Crystal 6', player)[0] greenpendant = world.find_items('Green Pendant', player)[0] - if world.prizeshuffle[player] == 'none': + if world.prizeshuffle[player] in ['none', 'dungeon']: (crystal5, crystal6, greenpendant) = tuple([x.parent_region.dungeon.name for x in [crystal5, crystal6, greenpendant]]) tt['bomb_shop'] = 'Big Bomb?\nMy supply is blocked until you clear %s and %s.' % (crystal5, crystal6) tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant + elif world.prizeshuffle[player] == 'district': + (crystal5, crystal6, greenpendant) = tuple([x.item.dungeon_object.name for x in [crystal5, crystal6, greenpendant]]) + tt['bomb_shop'] = 'Big Bomb?\nThe crystals can be found near %s and %s.' % (crystal5, crystal6) + tt['sahasrahla_bring_courage'] = 'I lost my family heirloom near %s' % greenpendant else: tt['bomb_shop'] = 'Big Bomb?\nThe crystals can be found %s and %s.' % (crystal5.hint_text, crystal6.hint_text) tt['sahasrahla_bring_courage'] = 'My family heirloom can be found %s' % greenpendant.hint_text diff --git a/Rules.py b/Rules.py index b75a80c3..4020ce69 100644 --- a/Rules.py +++ b/Rules.py @@ -2029,7 +2029,7 @@ def add_hmg_key_logic_rules(world, player): def add_key_logic_rules(world, player): key_logic = world.key_logic[player] eval_func = eval_small_key_door - if world.key_logic_algorithm[player] == 'strict' and world.keyshuffle[player] == 'wild': + if world.key_logic_algorithm[player] == 'strict' and world.keyshuffle[player] not in ['none', 'universal']: eval_func = eval_small_key_door_strict elif world.key_logic_algorithm[player] != 'dangerous': eval_func = eval_small_key_door_partial diff --git a/mystery_example.yml b/mystery_example.yml index 79f9815d..8fde1fa0 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -151,6 +151,7 @@ prize_shuffle: none: 1 dungeon: 1 + district: 1 wild: 1 dungeon_items: standard: 10 @@ -160,18 +161,22 @@ mcu: 1 # map, compass, universal smalls # for use when you aren't using the dungeon_items above # map_shuffle: - # on: 1 - # off: 1 + # none: 1 + # district: 1 + # wild: 1 # compass_shuffle: - # on: 1 - # off: 1 + # none: 1 + # district: 1 + # wild: 1 # smallkey_shuffle: # none: 5 + # district: 1 # wild: 1 # universal: 1 # bigkey_shuffle: - # on: 1 - # off: 1 + # none: 1 + # district: 1 + # wild: 1 dungeon_counters: on: 5 off: 0 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 7d2de9da..91c84430 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -369,12 +369,18 @@ "type": "bool" }, "mapshuffle": { - "action": "store_true", - "type": "bool" + "choices": [ + "none", + "district", + "wild" + ] }, "compassshuffle": { - "action": "store_true", - "type": "bool" + "choices": [ + "none", + "district", + "wild" + ] }, "keyshuffle": { "choices": [ @@ -384,13 +390,17 @@ ] }, "bigkeyshuffle": { - "action": "store_true", - "type": "bool" + "choices": [ + "none", + "district", + "wild" + ] }, "prizeshuffle": { "choices": [ "none", "dungeon", + "district", "wild" ] }, diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 68fba490..d40944ae 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -53,15 +53,26 @@ "randomizer.dungeon.keysanity": "Shuffle: ", "randomizer.dungeon.mapshuffle": "Maps", + "randomizer.dungeon.mapshuffle.none": "In Dungeon", + "randomizer.dungeon.mapshuffle.district": "Nearby", + "randomizer.dungeon.mapshuffle.wild": "Randomized", "randomizer.dungeon.compassshuffle": "Compasses", + "randomizer.dungeon.compassshuffle.none": "In Dungeon", + "randomizer.dungeon.compassshuffle.district": "Nearby", + "randomizer.dungeon.compassshuffle.wild": "Randomized", "randomizer.dungeon.smallkeyshuffle": "Small Keys", "randomizer.dungeon.smallkeyshuffle.none": "In Dungeon", + "randomizer.dungeon.smallkeyshuffle.district": "Nearby", "randomizer.dungeon.smallkeyshuffle.wild": "Randomized", "randomizer.dungeon.smallkeyshuffle.universal": "Universal", "randomizer.dungeon.bigkeyshuffle": "Big Keys", + "randomizer.dungeon.bigkeyshuffle.none": "In Dungeon", + "randomizer.dungeon.bigkeyshuffle.district": "Nearby", + "randomizer.dungeon.bigkeyshuffle.wild": "Randomized", "randomizer.dungeon.prizeshuffle": "Prizes", "randomizer.dungeon.prizeshuffle.none": "On Boss", "randomizer.dungeon.prizeshuffle.dungeon": "In Dungeon", + "randomizer.dungeon.prizeshuffle.district": "Nearby", "randomizer.dungeon.prizeshuffle.wild": "Randomized", "randomizer.dungeon.decoupledoors": "Decouple Doors", "randomizer.dungeon.door_self_loops": "Allow Self-Looping Spiral Stairs", diff --git a/resources/app/gui/randomize/dungeon/keysanity.json b/resources/app/gui/randomize/dungeon/keysanity.json index 5a2a8e60..9cf39dd0 100644 --- a/resources/app/gui/randomize/dungeon/keysanity.json +++ b/resources/app/gui/randomize/dungeon/keysanity.json @@ -1,9 +1,32 @@ { "keysanity": { + "mapshuffle": { + "type": "selectbox", + "options": [ + "none", + "district", + "wild" + ], + "config": { + "padx": [20,0] + } + }, + "compassshuffle": { + "type": "selectbox", + "options": [ + "none", + "district", + "wild" + ], + "config": { + "padx": [20,0] + } + }, "smallkeyshuffle": { "type": "selectbox", "options": [ "none", + "district", "wild", "universal" ], @@ -11,8 +34,16 @@ "padx": [20,0] } }, - "mapshuffle": { "type": "checkbox" }, - "compassshuffle": { "type": "checkbox" }, - "bigkeyshuffle": { "type": "checkbox" } + "bigkeyshuffle": { + "type": "selectbox", + "options": [ + "none", + "district", + "wild" + ], + "config": { + "padx": [20,0] + } + } } } diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index 89addb90..ec22f036 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -6,6 +6,7 @@ "options": [ "none", "dungeon", + "district", "wild" ], "config": { diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index fb5674e3..17d66946 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -147,11 +147,14 @@ class CustomSettings(object): args.compassshuffle[p] = get_setting(settings['compassshuffle'], args.compassshuffle[p]) if get_setting(settings['keysanity'], args.keysanity): - args.bigkeyshuffle[p] = True + if args.bigkeyshuffle[p] == 'none': + args.bigkeyshuffle[p] = 'wild' if args.keyshuffle[p] == 'none': args.keyshuffle[p] = 'wild' - args.mapshuffle[p] = True - args.compassshuffle[p] = True + if args.mapshuffle[p] == 'none': + args.mapshuffle[p] = 'wild' + if args.compassshuffle[p] == 'none': + args.compassshuffle[p] = 'wild' args.shufflebosses[p] = get_setting(settings['boss_shuffle'], get_setting(settings['shufflebosses'], args.shufflebosses[p])) args.shuffleenemies[p] = get_setting(settings['enemy_shuffle'], get_setting(settings['shuffleenemies'], args.shuffleenemies[p])) diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index 7f0efec5..b521bfcd 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -233,7 +233,7 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se def explore_proposal(name, entrance_regions, all_regions, proposed_map, valid_doors, bk_special, world, player): start = ExplorationState(dungeon=name) - bk_relevant = (world.door_type_mode[player] == 'original' and not world.bigkeyshuffle[player]) or bk_special + bk_relevant = (world.door_type_mode[player] == 'original' and world.bigkeyshuffle[player] == 'none') or bk_special start.big_key_special = bk_special original_state = extend_reachable_state_lenient(entrance_regions, start, proposed_map, all_regions, valid_doors, bk_relevant, world, player) @@ -302,7 +302,7 @@ def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_re target_regions.add(region) start = ExplorationState(dungeon=name) - bk_relevant = (world.door_type_mode[player] == 'original' and not world.bigkeyshuffle[player]) or bk_special + bk_relevant = (world.door_type_mode[player] == 'original' and world.bigkeyshuffle[player] == 'none') or bk_special start.big_key_special = bk_special original_state = extend_reachable_state_lenient(starting_regions, start, proposed_map, all_regions, valid_doors, bk_relevant, world, player) diff --git a/source/gui/randomize/dungeon.py b/source/gui/randomize/dungeon.py index 7400dbe4..88f11379 100644 --- a/source/gui/randomize/dungeon.py +++ b/source/gui/randomize/dungeon.py @@ -14,6 +14,8 @@ def dungeon_page(parent): self.frames = {} self.frames["keysanity"] = Frame(self) self.frames["keysanity"].pack(anchor=W) + self.frames["keysanity2"] = Frame(self) + self.frames["keysanity2"].pack(anchor=W) ## Dungeon Item Shuffle mscbLabel = Label(self.frames["keysanity"], text="Dungeon Items: ") @@ -23,9 +25,15 @@ def dungeon_page(parent): # Defns include frame name, widget type, widget options, widget placement attributes # This first set goes in the Keysanity frame with open(os.path.join("resources","app","gui","randomize","dungeon","keysanity.json")) as keysanityItems: - myDict = json.load(keysanityItems) - myDict = myDict["keysanity"] - dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["keysanity"]) + myDictFile = json.load(keysanityItems) + myDict = myDictFile["keysanity"] + myDict1, myDict2 = dict(), dict() + count = 2 + for key in myDict.keys(): + (myDict1 if count > 0 else myDict2)[key] = myDict[key] + count -= 1 + dictWidgets = {**widgets.make_widgets_from_dict(self, myDict1, self.frames["keysanity"]), \ + **widgets.make_widgets_from_dict(self, myDict2, self.frames["keysanity2"])} for key in dictWidgets: self.widgets[key] = dictWidgets[key] packAttrs = {"side":LEFT} diff --git a/source/item/District.py b/source/item/District.py index 1ad4fba6..bec993bd 100644 --- a/source/item/District.py +++ b/source/item/District.py @@ -96,12 +96,15 @@ def resolve_districts(world): for name, district in world.districts[player].items(): if district.dungeon: + dungeon = world.get_dungeon(district.dungeon, player) + dungeon.districts = [district] + dungeon.districts layout = world.dungeon_layouts[player][district.dungeon] district.locations.update([l.name for r in layout.master_sector.regions for l in r.locations if not l.item and l.real]) else: for region_name in district.regions: region = world.get_region(region_name, player) + region.districts.append(district) for location in region.locations: if not location.item and location.real: district.locations.add(location.name) @@ -115,6 +118,7 @@ def resolve_districts(world): RuntimeError(f'No region connected to entrance: {ent.name} Likely a missing entry in OWExitTypes') visited.add(region) if region.type == RegionType.Cave: + region.districts.append(district) for location in region.locations: if not location.item and location.real: district.locations.add(location.name) @@ -123,6 +127,8 @@ def resolve_districts(world): queue.appendleft(ext.connected_region) elif region.type == RegionType.Dungeon and region.dungeon: district.dungeons.add(region.dungeon.name) + if district not in region.dungeon.districts: + region.dungeon.districts.append(district) elif region.name in inaccessible: district.access_points.add(region) diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 8c800f95..c21fa9ca 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -134,7 +134,7 @@ def create_item_pool_config(world): groups = LocationGroup('Major').locs(init_set) if world.prizeshuffle[player] != 'none': groups.locations.extend(mode_grouping['Prizes']) - if world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] != 'none': groups.locations.extend(mode_grouping['Big Keys']) if world.dropshuffle[player] != 'none': groups.locations.extend(mode_grouping['Big Key Drops']) @@ -144,9 +144,9 @@ def create_item_pool_config(world): groups.locations.extend(mode_grouping['Key Drops']) if world.pottery[player] not in ['none', 'cave']: groups.locations.extend(mode_grouping['Pot Keys']) - if world.compassshuffle[player]: + if world.compassshuffle[player] != 'none': groups.locations.extend(mode_grouping['Compasses']) - if world.mapshuffle[player]: + if world.mapshuffle[player] != 'none': groups.locations.extend(mode_grouping['Maps']) if world.shopsanity[player]: groups.locations.append('Capacity Upgrade - Left') @@ -259,12 +259,12 @@ def location_prefilled(location, world, player): def previously_reserved(location, world, player): if '- Boss' in location.name or '- Prize' in location.name: - if world.restrict_boss_items[player] == 'mapcompass' and (not world.compassshuffle[player] - or not world.mapshuffle[player]): + if world.restrict_boss_items[player] == 'mapcompass' and (world.compassshuffle[player] == 'none' + or world.mapshuffle[player] == 'none'): return True - if world.restrict_boss_items[player] == 'dungeon' and (not world.compassshuffle[player] - or not world.mapshuffle[player] - or not world.bigkeyshuffle[player] + if world.restrict_boss_items[player] == 'dungeon' and (world.compassshuffle[player] == 'none' + or world.mapshuffle[player] == 'none' + or world.bigkeyshuffle[player] == 'none' or world.keyshuffle[player] == 'none' or world.prizeshuffle[player] in ['none', 'dungeon']): return True @@ -303,6 +303,7 @@ def massage_item_pool(world): if item.prize: dungeon = dungeon_pool[item.player].pop() dungeon.prize = item + item.dungeon_object = dungeon player_pool[item.player].append(item) for dungeon in world.dungeons: for item in dungeon.all_items: @@ -381,13 +382,13 @@ def determine_major_items(world, player): pass # now what? if world.prizeshuffle[player] not in ['none', 'dungeon']: major_item_set.update({x for x, y in item_table.items() if y[2] == 'Prize'}) - if world.bigkeyshuffle[player]: + if world.bigkeyshuffle[player] != 'none': major_item_set.update({x for x, y in item_table.items() if y[2] == 'BigKey'}) if world.keyshuffle[player] != 'none': major_item_set.update({x for x, y in item_table.items() if y[2] == 'SmallKey'}) - if world.compassshuffle[player]: + if world.compassshuffle[player] != 'none': major_item_set.update({x for x, y in item_table.items() if y[2] == 'Compass'}) - if world.mapshuffle[player]: + if world.mapshuffle[player] != 'none': major_item_set.update({x for x, y in item_table.items() if y[2] == 'Map'}) if world.shopsanity[player]: major_item_set.add('Bomb Upgrade (+5)') diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index e7da45df..6c347db3 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -96,8 +96,14 @@ def roll_settings(weights): dungeon_items = get_choice('dungeon_items') dungeon_items = '' if dungeon_items == 'standard' or dungeon_items is None else dungeon_items dungeon_items = 'mcsb' if dungeon_items == 'full' else dungeon_items - ret.mapshuffle = get_choice_bool('map_shuffle') if 'map_shuffle' in weights else 'm' in dungeon_items - ret.compassshuffle = get_choice_bool('compass_shuffle') if 'compass_shuffle' in weights else 'c' in dungeon_items + if 'map_shuffle' in weights: + ret.mapshuffle = get_choice('map_shuffle') + elif 'm' in dungeon_items: + ret.mapshuffle = 'wild' + if 'compass_shuffle' in weights: + ret.compassshuffle = get_choice('compass_shuffle') + elif 'c' in dungeon_items: + ret.compassshuffle = 'wild' if 'smallkey_shuffle' in weights: ret.keyshuffle = get_choice('smallkey_shuffle') else: @@ -105,7 +111,10 @@ def roll_settings(weights): ret.keyshuffle = 'wild' if 'u' in dungeon_items: ret.keyshuffle = 'universal' - ret.bigkeyshuffle = get_choice_bool('bigkey_shuffle') if 'bigkey_shuffle' in weights else 'b' in dungeon_items + if 'bigkey_shuffle' in weights: + ret.bigkeyshuffle = get_choice('bigkey_shuffle') + elif 'b' in dungeon_items: + ret.bigkeyshuffle = 'wild' ret.prizeshuffle = get_choice('prize_shuffle') ret.accessibility = get_choice('accessibility') @@ -140,7 +149,7 @@ def roll_settings(weights): ret.dungeon_counters = get_choice_non_bool('dungeon_counters') if 'dungeon_counters' in weights else 'default' if ret.dungeon_counters == 'default': - ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle == 'on' else 'off' + ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle != 'none' else 'off' ret.pseudoboots = get_choice_bool('pseudoboots') ret.shopsanity = get_choice_bool('shopsanity') From 12f65f0e05994454153d79af95bb33d0d0feac7f Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 25 Dec 2024 22:59:23 -0600 Subject: [PATCH 30/31] Baserom update to allow textbox suppression for "this dungeon" items --- Rom.py | 2 +- data/base2current.bps | Bin 134005 -> 134014 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 6e0cd76d..261db448 100644 --- a/Rom.py +++ b/Rom.py @@ -43,7 +43,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '48d4747c281935f6f5803f13864d8d69' +RANDOMIZERBASEHASH = '1fde4fa24bc9d3efe450c3bc30e4cf2c' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 1eeb15c85371f3849b98e00ad4174d7d682cf5dc..b8544acb14b79983294fa51d205def14fdd139ac 100644 GIT binary patch delta 2057 zcmWmEdsI_L8UXNoHwh$xKtNssg>Vs21PuzS1%*%_i}Db0t)Q%;qJptUYr*Fl?oEvp zIB9?(Trg;EMWhiltoP_eV8g>wrBxfjmO{Z@v#njDZK-Zrg)QCNKW2X4d^7XMd^2a9 z^`hB&QO5{CCwd4mkj?-W3W9aFZ!Bl#@pJePf{wbR!9mpLk`3{Umuo7(2-+liQzV|G z9T{ZB0#qkj27c(S2*E5)U;+BYV;lSz<#;ZFJ&5%56L0*6cBE-*@SP0pnJ209nk2!1b8V^ZRsz0Y6<&+rp^a(>+!Hyi@TUT?sDs9JxmakSyLq{>7xBE6$;a zH>SBg@{kFM=*U+zA;3kWj9|e-fHX7~od}(b&%$UBUH_7fAr1HpS{pOs!L7sB4Aa$( zqEQ+jK~b@XAQv^n7D=&tw8PPE#8(@1cp4*Jq~JpklN|RgA6_!y2~PzupOGdGhypW5 zjl1XQn?s_@JGFSx?La~O*QCye(BjLPr*C~Pgk$JI+8nT;@6y8N_CKHTw2CP5MkxLGL7x=$O`h`gDO3H#9fn{v^yK2mDZQ2Wp?H%~%4 zI^0#UNPM3rd{8BUbTxs$Vi zvzW7p^IgtEoX0tjaYCjO@iLu8F{g;ri&Mrqmop&KSzoqo@SJRlpRsM&GDYnp4Em`- zwdIA;gRFTms7CXxH9(;*t3tXi!>MZ^_P5mX2vZ39)w)e`bl+oLe*1K}F$d-MX6AbB za-Q8GHdJ<)E?;fde*IP3`KwJRd6%;}VwcmR;Sy)Nw0601{tlz6vh-n8$D{gSQSrDbN&d=re~E?2?+X9AsMaei^p8AqvL%ci}sFGeW3kG ziL_%^&Rf5r$LMN_O=M}Zs=qkT#fL35)MAq%z>M+qYD#(?4ff#>gZ|SOBdlxFJgr4@ z`$JdOj?wxcqAHot{Zz+=?-68=zKd*V(~$K@L6UVQO-;QuP*;yC$vUH^>QmG0s)pu9 zvZ*aWL!L#Y{kvfZjrH4nD@Rq9yV+$nnSKGG8^UZJlo7=u>%iOG?fTx@lPsQ~Yrpxs z&GoqgY}kdD`4#VL+{k_NUQq&qlv*^Vl89F|O?L#UabpEJZH&kx4IP3Dd}3>D6Dcz$ zqRv}yic&&}v%E8=4D|Ze8&HP)KRFpA549H)Dtx$~#uLlQ0of(G_qRv1U4}Q66Pc5A z)+9YSNgsGb&pe|4hpe9zEJ=~g%p}WZNQoPvF_vgXN;UO=QhpaigsyJPAvV4DJt=&t z${tQ6Tx7&5A|&vK-Y}Fs7z8WPfx#%QuN#aOxE>{x%)p=&U~#kUQvg5a!R;qLL516U zuPGnazFTN$@#W#XFy3lj7H^=W(15lu!xPu|k|nB1tI+$#93q+d@L#9J+`FFRSPJhj ze|GEu|KoGZ`;}7x7Lx3+d0URYR8NV_07U(d{3_3o)HHt}?P67ClDV_3_b@~L7nl2a0p~C zdxIW>3sj;nj6)FR>jw%rNv-vR*|?9#X`E}Qtd#gcX!g;1saj-1-Zp2Umn#2{xmGQW z$Mw=8lqMp`YLiPYRq7A1pk$l5OS&g5fFx*h@*57kQb1+*KsLfWh>c>ieJ>|iH@m7GkZL0o0ZgT1O#G7 z64}5=aCHk>rqn&)^UU$QYQEVU(_53&1)hW_c5NZWV^_pMdzd`w_!*s$HzhV@96vMW zVwaz{Nek@q7B5?{xt<2Rh!t&uFuo)@{gux4nE2UL#4ddsQUMcBvPZVSc7XlVm$_U; ziddIC=nR4gwzmW#CGP%*8w}zW$&idk?WKNJ!$IyqJIf#v7P23dfx^{oNyM)=ih{3( z6Z~WBtuv6zhgd4O5|+YRHoFq69 z^Riazu9bEU0kq=}5Cusr2tz>3^lj3yvqft}pu!@LG$_Dck5v%KDm{|`9GE42Q#v)q zxYDWe5Iin@1AOqR6yY~E-%AJ}$E~t1;nXNOmOCO-lYg8%i+0T-_Ei-sJe{APH)J<7P!jMceT=#Y-Yf6p=$g(Q&&SAt!12Adc)D~WB(j1AIG+!oqmpMjGE$zZfdR7z?36~z4C(J zqX{09Nq663NHOv97At=B2LK)3iChHjtZHr~NLznoqNp0;8U7_|$d|7p5^ge8CtmWx zff4ie!b+^0w?qE?D&umUvk)mYCPK%`qhmy%WOcC*MDUmmjejhLU{=29k~AQB*s^t6 zWm9lu*?A+8cR4^@dz&(;NFx!=K3@873A~Rt(x!n8AEs$%aW@zjLs{x6OD=goH_6Gk zDwdxe*AQbEvg`}tyiQ^z$J zQ)f(^KslL178={eH65qz#SVdUVT(JHTyAI^yy^gc;cFDMP%FrA%sWqdCevL(Whljm z=Mx|TwHLNYQm!+WU088ph48~39KN8DZtJDwRs)@dQx^_FBOba~9zAu9Ayt<&9fw)< z0#~`>I&*RNQZu!Yo)_yH-f7_e8t1B3?Rc_dBmLGMV{;U~)-_ue?$kSk0%wvY9P_)> zkctPpB6#mkcg=@nywMc~bMW`B?eIG0btm%A$J!kM>+y2;6aDI&dYeFF8q)V9_7@Ya zFAdg$fPdiPbhjln-Hj+p6~L^0u-_gdk56}->d0L-vw*b9%sn}>t?%78<(_*{X3=3mPsWBL zo82cjdDT>OTFdFrT8pyYB#6ZH-UTc7e_6Bkw_;Ph9nV}? z$BPC#8RP(gb7sfd%AY75y=Lgx$Hu3LXuQ+bVE?}M4pSv_N;~T9#or(2^}dd3dcL(r z-69lJ7Sjj6#Ou8TsPS2E)RKx;!{d+nlKE{__5g=WP$g?K+XGBydy7C|s%*Me5U51N8~!_62#ETjLE>6&Ckxg&rL5b519R^&MANl{giZ zuaYK*(CJHCa28&^w2n79xW7O*HOiPrKX-cmCkC9t#$R*GKQeezS+k$hVv>?~7_9kW z&#UVP#rhFTIrYM#%%*BO#f>6zqq&|^Si-Td|IP5ZA>>Ixsdb^2EEkfoX3qq&FUXNP zp?17Ip>ph<5VuH3g+-5=Zw^KIgpBVb^~B9ShFDZaT~eH3dVamfj4O!xGBWutlX90C zy31_2$2_~o{DPOi$)7b>F*zBjn51Ohq``8)FjQ!$eLeAEAnA>X11gCu;O8DWW)B2H z92O2l@bjsGNb&q4GK#%2AO}cpy7MjY-|NWbM=Dr_ePgpAnH?SbLL>{;-?dBpzn(^} zVd^ffL(k>0);F^UVMYvGk8n z-n6rS82vVk*;>RVp|Ax^B2n`+R`+5Fzyr2;GI6>@p&iDZT)zUY0L-B){UH>hIo2O4 zg-}N)PlpkhPY2I{7WyrDJx%~TJ*^GkIl**OtCS>S7Kii${mJg9jj7<}-I+U5%Gb=1>Ueztb;@;RF8ST_q3>Gr8&#i1G9e4*UJ<9YO7( z Date: Wed, 25 Dec 2024 23:10:40 -0600 Subject: [PATCH 31/31] Version bump 0.5.1.0 --- CHANGELOG.md | 6 ++++++ OverworldShuffle.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62ed719d..8427f9be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.5.1.0 +- New "Nearby" Dungeon Item Shuffle option +- Fixed issue with smith follower getting deleted incorrectly on s+q +- Various generation and logic fixes for HMG/NL +- \~Merged in DR v1.4.6.2~ + ## 0.5.0.6 - New improved Pseudoboots behavior - Fixed issue with missing Blue Potion in Dark Lake Shop in Inverted diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 6663bdf7..6aefd0d6 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.5.0.6' +version_number = '0.5.1.0' # branch indicator is intentionally different across branches version_branch = '-u'