diff --git a/BaseClasses.py b/BaseClasses.py index 26a7906b..365bbd18 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -70,6 +70,7 @@ class World(object): self.customitemarray = customitemarray self.can_take_damage = True self.hints = hints.copy() + self.prizes = {} self.dynamic_regions = [] self.dynamic_locations = [] self.spoiler = Spoiler(self) @@ -122,6 +123,7 @@ class World(object): set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', False) set_player_attr('bigkeyshuffle', False) + set_player_attr('bomblogic', False) set_player_attr('difficulty_requirements', None) set_player_attr('boss_shuffle', 'none') set_player_attr('enemy_shuffle', 'none') @@ -149,6 +151,7 @@ class World(object): set_player_attr('standardize_palettes', 'standardize') set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False}) set_player_attr('owswaps', [[],[],[]]) + set_player_attr('prizes', {'pull': [0, 0, 0], 'crab': [0, 0], 'stun': 0, 'fish': 0}) def get_name_string_for_object(self, obj): return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' @@ -687,6 +690,140 @@ class CollectionState(object): return True return False + def can_farm_rupees(self, player): + tree_pulls = ['Lost Woods East Area', + 'Snitch Lady (East)', + 'Turtle Rock Area', + 'Pyramid Area', + 'Hype Cave Area', + 'Dark South Pass Area', + 'Bumper Cave Area'] + pre_aga_tree_pulls = ['Hyrule Castle Courtyard', 'Mountain Entry Area'] + post_aga_tree_pulls = ['Statues Area', 'Eastern Palace Area'] + + rupee_farms = ['Archery Game', '50 Rupee Cave', '20 Rupee Cave'] + + def can_reach_non_bunny(regionname): + region = self.world.get_region(regionname, player) + return region.can_reach(self) and ((self.world.mode[player] != 'inverted' and region.is_light_world) or (self.world.mode[player] == 'inverted' and region.is_dark_world) or self.has('Pearl', player)) + + for region in rupee_farms: + if can_reach_non_bunny(region): + return True + + if any(i in [0xda, 0xdb] for i in self.world.prizes[player]['pull']): + for region in tree_pulls: + if can_reach_non_bunny(region): + return True + if not self.has('Beat Agahnim 1', player): + for region in pre_aga_tree_pulls: + if can_reach_non_bunny(region): + return True + else: + for region in post_aga_tree_pulls: + if can_reach_non_bunny(region): + return True + return False + + def can_farm_bombs(self, player): + if self.world.mode[player] == 'standard' and not self.has('Zelda Delivered', player): + return True + + bush_bombs = ['Flute Boy Approach Area', + 'Kakariko Area', + 'Village of Outcasts Area', + 'Forgotten Forest Area', + 'Bat Cave Ledge', + 'East Dark Death Mountain (Bottom)'] + rock_bombs = ['Links House Area', + 'Dark Chapel Area', + 'Wooden Bridge Area', + 'Ice Cave Area', + 'Eastern Nook Area', + 'West Death Mountain (Bottom)', + 'Kakariko Fortune Area', + 'Skull Woods Forest', + 'Catfish Area', + 'Dark Fortune Area', + 'Qirn Jump Area', + 'Shield Shop Area', + 'Palace of Darkness Nook Area', + 'Swamp Nook Area', + 'Dark South Pass Area'] + bonk_bombs = ['Kakariko Fortune Area', 'Dark Graveyard Area'] #TODO: Flute Boy Approach Area and Bonk Rock Ledge are available post-Aga + bomb_caves = ['Graveyard Cave', 'Light World Bomb Hut'] + + tree_pulls = ['Lost Woods East Area', + 'Snitch Lady (East)', + 'Turtle Rock Area', + 'Pyramid Area', + 'Hype Cave Area', + 'Dark South Pass Area', + 'Bumper Cave Area'] + pre_aga_tree_pulls = ['Hyrule Castle Courtyard', 'Mountain Entry Area'] + post_aga_tree_pulls = ['Statues Area', 'Eastern Palace Area'] + + bush_crabs = ['Lost Woods East Area', 'Mountain Entry Area'] + pre_aga_bush_crabs = ['Lumberjack Area', 'South Pass Area'] + rock_crabs = ['Desert Pass Area'] + + def can_reach_non_bunny(regionname): + region = self.world.get_region(regionname, player) + return region.can_reach(self) and ((self.world.mode[player] != 'inverted' and region.is_light_world) or (self.world.mode[player] == 'inverted' and region.is_dark_world) or self.has('Pearl', player)) + + # bomb pickups + for region in bush_bombs + bomb_caves: + if can_reach_non_bunny(region): + return True + + if self.can_lift_rocks(player): + for region in rock_bombs: + if can_reach_non_bunny(region): + return True + + if self.has_Boots(player): + for region in bonk_bombs: + if can_reach_non_bunny(region): + return True + + # tree pulls + if any(i in [0xdc, 0xdd, 0xde] for i in self.world.prizes[player]['pull']): + for region in tree_pulls: + if can_reach_non_bunny(region): + return True + if not self.has('Beat Agahnim 1', player): + for region in pre_aga_tree_pulls: + if can_reach_non_bunny(region): + return True + else: + for region in post_aga_tree_pulls: + if can_reach_non_bunny(region): + return True + + # bush crabs (final item isn't considered) + if self.world.enemy_shuffle[player] != 'none': + if self.world.prizes[player]['crab'][0] in [0xdc, 0xdd, 0xde]: + for region in bush_crabs: + if can_reach_non_bunny(region): + return True + if not self.has('Beat Agahnim 1', player): + for region in pre_aga_bush_crabs: + if can_reach_non_bunny(region): + return True + if self.can_lift_rocks(player) and self.world.prizes[player]['crab'][0] in [0xdc, 0xdd, 0xde]: + for region in rock_crabs: + if can_reach_non_bunny(region): + return True + + # stun prize + if self.can_stun_enemies(player) and self.world.prizes[player]['stun'] in [0xdc, 0xdd, 0xde]: + return True + + # bomb purchases + if self.can_farm_rupees(player) and (self.can_buy_unlimited('Bombs (10)', player) or self.can_reach('Big Bomb Shop', None, player)): + return True + return False + def item_count(self, item, player): return self.prog_items[item, player] @@ -743,10 +880,21 @@ class CollectionState(object): or self.can_shoot_arrows(player) or self.has('Fire Rod', player))) - # Check if the player starts without bombs + def can_stun_enemies(self, player): + if self.world.difficulty_adjustments[player] == 'expert': + return False + elif self.world.difficulty_adjustments[player] == 'hard': + return self.has('Hookshot', player) + else: + return self.has('Hookshot', player) \ + or self.has('Blue Boomerang', player) \ + or self.has('Red Boomerang', player) + + # In the future, this can be used to check if the player starts without bombs def can_use_bombs(self, player): - StartingBombs = True - return (self.bomb_mode_check(player, 1) and (StartingBombs or self.has('Bomb Upgrade (+10)', player))) + if self.world.swords[player] == 'bombs': + return self.has_bomb_level(player, 1) + return (not self.world.bomblogic[player] or self.has('Bomb Upgrade (+10)', player)) and self.can_farm_bombs(player) def can_hit_crystal(self, player): return (self.can_use_bombs(player) @@ -778,6 +926,7 @@ class CollectionState(object): def can_get_good_bee(self, player): cave = self.world.get_region('Good Bee Cave', player) return ( + self.can_use_bombs(player) and self.has_bottle(player) and self.has('Bug Catching Net', player) and (self.has_Boots(player) or (self.can_use_medallions(player) and self.has('Quake', player))) and @@ -2273,6 +2422,7 @@ class Spoiler(object): 'logic': self.world.logic, 'mode': self.world.mode, 'retro': self.world.retro, + 'bomblogic': self.world.bomblogic, 'weapons': self.world.swords, 'goal': self.world.goal, 'ow_shuffle': self.world.owShuffle, @@ -2388,6 +2538,7 @@ class Spoiler(object): outfile.write('Experimental:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['experimental'][player] else 'No')) outfile.write('Key Drops shuffled:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) outfile.write('Shopsanity:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shopsanity'][player] else 'No')) + outfile.write('Bomblogic:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['bomblogic'][player] else 'No')) if self.doors: outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( diff --git a/CHANGELOG.md b/CHANGELOG.md index 343a7c36..94736e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +### 0.1.7.2 +- Fixed music algorithm to play correct track in OW Shuffle +- Removed convenient portal on WDM in OW Layout Shuffle +- Fixed Mystery to not spoil OW Shuffle in filename + +### 0.1.7.1 +- Improved bomb logic to consider tree pulls, bush crabs, and stun prize +- Fixed Mystery to use new updated OW mode terminology + +### 0.1.7.0 +- Expanded new DR bomb logic to all modes (bomb usage in logic only if there is an unlimited supply of bombs available) +- ~~Merged DR v0.5.0.1 - Bomblogic mode / Enemizer fixes~~ + ### 0.1.6.9 - ~~Merged DR v0.4.0.12 - Secure random update / Credits fix~~ diff --git a/CLI.py b/CLI.py index 901fda1f..826a80ae 100644 --- a/CLI.py +++ b/CLI.py @@ -97,6 +97,7 @@ def parse_cli(argv, no_defaults=False): 'ow_swap', 'ow_keepsimilar', 'ow_fluteshuffle', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'ganon_item', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', + 'bomblogic', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', @@ -127,6 +128,7 @@ def parse_settings(): settings = { "lang": "en", "retro": False, + "bomblogic": False, "mode": "open", "logic": "noglitches", "goal": "ganon", diff --git a/EntranceShuffle.py b/EntranceShuffle.py index ca281ea7..495a99be 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -7,7 +7,7 @@ from collections import defaultdict def link_entrances(world, player): invFlag = world.mode[player] == 'inverted' - Dungeon_Exits = Dungeon_Exits_Base.copy() if not invFlag else Inverted_Dungeon_Exits_Base.copy() + Dungeon_Exits = Dungeon_Exits_Base.copy() Cave_Exits = Cave_Exits_Base.copy() Old_Man_House = Old_Man_House_Base.copy() Cave_Three_Exits = Cave_Three_Exits_Base.copy() @@ -2058,14 +2058,13 @@ Cave_Exits_Base = [['Elder House Exit (East)', 'Elder House Exit (West)'], ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)'], ['Fairy Ascension Cave Exit (Bottom)', 'Fairy Ascension Cave Exit (Top)'], ['Bumper Cave Exit (Top)', 'Bumper Cave Exit (Bottom)'], - ['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']] - -Cave_Exits_Base += [('Superbunny Cave Exit (Bottom)', 'Superbunny Cave Exit (Top)'), - ('Spiral Cave Exit (Top)', 'Spiral Cave Exit')] + ['Hookshot Cave Back Exit', 'Hookshot Cave Front Exit'], + ['Superbunny Cave Exit (Bottom)', 'Superbunny Cave Exit (Top)'], + ['Spiral Cave Exit (Top)', 'Spiral Cave Exit']] -Cave_Three_Exits_Base = [('Spectacle Rock Cave Exit (Peak)', 'Spectacle Rock Cave Exit (Top)', - 'Spectacle Rock Cave Exit'), +Cave_Three_Exits_Base = [['Spectacle Rock Cave Exit (Peak)', 'Spectacle Rock Cave Exit (Top)', + 'Spectacle Rock Cave Exit'], ['Paradox Cave Exit (Top)', 'Paradox Cave Exit (Middle)','Paradox Cave Exit (Bottom)']] @@ -2387,20 +2386,6 @@ Inverted_DW_Dungeon_Entrances = ['Thieves Town', Inverted_LW_Dungeon_Entrances_Must_Exit = ['Desert Palace Entrance (East)'] -Inverted_Dungeon_Exits_Base = [['Desert Palace Exit (South)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)'], - 'Desert Palace Exit (North)', - 'Eastern Palace Exit', - 'Tower of Hera Exit', - 'Thieves Town Exit', - 'Skull Woods Final Section Exit', - 'Ice Palace Exit', - 'Misery Mire Exit', - 'Palace of Darkness Exit', - 'Swamp Palace Exit', - 'Agahnims Tower Exit', - ['Turtle Rock Ledge Exit (East)', - 'Turtle Rock Exit (Front)', 'Turtle Rock Ledge Exit (West)', 'Turtle Rock Isolated Ledge Exit']] - Inverted_LW_Entrances_Must_Exit = ['Death Mountain Return Cave (West)', 'Two Brothers House (West)'] @@ -2718,6 +2703,10 @@ mandatory_connections = [('Links House S&Q', 'Links House'), ('Fairy Ascension Cave Pots', 'Fairy Ascension Cave (Bottom)'), ('Fairy Ascension Cave Drop', 'Fairy Ascension Cave (Drop)'), ('Superbunny Cave Climb', 'Superbunny Cave (Top)'), + ('Hookshot Cave Front to Middle', 'Hookshot Cave (Middle)'), + ('Hookshot Cave Middle to Front', 'Hookshot Cave (Front)'), + ('Hookshot Cave Middle to Back', 'Hookshot Cave (Back)'), + ('Hookshot Cave Back to Middle', 'Hookshot Cave (Middle)'), ('Ganon Drop', 'Bottom of Pyramid') ] @@ -2862,16 +2851,16 @@ default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'), ('Dark Desert Hint', 'Dark Desert Hint'), ('Dark Desert Fairy', 'Dark Desert Healer Fairy'), ('Spike Cave', 'Spike Cave'), - ('Hookshot Cave', 'Hookshot Cave'), + ('Hookshot Cave', 'Hookshot Cave (Front)'), ('Superbunny Cave (Top)', 'Superbunny Cave (Top)'), ('Cave Shop (Dark Death Mountain)', 'Cave Shop (Dark Death Mountain)'), ('Dark Death Mountain Fairy', 'Dark Death Mountain Healer Fairy'), ('Superbunny Cave (Bottom)', 'Superbunny Cave (Bottom)'), ('Superbunny Cave Exit (Top)', 'East Dark Death Mountain (Top)'), ('Superbunny Cave Exit (Bottom)', 'East Dark Death Mountain (Bottom)'), - ('Hookshot Cave Exit (South)', 'East Dark Death Mountain (Top)'), - ('Hookshot Cave Exit (North)', 'Dark Death Mountain Floating Island'), - ('Hookshot Cave Back Entrance', 'Hookshot Cave'), + ('Hookshot Cave Front Exit', 'East Dark Death Mountain (Top)'), + ('Hookshot Cave Back Exit', 'Dark Death Mountain Floating Island'), + ('Hookshot Cave Back Entrance', 'Hookshot Cave (Back)'), ('Mimic Cave', 'Mimic Cave'), ('Pyramid Exit', 'Pyramid Exit Ledge') @@ -3198,8 +3187,8 @@ exit_ids = {'Links House Exit': (0x01, 0x00), 'Bumper Cave Exit (Bottom)': (0x16, 0x17), 'Superbunny Cave Exit (Top)': (0x14, 0x15), 'Superbunny Cave Exit (Bottom)': (0x13, 0x14), - 'Hookshot Cave Exit (South)': (0x3A, 0x3B), - 'Hookshot Cave Exit (North)': (0x3B, 0x3C), + 'Hookshot Cave Front Exit': (0x3A, 0x3B), + 'Hookshot Cave Back Exit': (0x3B, 0x3C), 'Ganons Tower Exit': (0x37, 0x38), 'Pyramid Exit': (0x36, 0x37), 'Waterfall of Wishing': 0x5C, diff --git a/Fill.py b/Fill.py index de59a2b5..3e046600 100644 --- a/Fill.py +++ b/Fill.py @@ -712,3 +712,33 @@ def balance_money_progression(world): unchecked_locations.remove(location) if location.item.name.startswith('Rupee'): wallet[location.item.player] += rupee_chart[location.item.name] + +def set_prize_drops(world, player): + prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF, + 0xDC, 0xDC, 0xDC, 0xDD, 0xDC, 0xDC, 0xDE, 0xDC, 0xE1, 0xD8, 0xE1, 0xE2, 0xE1, 0xD8, 0xE1, 0xE2, 0xDF, 0xD9, 0xD8, 0xE1, 0xDF, 0xDC, 0xD9, 0xD8, + 0xD8, 0xE3, 0xE0, 0xDB, 0xDE, 0xD8, 0xDB, 0xE2, 0xD9, 0xDA, 0xDB, 0xD9, 0xDB, 0xD9, 0xDB] + + # randomize last 7 slots + new_prizes = random.sample(prizes, 7) + + if world.difficulty_adjustments[player] in ['hard', 'expert']: + prize_replacements = {0xE0: 0xDF, # Fairy -> heart + 0xE3: 0xD8} # Big magic -> small magic + new_prizes = [prize_replacements.get(prize, prize) for prize in new_prizes] + + if world.retro[player]: + prize_replacements = {0xE1: 0xDA, #5 Arrows -> Blue Rupee + 0xE2: 0xDB} #10 Arrows -> Red Rupee + new_prizes = [prize_replacements.get(prize, prize) for prize in new_prizes] + + # write tree pull prizes + world.prizes[player]['pull'] = [ new_prizes.pop(), new_prizes.pop(), new_prizes.pop() ] + + # rupee crab prizes + world.prizes[player]['crab'] = [ new_prizes.pop(), new_prizes.pop() ] + + # stunned enemy prize + world.prizes[player]['stun'] = new_prizes.pop() + + # saved fish prize + world.prizes[player]['fish'] = new_prizes.pop() \ No newline at end of file diff --git a/Gui.py b/Gui.py index d1b0d8bc..f6fc7d62 100755 --- a/Gui.py +++ b/Gui.py @@ -26,6 +26,13 @@ from source.classes.BabelFish import BabelFish from source.classes.Empty import Empty +def check_python_version(fish): + import sys + version = sys.version_info + if version.major < 3 or version.minor < 7: + messagebox.showinfo("Door Shuffle " + ESVersion, fish.translate("cli","cli","old.python.version") % sys.version) + + def guiMain(args=None): # Save settings to file def save_settings(args): @@ -195,6 +202,8 @@ def guiMain(args=None): # load adjust settings into options loadadjustargs(self, self.settings) + check_python_version(self.fish) + # run main window mainWindow.mainloop() diff --git a/ItemList.py b/ItemList.py index ee35450e..afd48f61 100644 --- a/ItemList.py +++ b/ItemList.py @@ -37,7 +37,7 @@ Difficulty = namedtuple('Difficulty', ['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield', 'basicshield', 'progressivearmor', 'basicarmor', 'swordless', 'bombs_only', 'progressivesword', 'basicsword', 'basicbow', 'timedohko', 'timedother', - 'retro', + 'retro', 'bomblogic', 'extras', 'progressive_sword_limit', 'progressive_shield_limit', 'progressive_armor_limit', 'progressive_bottle_limit', 'progressive_bow_limit', 'heart_piece_limit', 'boss_heart_container_limit']) @@ -62,6 +62,7 @@ difficulties = { timedohko = ['Green Clock'] * 25, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, retro = ['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10, + bomblogic = ['Bomb Upgrade (+10)'] * 2, extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit = 4, progressive_shield_limit = 3, @@ -88,6 +89,7 @@ difficulties = { timedohko = ['Green Clock'] * 25, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15, + bomblogic = ['Bomb Upgrade (+10)'] * 2, extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit = 3, progressive_shield_limit = 2, @@ -114,6 +116,7 @@ difficulties = { timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15, + bomblogic = ['Bomb Upgrade (+10)'] * 2, extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit = 2, progressive_shield_limit = 1, @@ -255,10 +258,10 @@ def generate_itempool(world, player): # set up item pool if world.custom: - (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.customitemarray) + (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bomblogic[player], world.customitemarray) world.rupoor_cost = min(world.customitemarray[player]["rupoorcost"], 9999) else: - (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.owShuffle[player], world.owSwap[player], world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.doorShuffle[player], world.logic[player]) + (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.owShuffle[player], world.owSwap[player], world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bomblogic[player], world.doorShuffle[player], world.logic[player]) if player in world.pool_adjustment.keys(): amt = world.pool_adjustment[player] @@ -297,7 +300,7 @@ def generate_itempool(world, player): if item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']: if item not in possible_weapons: possible_weapons.append(item) - if item in ['Bombs (10)']: + if not world.bomblogic[player] and item in ['Bombs (10)']: if item not in possible_weapons and world.doorShuffle[player] != 'crossed': possible_weapons.append(item) starting_weapon = random.choice(possible_weapons) @@ -546,6 +549,18 @@ def set_up_shops(world, player): cap_shop.inventory[1] = None else: cap_shop.inventory[0] = None + if world.bomblogic[player]: + for item in world.itempool: + if item.name == 'Bomb Upgrade (+10)' and item.player == player: + item.advancement = True + if world.shopsanity[player]: + removals = [item for item in world.itempool if item.name == 'Bomb Upgrade (+5)' and item.player == player] + for remove in removals: + world.itempool.remove(remove) + world.itempool.append(ItemFactory('Rupees (50)', player)) # replace the bomb upgrade + else: + cap_shop = world.get_region('Capacity Upgrade', player).shop + cap_shop.inventory[0] = cap_shop.inventory[1] # remove bomb capacity upgrades in bomblogic def customize_shops(world, player): @@ -587,7 +602,7 @@ def customize_shops(world, player): shop.shopkeeper_config = shopkeeper # handle capacity upgrades - randomly choose a bomb bunch or arrow bunch to become capacity upgrades if world.difficulty[player] == 'normal': - if not found_bomb_upgrade and len(possible_replacements) > 0: + if not found_bomb_upgrade and len(possible_replacements) > 0 and not world.bomblogic[player]: choices = [] for shop, idx, loc, item in possible_replacements: if item.name in ['Bombs (3)', 'Bombs (10)']: @@ -735,7 +750,7 @@ rupee_chart = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)' 'Rupees (100)': 100, 'Rupees (300)': 300} -def get_pool_core(progressive, owShuffle, owSwap, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, door_shuffle, logic): +def get_pool_core(progressive, owShuffle, owSwap, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bomblogic, door_shuffle, logic): pool = [] placed_items = {} precollected_items = [] @@ -760,12 +775,6 @@ def get_pool_core(progressive, owShuffle, owSwap, shuffle, difficulty, treasure_ pool.remove('Pegasus Boots') pool.extend(['Rupees (20)']) - if owSwap in ['mixed', 'crossed'] and owShuffle == 'vanilla': - precollected_items.append('Bombs (3)') - precollected_items.append('Rupees (5)') - precollected_items.append('Rupees (5)') - precollected_items.append('Rupees (5)') - if want_progressives(): pool.extend(progressivegloves) else: @@ -788,6 +797,11 @@ def get_pool_core(progressive, owShuffle, owSwap, shuffle, difficulty, treasure_ diff = difficulties[difficulty] pool.extend(diff.baseitems) + if bomblogic: + pool = [item.replace('Bomb Upgrade (+5)','Rupees (5)') for item in pool] + pool = [item.replace('Bomb Upgrade (+10)','Rupees (5)') for item in pool] + pool.extend(diff.bomblogic) + # expert+ difficulties produce the same contents for # all bottles, since only one bottle is available if diff.same_bottle: @@ -890,7 +904,7 @@ def get_pool_core(progressive, owShuffle, owSwap, shuffle, difficulty, treasure_ pool = [item.replace('Bomb Upgrade (+10)', 'Small Heart') for item in pool] return (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) -def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, customitemarray): +def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bomblogic, customitemarray): if isinstance(customitemarray,dict) and 1 in customitemarray: customitemarray = customitemarray[1] pool = [] @@ -1009,7 +1023,7 @@ def test(): for door_shuffle in ['basic', 'crossed', 'vanilla']: for owShuffle in ['full', 'vanilla']: for owSwap in ['mixed', 'vanilla']: - out = get_pool_core(progressive, owShuffle, owSwap, shuffle, difficulty, 30, timer, goal, mode, swords, retro, door_shuffle, logic) + out = get_pool_core(progressive, owShuffle, owSwap, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bomblogic, door_shuffle, logic) count = len(out[0]) + len(out[1]) correct_count = total_items_to_place @@ -1019,7 +1033,7 @@ def test(): if retro: correct_count += 28 try: - assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro)) + assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bomblogic)) except AssertionError as e: print(e) diff --git a/Main.py b/Main.py index 0b81b934..949d0ab6 100644 --- a/Main.py +++ b/Main.py @@ -25,11 +25,13 @@ from RoomData import create_rooms from Rules import set_rules from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items -from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations +from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations, set_prize_drops from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.4.0.12u' +__version__ = '0.5.0.1-u' + +from source.classes.BabelFish import BabelFish class EnemizerError(RuntimeError): @@ -61,7 +63,15 @@ def get_random_ganon_item(swordmode): return random.choice(options) +def check_python_version(): + import sys + version = sys.version_info + if version.major < 3 or version.minor < 7: + logging.warning(BabelFish().translate("cli","cli","old.python.version"), sys.version) + + def main(args, seed=None, fish=None): + check_python_version() if args.outputpath: os.makedirs(args.outputpath, exist_ok=True) output_path.cached_path = args.outputpath @@ -95,6 +105,7 @@ def main(args, seed=None, fish=None): world.compassshuffle = args.compassshuffle.copy() world.keyshuffle = args.keyshuffle.copy() world.bigkeyshuffle = args.bigkeyshuffle.copy() + world.bomblogic = args.bomblogic.copy() world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)} world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)} world.crystals_ganon_orig = args.crystals_ganon.copy() @@ -211,6 +222,8 @@ def main(args, seed=None, fish=None): else: lock_shop_locations(world, player) + for player in range(1, world.players + 1): + set_prize_drops(world, player) logger.info(world.fish.translate("cli","cli","placing.dungeon.prizes")) @@ -275,7 +288,7 @@ def main(args, seed=None, fish=None): customize_shops(world, player) balance_money_progression(world) - if world.owShuffle[1] != 'vanilla' or world.owSwap[1] != 'vanilla': + if world.owShuffle[1] != 'vanilla' or world.owSwap[1] != 'vanilla' or str(world.seed).startswith('M'): outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' else: outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' @@ -295,11 +308,11 @@ def main(args, seed=None, fish=None): rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom) if use_enemizer and (args.enemizercli or not args.jsonout): - base_patch = LocalRom(args.rom) # update base2current.json (side effect) + local_rom = LocalRom(args.rom) # update base2current.json (side effect) if args.rom and not(os.path.isfile(args.rom)): raise RuntimeError("Could not find valid base rom for enemizing at expected path %s." % args.rom) if os.path.exists(args.enemizercli): - patch_enemizer(world, player, rom, args.rom, args.enemizercli, sprite_random_on_hit) + patch_enemizer(world, player, rom, local_rom, args.enemizercli, sprite_random_on_hit) enemized = True if not args.jsonout: rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000) @@ -409,6 +422,7 @@ def copy_world(world): ret.compassshuffle = world.compassshuffle.copy() ret.keyshuffle = world.keyshuffle.copy() ret.bigkeyshuffle = world.bigkeyshuffle.copy() + ret.bomblogic = world.bomblogic.copy() ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy() ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy() ret.crystals_ganon_orig = world.crystals_ganon_orig.copy() @@ -431,6 +445,7 @@ def copy_world(world): ret.standardize_palettes = world.standardize_palettes.copy() ret.owswaps = world.owswaps.copy() ret.owflutespots = world.owflutespots.copy() + ret.prizes = world.prizes.copy() for player in range(1, world.players + 1): create_regions(ret, player) diff --git a/Mystery.py b/Mystery.py index bc225c2d..7fca2d3d 100644 --- a/Mystery.py +++ b/Mystery.py @@ -135,9 +135,9 @@ def roll_settings(weights): ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla' overworld_swap = get_choice('overworld_swap') ret.ow_swap = overworld_swap if overworld_swap != 'none' else 'vanilla' - ret.ow_keepsimilar = get_choice('ow_keepsimilar') - overworld_flute = get_choice('overworld_flute') - ret.ow_swap = overworld_flute if overworld_flute != 'none' else 'vanilla' + ret.ow_keepsimilar = get_choice('overworld_keepsimilar') + overworld_flute = get_choice('flute_shuffle') + ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla' entrance_shuffle = get_choice('entrance_shuffle') ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' door_shuffle = get_choice('door_shuffle') @@ -186,6 +186,8 @@ def roll_settings(weights): ret.retro = True ret.retro = get_choice('retro') == 'on' # this overrides world_state if used + ret.bomblogic = get_choice('bomblogic') == 'on' + ret.hints = get_choice('hints') == 'on' ret.swords = {'randomized': 'random', @@ -239,6 +241,7 @@ def roll_settings(weights): ret.sprite = get_choice('sprite', romweights) ret.disablemusic = get_choice('disablemusic', romweights) == 'on' ret.quickswap = get_choice('quickswap', romweights) == 'on' + ret.reduce_flashing = get_choice('reduce_flashing', romweights) == 'on' ret.fastmenu = get_choice('menuspeed', romweights) ret.heartcolor = get_choice('heartcolor', romweights) ret.heartbeep = get_choice('heartbeep', romweights) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 2f75659e..612a5f7e 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -2,7 +2,7 @@ import RaceRandom as random, logging, copy from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OpenStd, parallel_links, IsParallel -__version__ = '0.1.6.9-u' +__version__ = '0.1.7.2-u' def link_overworld(world, player): # setup mandatory connections diff --git a/PotShuffle.py b/PotShuffle.py index d62f52eb..a0c048bf 100644 --- a/PotShuffle.py +++ b/PotShuffle.py @@ -51,7 +51,7 @@ vanilla_pots = { 43: [Pot(16, 5, PotItem.Heart, 'PoD Sexy Statue'), Pot(44, 5, PotItem.Switch, 'PoD Sexy Statue'), Pot(16, 6, PotItem.Heart, 'PoD Sexy Statue'), Pot(44, 6, PotItem.Bomb, 'PoD Sexy Statue'), Pot(16, 7, PotItem.Heart, 'PoD Sexy Statue'), Pot(44, 7, PotItem.Bomb, 'PoD Sexy Statue'), Pot(146, 21, PotItem.Bomb, 'PoD Map Balcony'), Pot(170, 21, PotItem.FiveArrows, 'PoD Map Balcony'), Pot(146, 22, PotItem.Bomb, 'PoD Map Balcony'), Pot(170, 22, PotItem.FiveArrows, 'PoD Map Balcony')], - 44: [Pot(108, 24, PotItem.Heart, 'Hookshot Cave'), Pot(112, 24, PotItem.Heart, 'Hookshot Cave')], + 44: [Pot(108, 24, PotItem.Heart, 'Hookshot Cave (Middle)'), Pot(112, 24, PotItem.Heart, 'Hookshot Cave (Middle)')], 47: [Pot(28, 7, PotItem.Heart, 'Kakariko Well (top)'), Pot(32, 7, PotItem.Heart, 'Kakariko Well (top)'), Pot(28, 9, PotItem.FiveRupees, 'Kakariko Well (top)'), Pot(32, 9, PotItem.FiveRupees, 'Kakariko Well (top)'), Pot(172, 19, PotItem.FiveRupees, 'Kakariko Well (top)'), Pot(180, 19, PotItem.FiveRupees, 'Kakariko Well (top)'), Pot(104, 27, PotItem.Heart, 'Kakariko Well (bottom)'), Pot(104, 28, PotItem.Heart, 'Kakariko Well (bottom)')], 49: [Pot(92, 28, PotItem.Bomb, 'Hera Beetles'), Pot(96, 28, PotItem.Nothing, 'Hera Beetles')], @@ -66,8 +66,8 @@ vanilla_pots = { 55: [Pot(60, 6, PotItem.Key, 'Swamp Trench 1 Alcove'), Pot(48, 20, PotItem.Nothing, 'Swamp Trench 1 Key Ledge')], 56: [Pot(164, 12, PotItem.Bomb, 'Swamp Pot Row'), Pot(164, 13, PotItem.FiveRupees, 'Swamp Pot Row'), Pot(164, 18, PotItem.Bomb, 'Swamp Pot Row'), Pot(164, 19, PotItem.Key, 'Swamp Pot Row')], 57: [Pot(12, 20, PotItem.Heart, 'Skull Spike Corner'), Pot(48, 28, PotItem.FiveArrows, 'Skull Spike Corner'), Pot(100, 22, PotItem.SmallMagic, 'Skull Final Drop'), Pot(100, 26, PotItem.FiveArrows, 'Skull Final Drop')], - 60: [Pot(24, 8, PotItem.SmallMagic, 'Hookshot Cave'), Pot(64, 12, PotItem.FiveRupees, 'Hookshot Cave'), Pot(20, 14, PotItem.OneRupee, 'Hookshot Cave'), Pot(20, 19, PotItem.Nothing, 'Hookshot Cave'), - Pot(68, 18, PotItem.FiveRupees, 'Hookshot Cave'), Pot(96, 19, PotItem.Heart, 'Hookshot Cave'), Pot(64, 20, PotItem.FiveRupees, 'Hookshot Cave'), Pot(64, 26, PotItem.FiveRupees, 'Hookshot Cave')], + 60: [Pot(24, 8, PotItem.SmallMagic, 'Hookshot Cave (Front)'), Pot(64, 12, PotItem.FiveRupees, 'Hookshot Cave (Front)'), Pot(20, 14, PotItem.OneRupee, 'Hookshot Cave (Front)'), Pot(20, 19, PotItem.Nothing, 'Hookshot Cave (Front)'), + Pot(68, 18, PotItem.FiveRupees, 'Hookshot Cave (Front)'), Pot(96, 19, PotItem.Heart, 'Hookshot Cave (Front)'), Pot(64, 20, PotItem.FiveRupees, 'Hookshot Cave (Front)'), Pot(64, 26, PotItem.FiveRupees, 'Hookshot Cave (Front)')], 61: [Pot(76, 12, PotItem.Bomb, 'GT Mini Helmasaur Room'), Pot(112, 12, PotItem.Bomb, 'GT Mini Helmasaur Room'), Pot(24, 22, PotItem.Heart, 'GT Crystal Inner Circle'), Pot(40, 22, PotItem.FiveArrows, 'GT Crystal Inner Circle'), Pot(32, 24, PotItem.Heart, 'GT Crystal Inner Circle'), Pot(20, 26, PotItem.FiveRupees, 'GT Crystal Inner Circle'), Pot(36, 26, PotItem.BigMagic, 'GT Crystal Inner Circle')], 62: [Pot(96, 6, PotItem.Bomb, 'Ice Stalfos Hint'), Pot(100, 6, PotItem.SmallMagic, 'Ice Stalfos Hint'), Pot(88, 10, PotItem.Heart, 'Ice Stalfos Hint'), Pot(92, 10, PotItem.SmallMagic, 'Ice Stalfos Hint')], diff --git a/README.md b/README.md index 30282883..0a389551 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ See https://alttpr.com/ for more details on the normal randomizer. ### Trackers & Guides -This is a very new mode of LTTPR so the tools and info is very limited. - There is an [OW Rando Cheat Sheet](https://zelda.codemann8.com/images/shared/ow-rando-reference-sheet.png) that shows all the transitions that exist and are candidates for shuffle. +This is a very new mode of LTTPR so the tools and info is very limited. +- There is an [OW Rando Cheat Sheet](https://zelda.codemann8.com/images/shared/ow-rando-reference-sheet.png) that shows all the transitions that exist and are candidates for shuffle. - There is OW tracking capability within the following trackers: - CodeTracker, an [EmoTracker](https://emotracker.net) package for LTTPR - [Community Tracker](https://alttptracker.dunka.net/) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0ccceb80..c0fae696 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,116 +1,19 @@ # New Features -## Maiden Hint for Theives Town Attic - -In crossed dungeon mode, if you bring the maiden to the boss room when the attic is not bombed (and thus no light in the room), she mentions the dungeon where you can find the cracked floor. - -## Shuffle Links House - -Links house can now be shuffled in different ER settings. It will be limited to the Light World (or Dark World in inverted) if Crossed or Insanity shuffle is not one. It it also limited if door shuffle settings allow the Sanctuary to be in the dark world. (This is prevent having no Light World spawn points in Open modes) This setting is ignored by standard mode. THe CLI parameter is --shufflelinks - -## OWG Glitch Logic - -Thanks to qadan, cheuer, & compiling - -## Pseudo Boots - -Thanks to Bonta. You can now start with pseudo boots that let you move fast, but have no other logical uses (bonking open things, hovering, etc) - -## Pendant/Crystal Indicator - -For accessibility, you now get a C or P indicator to the left of the magic bar on the HUD when instead a Crystal or Pendant. Requires ownership of the map of that dungeon for display. Thanks to kan. +Bomb Logic added as an option. This removes your ability to use bombs until you find a "bomb bag", a +10 Bomb Capacity item. It is accounted for in the logic, so you aren't expected to get items behind bomb walls until you have found the bomb capacity item. The upgrades are removed from the upgrade fairy as well. + +``` +--bomblogic +``` # Bug Fixes and Notes. - -* 0.4.0.12 - * ER Inverted fix for HC Ledge, and Aga Tower choosing Links House incorrectly - * Credits again - hopefully for good - * Incorporated music fixes for now (may revisit later) - * Secure random re-incorporated -* 0.4.0.11 - * Some minor base rom fixes - * Improved distribution of bombable/dashable doors -* 0.4.0.10 - * Renamed to pseudoboots - * Some release note updates -* 0.4.0.9 - * Fixes for stats and P/C indicator (thanks Kara) - * Swamp lobby fixes (thanks Catobat) - * Fix for --hints flag on CLI -* 0.4.0.8 - * Ganon jokes added for when silvers aren't available - * Some text updated (Blind jokes, uncle text) - * Fixed some enemizer Mystery settings - * Added a setting that's random enemy shuffle without Unkillable Thieves possible - * Fixed shop spoiler when money balancing/multiworld balancing - * Fixed a problem with insanity - * Fixed an issue with the credit stats specific to DR (e.g. collection rate total) - * More helpful error message when bps is missing? - * Minor generation issues involving enemizer and the link sprite - * Baserom updates (from Bonta, kan, qwertymodo, ardnaxelark) - * Boss icon on dungeon map (if you have a compass) - * Progressive bow sprite replacement - * Quickswap - consecutive special swaps - * Bonk Counter - * One mind - * MSU fix - * Chest turn tracking (not yet in credits) - * Damaged and magic stats in credits (gt bk removed) - * Fix for infinite bombs - * Pseudo boots option - * Always allowed medallions for swordless (no option yet) -* 0.4.0.7 - * Reduce flashing option added - * Sprite author credit added - * Ranged Crystal switch rules tweaked - * Baserom update: includes Credits Speedup, reduced flashing option, msu resume (but turned off by default) - * Create link sprite's zspr from local ROM and no longer attempts to download it from website - * Some minor bug fixes -* 0.4.0.6 - * Hints now default to off - * The maiden gives you a hint to the attic if you bring her to the unlit boss room - * Beemizer support and fix for shopsanity - * Capacity upgrades removed in hard/expert item difficulties - * Swamp Hub added to lobby shuffle with ugly cave entrance. - * TR Lava Escape added to lobby shuffle. - * Hyrule Main Lobby and Sanctuary can now have a more visible outside exit, and rugs modified to be fully clipped. -* 0.4.0.5 - * Insanity - less restrictions on exiting (all modes) - * Fix for simple bosses shuffle - * Fix for boss shuffle from Mystery.py - * Minor msu fade out bug (thanks codemann8) - * Other bug fixes (thanks Catobat) -* 0.4.0.4 - * Added --shufflelinks option - * Moved spawning as a bunny indoors to experimental - * Baserom bug fixes -* 0.4.0.3 - * Fixed a bug where Sanctuary could be chosen as a lobby for a DW dungeon in non-crossed ER modes -* 0.4.0.2 - * Fixed a bug where Defeat Ganon is not possible - * Fixed the item counter total - * Fixed the bunny state when starting out in Sanc in a dark world dungeon -* 0.4.0.1 - * Moved stonewall pre-opening to not happen in experimental - * Updated baserom - * Boss RNG perseved between files - * Vanilla prize pack fix - * Starting equipment fix - * Post-Aga world state option - * Code optimzation - * Bottle quickswap via double shoulder - * Credits update - * Accessibility option - * Sewer map/compass fix - * Fixed a standard bug where the exits to the ledge would be unavailable if the pyramid was pre-opened - * DR ASM optimization - * Removed Archery Game from Take-Any caves in inverted - * Fixed a problem with new YAML parser -* 0.4.0.0 - * Mystery yaml parser updated to a package maintained version (Thanks StructuralMike) - * Bomb-logic and extend crystal switch logic (Thanks StructuralMike) - * Fixed logic for moved locations in playthrough (Thanks compiling) - * OWG Glitch logic added +* 0.5.0.1 + * --bomblogic option added +* 0.5.0.0 + * Handles headered roms for enemizer (Thanks compiling) + * Warning added for earlier version of python (Thanks compiling) + * Minor logic issue for defeating Aga in standard (Thanks compiling) + * Fix for boss music in non-DR modes (Thanks codemann8) # Known Issues @@ -118,21 +21,4 @@ For accessibility, you now get a C or P indicator to the left of the magic bar o * Hints for items in shops can be misleading (ER) * Forfeit in Multiworld not granting all shop items * Potential keylocks in multi-entrance dungeons -* Incorrect vanilla key logic for Mire - -## Other Notes - -### Triforce Hunt Options - -Thanks to deathFouton! - ---triforce_pool and --triforce_goal added to the CLI. - -Also, to the Mystery.py he added the following options: -* triforce_goal_min -* triforce_goal_max -* triforce_pool_min -* triforce_pool_max -* triforce_min_difference - -See the example yaml file for demonstrated usage. \ No newline at end of file +* Incorrect vanilla key logic for Mire \ No newline at end of file diff --git a/Regions.py b/Regions.py index 1138c900..40d84a28 100644 --- a/Regions.py +++ b/Regions.py @@ -297,8 +297,10 @@ def create_regions(world, player): create_cave_region(player, 'Dark World Lumberjack Shop', 'a common shop', ['Dark Lumberjack Shop - Left', 'Dark Lumberjack Shop - Middle', 'Dark Lumberjack Shop - Right']), create_cave_region(player, 'Spike Cave', 'Spike Cave', ['Spike Cave']), create_cave_region(player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'), - create_cave_region(player, 'Hookshot Cave', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'], - ['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']), + create_cave_region(player, 'Hookshot Cave (Front)', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'], + ['Hookshot Cave Front to Middle', 'Hookshot Cave Front Exit']), + create_cave_region(player, 'Hookshot Cave (Back)', 'a connector', None, ['Hookshot Cave Back to Middle', 'Hookshot Cave Back Exit']), + create_cave_region(player, 'Hookshot Cave (Middle)', 'a connector', None, ['Hookshot Cave Middle to Back', 'Hookshot Cave Middle to Front']), create_cave_region(player, 'Superbunny Cave (Top)', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']), create_cave_region(player, 'Superbunny Cave (Bottom)', 'a connector', None, ['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']), create_cave_region(player, 'Cave Shop (Dark Death Mountain)', 'a common shop', ['Dark Death Mountain Shop - Left', 'Dark Death Mountain Shop - Middle', 'Dark Death Mountain Shop - Right']), diff --git a/Rom.py b/Rom.py index c4928594..7d2bc546 100644 --- a/Rom.py +++ b/Rom.py @@ -31,7 +31,7 @@ from OverworldShuffle import default_flute_connections, flute_data JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'd97c9a8977e73da261852d69a287abcc' +RANDOMIZERBASEHASH = 'b9914f4de5a16b9a8bb94363fec0ac15' class JsonRom(object): @@ -87,10 +87,12 @@ class LocalRom(object): self.name = name self.hash = hash self.orig_buffer = None + self.file = file + self.has_smc_header = False if not os.path.isfile(file): raise RuntimeError("Could not find valid local base rom for patching at expected path %s." % file) with open(file, 'rb') as stream: - self.buffer = read_rom(stream) + self.buffer, self.has_smc_header = read_rom(stream) if patch: self.patch_base_rom() self.orig_buffer = self.buffer.copy() @@ -188,9 +190,12 @@ def write_int32s(rom, startaddress, values): def read_rom(stream): "Reads rom into bytearray and strips off any smc header" buffer = bytearray(stream.read()) + has_smc_header = False if len(buffer)%0x400 == 0x200: buffer = buffer[0x200:] - return buffer + has_smc_header = True + return buffer, has_smc_header + def convert_char_to_credits(char): char_map = { @@ -205,8 +210,16 @@ def convert_char_to_credits(char): ":": (0xA3, 0xC3), "_": (0xA6, 0xC6)} return char_map[char] if char in char_map else (0x9F, 0x9F) -def patch_enemizer(world, player, rom, baserom_path, enemizercli, random_sprite_on_hit): - baserom_path = os.path.abspath(baserom_path) + +def patch_enemizer(world, player, rom, local_rom, enemizercli, random_sprite_on_hit): + baserom_path = os.path.abspath(local_rom.file) + unheadered_path = None + if local_rom.has_smc_header: + headered_path = baserom_path + unheadered_path = baserom_path = os.path.abspath(output_path('unheadered_rom.sfc')) + with open(headered_path, 'rb') as headered: + with open(baserom_path, 'wb') as unheadered: + unheadered.write(headered.read()[0x200:]) basepatch_path = os.path.abspath(local_path(os.path.join("data","base2current.json"))) enemizer_basepatch_path = os.path.join(os.path.dirname(enemizercli), "enemizerBasePatch.json") randopatch_path = os.path.abspath(output_path('enemizer_randopatch.json')) @@ -350,6 +363,12 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, random_sprite_ rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette) rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette) + if local_rom.has_smc_header: + try: + os.remove(unheadered_path) + except OSError: + pass + try: os.remove(randopatch_path) except OSError: @@ -635,12 +654,12 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(0x18004C, 0x01) # patch for allowing Frogsmith to enter multi-entrance caves # patches map data specific for OW Shuffle - inverted_buffer[0x03] = inverted_buffer[0x03] | 0x2 # convenient portal on WDM + #inverted_buffer[0x03] = inverted_buffer[0x03] | 0x2 # convenient portal on WDM inverted_buffer[0x1A] = inverted_buffer[0x1A] | 0x2 # rocks added to prevent OWG hardlock inverted_buffer[0x1B] = inverted_buffer[0x1B] | 0x2 # rocks added to prevent OWG hardlock inverted_buffer[0x22] = inverted_buffer[0x22] | 0x2 # rocks added to prevent OWG hardlock inverted_buffer[0x3F] = inverted_buffer[0x3F] | 0x2 # added C to terrain - inverted_buffer[0x43] = inverted_buffer[0x43] | 0x2 # convenient portal on WDDM + #inverted_buffer[0x43] = inverted_buffer[0x43] | 0x2 # convenient portal on WDDM inverted_buffer[0x5A] = inverted_buffer[0x5A] | 0x2 # rocks added to prevent OWG hardlock inverted_buffer[0x5B] = inverted_buffer[0x5B] | 0x2 # rocks added to prevent OWG hardlock inverted_buffer[0x62] = inverted_buffer[0x62] | 0x2 # rocks added to prevent OWG hardlock @@ -1068,9 +1087,13 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(0x178000 + i, random.randint(0, 255)) # shuffle prize packs - prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF, - 0xDC, 0xDC, 0xDC, 0xDD, 0xDC, 0xDC, 0xDE, 0xDC, 0xE1, 0xD8, 0xE1, 0xE2, 0xE1, 0xD8, 0xE1, 0xE2, 0xDF, 0xD9, 0xD8, 0xE1, 0xDF, 0xDC, 0xD9, 0xD8, - 0xD8, 0xE3, 0xE0, 0xDB, 0xDE, 0xD8, 0xDB, 0xE2, 0xD9, 0xDA, 0xDB, 0xD9, 0xDB, 0xD9, 0xDB] + pack_prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, + 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, + 0xE0, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF, + 0xDC, 0xDC, 0xDC, 0xDD, 0xDC, 0xDC, 0xDE, 0xDC, + 0xE1, 0xD8, 0xE1, 0xE2, 0xE1, 0xD8, 0xE1, 0xE2, + 0xDF, 0xD9, 0xD8, 0xE1, 0xDF, 0xDC, 0xD9, 0xD8, + 0xD8, 0xE3, 0xE0, 0xDB, 0xDE, 0xD8, 0xDB, 0xE2] dig_prizes = [0xB2, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD9, 0xD9, 0xD9, 0xD9, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDB, 0xDB, 0xDB, 0xDB, 0xDB, 0xDC, 0xDC, 0xDC, 0xDC, 0xDC, @@ -1082,52 +1105,49 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): def chunk(l,n): return [l[i:i+n] for i in range(0, len(l), n)] - # randomize last 7 slots - prizes [-7:] = random.sample(prizes, 7) - #shuffle order of 7 main packs - packs = chunk(prizes[:56], 8) + packs = chunk(pack_prizes, 8) random.shuffle(packs) - prizes[:56] = [drop for pack in packs for drop in pack] + pack_prizes = [drop for pack in packs for drop in pack] if world.difficulty_adjustments[player] in ['hard', 'expert']: prize_replacements = {0xE0: 0xDF, # Fairy -> heart 0xE3: 0xD8} # Big magic -> small magic - prizes = [prize_replacements.get(prize, prize) for prize in prizes] + pack_prizes = [prize_replacements.get(prize, prize) for prize in pack_prizes] dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes] if world.retro[player]: prize_replacements = {0xE1: 0xDA, # 5 Arrows -> Blue Rupee 0xE2: 0xDB} # 10 Arrows -> Red Rupee - prizes = [prize_replacements.get(prize, prize) for prize in prizes] + pack_prizes = [prize_replacements.get(prize, prize) for prize in pack_prizes] dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes] if world.swords[player] == "bombs": prize_replacements = {0xDC: 0xD9, # 1 Bomb -> Green Rupee 0xDD: 0xDA, # 3 Bombs -> Blue Rupee 0xDE: 0xDB} # 10 Bombs -> Red Rupee - prizes = [prize_replacements.get(prize, prize) for prize in prizes] + pack_prizes = [prize_replacements.get(prize, prize) for prize in pack_prizes] dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes] rom.write_bytes(0x180100, dig_prizes) # write tree pull prizes - rom.write_byte(0xEFBD4, prizes.pop()) - rom.write_byte(0xEFBD5, prizes.pop()) - rom.write_byte(0xEFBD6, prizes.pop()) + rom.write_byte(0xEFBD4, world.prizes[player]['pull'][0]) + rom.write_byte(0xEFBD5, world.prizes[player]['pull'][1]) + rom.write_byte(0xEFBD6, world.prizes[player]['pull'][2]) # rupee crab prizes - rom.write_byte(0x329C8, prizes.pop()) # first prize - rom.write_byte(0x329C4, prizes.pop()) # final prize + rom.write_byte(0x329C8, world.prizes[player]['crab'][0]) # first prize + rom.write_byte(0x329C4, world.prizes[player]['crab'][1]) # final prize # stunned enemy prize - rom.write_byte(0x37993, prizes.pop()) + rom.write_byte(0x37993, world.prizes[player]['stun']) # saved fish prize - rom.write_byte(0xE82CC, prizes.pop()) + rom.write_byte(0xE82CC, world.prizes[player]['fish']) # fill enemy prize packs - rom.write_bytes(0x37A78, prizes) + rom.write_bytes(0x37A78, pack_prizes) # set bonk prizes bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC, 0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79, 0xE3, 0xE3, @@ -1145,7 +1165,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x184000, [ # original_item, limit, replacement_item, filler 0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees - 0x51, 0x06, 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade + 0x51, 0x00 if world.bomblogic[player] else 0x06, 0x31 if world.bomblogic[player] else 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade. If bomblogic -> turns into Bombs (10) 0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade 0x58, 0x01, 0x36 if world.retro[player] else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode) 0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20 @@ -1326,7 +1346,10 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): equip[0x36C] = 0x18 equip[0x36D] = 0x18 equip[0x379] = 0x68 - starting_max_bombs = 10 + if world.bomblogic[player]: + starting_max_bombs = 0 + else: + starting_max_bombs = 10 starting_max_arrows = 30 startingstate = CollectionState(world) @@ -1650,7 +1673,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x180188, [0, 0, 10]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0, 0, 10]) # Mantle respawn refills (magic, bombs, arrows) bow_max, bow_small = 70, 10 - elif uncle_location.item is not None and uncle_location.item.name in ['Bombs (10)']: + elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bomblogic[player] else 'Bombs (10)']: rom.write_byte(0x18004E, 2) # Escape Fill (bombs) rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180188, [0, 3, 0]) # Zelda respawn refills (magic, bombs, arrows) diff --git a/Rules.py b/Rules.py index a45ee329..e0aaa08c 100644 --- a/Rules.py +++ b/Rules.py @@ -608,7 +608,8 @@ def global_rules(world, player): def bomb_rules(world, player): bonkable_doors = ['Two Brothers House Exit (West)', 'Two Brothers House Exit (East)'] # Technically this is incorrectly defined, but functionally the same as what is intended. bombable_doors = ['Ice Rod Cave', 'Light World Bomb Hut', 'Light World Death Mountain Shop', 'Mini Moldorm Cave', - 'Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)', 'Dark Lake Hylia Ledge Fairy', 'Hype Cave', 'Brewery'] + 'Hookshot Cave Back to Middle', 'Hookshot Cave Front to Middle', 'Hookshot Cave Middle to Front','Hookshot Cave Middle to Back', + 'Dark Lake Hylia Ledge Fairy', 'Hype Cave', 'Brewery'] for entrance in bonkable_doors: add_rule(world.get_entrance(entrance, player), lambda state: state.can_use_bombs(player) or state.has_Boots(player)) for entrance in bombable_doors: @@ -623,9 +624,10 @@ def bomb_rules(world, player): for location in bombable_items: add_rule(world.get_location(location, player), lambda state: state.can_use_bombs(player)) - cave_kill_locations = ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy'] + cave_kill_locations = ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', 'Spiral Cave'] for location in cave_kill_locations: add_rule(world.get_location(location, player), lambda state: state.can_kill_most_things(player) or state.can_use_bombs(player)) + add_rule(world.get_entrance('Spiral Cave (top to bottom)', player), lambda state: state.can_kill_most_things(player) or state.can_use_bombs(player)) paradox_switch_chests = ['Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle'] for location in paradox_switch_chests: @@ -1591,7 +1593,7 @@ def standard_rules(world, player): def bomb_escape_rule(): loc = world.get_location("Link's Uncle", player) - return loc.item and loc.item.name == 'Bombs (10)' + return loc.item and loc.item.name in ['Bomb Upgrade (+10)' if world.bomblogic[player] else 'Bombs (10)'] def standard_escape_rule(state): return state.can_kill_most_things(player) or bomb_escape_rule() @@ -2096,7 +2098,7 @@ def set_bunny_rules(world, player, inverted): # regions for the exits of multi-entrace caves/drops that bunny cannot pass # Note spiral cave may be technically passible, but it would be too absurd to require since OHKO mode is a thing. - bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave', + bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave (Middle)', 'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)'] bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree', 'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid', diff --git a/Tables.py b/Tables.py index 5365e69c..64464b9f 100644 --- a/Tables.py +++ b/Tables.py @@ -124,3 +124,18 @@ divisor_lookup = { # 0xf0: 0xb53, 0xf1: 0xb53, 0xf2: 0xba0, 0xf3: 0xba0, 0xf4: 0xba5, 0xf5: 0xba5, 0xf6: 0xbac, 0xf7: 0xbac, # 0xf8: 0xbac, 0xf9: 0xbba, 0xfa: 0xbc1, 0xfb: 0xbcc, 0xfc: 0xbd7, 0xfd: 0xbd7, 0xfe: 0xbba, 0xff: 0xbe3 # } + +prize_lookup = { + 0xd8: 'Small Magic Refill', + 0xd9: 'Rupee (1)', + 0xda: 'Rupees (5)', + 0xdb: 'Rupees (20)', + 0xdc: 'Bomb (1)', + 0xdd: 'Bombs (4)', + 0xde: 'Bombs (8)', + 0xdf: 'Heart', + 0xe0: 'Fairy', + 0xe1: 'Arrows (5)', + 0xe2: 'Arrows (10)', + 0xe3: 'Full Magic Refill' +} diff --git a/data/base2current.bps b/data/base2current.bps index 9e742fa0..cc13e078 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/mystery_example.yml b/mystery_example.yml index bf0987c6..b6b6f051 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -1,16 +1,16 @@ description: Example door rando weights - ow_shuffle: + overworld_shuffle: vanilla: 0 parallel: 2 full: 2 - ow_keepsimilar: + overworld_keepsimilar: on: 1 off: 1 - ow_swap: + overworld_swap: vanilla: 0 mixed: 2 crossed: 2 - ow_fluteshuffle: + flute_shuffle: vanilla: 0 balanced: 1 random: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 565f19d8..d7bf9410 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -287,6 +287,10 @@ "type": "bool", "help": "suppress" }, + "bomblogic": { + "action": "store_true", + "type": "bool" + }, "retro": { "action": "store_true", "type": "bool" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 27e3b38c..88d5f083 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -53,7 +53,8 @@ "enemizer.nothing.applied": "No Enemizer options will be applied until this is resolved.", "building.collection.spheres": "Building up collection spheres", "building.calculating.spheres": "Calculated sphere %i, containing %i of %i progress items.", - "building.final.spheres": "Calculated final sphere %i, containing %i of %i progress items." + "building.final.spheres": "Calculated final sphere %i, containing %i of %i progress items.", + "old.python.version": "Door Rando may have issues with python versions earlier than 3.7. Detected version: %s" }, "help": { "lang": [ "App Language, if available, defaults to English" ], @@ -303,6 +304,7 @@ "and a few other little things make this more like Zelda-1. (default: %(default)s)" ], "pseudoboots": [ " Players starts with pseudo boots that allow dashing but no item checks (default: %(default)s"], + "bomblogic": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ], "startinventory": [ "Specifies a list of items that will be in your starting inventory (separated by commas). (default: %(default)s)" ], "usestartinventory": [ "Toggle usage of Starting Inventory." ], "custom": [ "Not supported." ], diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 60f6acb1..ab0f0d45 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -207,6 +207,7 @@ "randomizer.item.hints": "Include Helpful Hints", "randomizer.item.retro": "Retro mode (universal keys)", "randomizer.item.pseudoboots": "Start with Pseudo Boots", + "randomizer.item.bomblogic": "Bomblogic", "randomizer.item.worldstate": "World State", "randomizer.item.worldstate.standard": "Standard", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 715c4871..c4dc9f05 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -1,6 +1,7 @@ { "checkboxes": { "retro": { "type": "checkbox" }, + "bomblogic": { "type": "checkbox" }, "shopsanity": { "type": "checkbox" }, "hints": { "type": "checkbox" diff --git a/source/classes/constants.py b/source/classes/constants.py index 9a50bb57..c983d98a 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -57,6 +57,7 @@ SETTINGSTOPROCESS = { "item": { "hints": "hints", "retro": "retro", + "bomblogic": "bomblogic", "shopsanity": "shopsanity", "pseudoboots": "pseudoboots", "worldstate": "mode",