diff --git a/Adjuster.py b/Adjuster.py index c6f42e6e..a6e964a8 100755 --- a/Adjuster.py +++ b/Adjuster.py @@ -36,6 +36,7 @@ def main(): parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout']) parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout']) parser.add_argument('--reduce_flashing', help='Reduce some in-game flashing.', action='store_true') + parser.add_argument('--shuffle_sfx', help='Shuffles sound sfx', action='store_true') parser.add_argument('--sprite', help='''\ Path to a sprite sheet to use for Link. Needs to be in binary format and have a length of 0x7000 (28672) bytes, diff --git a/AdjusterMain.py b/AdjusterMain.py index bc463444..7d7e2f6e 100644 --- a/AdjusterMain.py +++ b/AdjusterMain.py @@ -25,7 +25,7 @@ def adjust(args): args.sprite = None apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, - args.sprite, args.ow_palettes, args.uw_palettes, args.reduce_flashing) + args.sprite, args.ow_palettes, args.uw_palettes, args.reduce_flashing, args.shuffle_sfx) output_path.cached_path = args.outputpath rom.write_to_file(output_path('%s.sfc' % outfilebase)) diff --git a/BaseClasses.py b/BaseClasses.py index 01179702..b9f33267 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -114,6 +114,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') @@ -1031,8 +1032,7 @@ class CollectionState(object): # In the future, this can be used to check if the player starts without bombs def can_use_bombs(self, player): - StartingBombs = True - return StartingBombs or self.has('Bomb Upgrade (+10)', player) + return (not self.world.bomblogic[player] or self.has('Bomb Upgrade (+10)', player)) def can_hit_crystal(self, player): return (self.can_use_bombs(player) @@ -1064,6 +1064,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.has_sword(player) and self.has('Quake', player))) and @@ -2372,6 +2373,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, 'shuffle': self.world.shuffle, @@ -2470,6 +2472,7 @@ class Spoiler(object): outfile.write('Experimental: %s\n' % ('Yes' if self.metadata['experimental'][player] else 'No')) outfile.write('Key Drops shuffled: %s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) outfile.write(f"Shopsanity: {'Yes' if self.metadata['shopsanity'][player] else 'No'}\n") + outfile.write('Bomblogic: %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/Bosses.py b/Bosses.py index ebf8b4dd..253717bd 100644 --- a/Bosses.py +++ b/Bosses.py @@ -1,5 +1,5 @@ import logging -import random +import RaceRandom as random from BaseClasses import Boss from Fill import FillError diff --git a/CLI.py b/CLI.py index d87e1e2c..911f03ab 100644 --- a/CLI.py +++ b/CLI.py @@ -96,13 +96,14 @@ def parse_cli(argv, no_defaults=False): for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', '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', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'remote_items', 'shopsanity', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', - 'reduce_flashing']: + 'reduce_flashing', 'shuffle_sfx']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -126,6 +127,7 @@ def parse_settings(): settings = { "lang": "en", "retro": False, + "bomblogic": False, "mode": "open", "logic": "noglitches", "goal": "ganon", @@ -190,6 +192,7 @@ def parse_settings(): "ow_palettes": "default", "uw_palettes": "default", "reduce_flashing": False, + "shuffle_sfx": False, # Spoiler defaults to TRUE # Playthrough defaults to TRUE diff --git a/DoorShuffle.py b/DoorShuffle.py index 89e7a6d6..d5b69126 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1,4 +1,4 @@ -import random +import RaceRandom as random from collections import defaultdict, deque import logging import time @@ -99,8 +99,8 @@ def link_doors_main(world, player): analyze_portals(world, player) for portal in world.dungeon_portals[player]: connect_portal(portal, world, player) - - fix_big_key_doors_with_ugly_smalls(world, player) + if not world.doorShuffle[player] == 'vanilla': + fix_big_key_doors_with_ugly_smalls(world, player) if world.doorShuffle[player] == 'vanilla': for entrance, ext in open_edges: connect_two_way(world, entrance, ext, player) @@ -1658,7 +1658,7 @@ def change_door_to_small_key(d, world, player): def smooth_door_pairs(world, player): all_doors = [x for x in world.doors if x.player == player] skip = set() - bd_candidates, dashable_counts, bombable_counts = defaultdict(list), defaultdict(int), defaultdict(int) + bd_candidates = defaultdict(list) for door in all_doors: if door.type in [DoorType.Normal, DoorType.Interior] and door not in skip and not door.entranceFlag: partner = door.dest @@ -1684,30 +1684,19 @@ def smooth_door_pairs(world, player): remove_pair(door, world, player) if type_b == DoorKind.SmallKey: remove_pair(door, world, player) - elif type_a in [DoorKind.Bombable, DoorKind.Dashable] or type_b in [DoorKind.Bombable, DoorKind.Dashable]: + else: if valid_pair: - new_type = type_a - if type_a != type_b: - new_type = DoorKind.Dashable if type_a == DoorKind.Dashable or type_b == DoorKind.Dashable else DoorKind.Bombable - if type_a != new_type: - room_a.change(door.doorListPos, new_type) - if type_b != new_type: - room_b.change(partner.doorListPos, new_type) - add_pair(door, partner, world, player) - spoiler_type = 'Bomb Door' if new_type == DoorKind.Bombable else 'Dash Door' - world.spoiler.set_door_type(door.name + ' <-> ' + partner.name, spoiler_type, player) - counter = bombable_counts if new_type == DoorKind.Bombable else dashable_counts - counter[door.entrance.parent_region.dungeon] += 1 - else: + bd_candidates[door.entrance.parent_region.dungeon].append(door) + elif type_a in [DoorKind.Bombable, DoorKind.Dashable] or type_b in [DoorKind.Bombable, DoorKind.Dashable]: if type_a in [DoorKind.Bombable, DoorKind.Dashable]: room_a.change(door.doorListPos, DoorKind.Normal) remove_pair(door, world, player) - elif type_b in [DoorKind.Bombable, DoorKind.Dashable]: + else: room_b.change(partner.doorListPos, DoorKind.Normal) remove_pair(partner, world, player) elif valid_pair and type_a != DoorKind.SmallKey and type_b != DoorKind.SmallKey: bd_candidates[door.entrance.parent_region.dungeon].append(door) - shuffle_bombable_dashable(bd_candidates, bombable_counts, dashable_counts, world, player) + shuffle_bombable_dashable(bd_candidates, world, player) world.paired_doors[player] = [x for x in world.paired_doors[player] if x.pair or x.original] @@ -1744,15 +1733,15 @@ def stateful_door(door, kind): return False -def shuffle_bombable_dashable(bd_candidates, bombable_counts, dashable_counts, world, player): +def shuffle_bombable_dashable(bd_candidates, world, player): if world.doorShuffle[player] == 'basic': for dungeon, candidates in bd_candidates.items(): - diff = bomb_dash_counts[dungeon.name][1] - dashable_counts[dungeon] + diff = bomb_dash_counts[dungeon.name][1] if diff > 0: for chosen in random.sample(candidates, min(diff, len(candidates))): change_pair_type(chosen, DoorKind.Dashable, world, player) candidates.remove(chosen) - diff = bomb_dash_counts[dungeon.name][0] - bombable_counts[dungeon] + diff = bomb_dash_counts[dungeon.name][0] if diff > 0: for chosen in random.sample(candidates, min(diff, len(candidates))): change_pair_type(chosen, DoorKind.Bombable, world, player) @@ -1761,16 +1750,12 @@ def shuffle_bombable_dashable(bd_candidates, bombable_counts, dashable_counts, w remove_pair_type_if_present(excluded, world, player) elif world.doorShuffle[player] == 'crossed': all_candidates = sum(bd_candidates.values(), []) - all_bomb_counts = sum(bombable_counts.values()) - all_dash_counts = sum(dashable_counts.values()) - if all_dash_counts < 8: - for chosen in random.sample(all_candidates, min(8 - all_dash_counts, len(all_candidates))): - change_pair_type(chosen, DoorKind.Dashable, world, player) - all_candidates.remove(chosen) - if all_bomb_counts < 12: - for chosen in random.sample(all_candidates, min(12 - all_bomb_counts, len(all_candidates))): - change_pair_type(chosen, DoorKind.Bombable, world, player) - all_candidates.remove(chosen) + for chosen in random.sample(all_candidates, min(8, len(all_candidates))): + change_pair_type(chosen, DoorKind.Dashable, world, player) + all_candidates.remove(chosen) + for chosen in random.sample(all_candidates, min(12, len(all_candidates))): + change_pair_type(chosen, DoorKind.Bombable, world, player) + all_candidates.remove(chosen) for excluded in all_candidates: remove_pair_type_if_present(excluded, world, player) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 47922be5..5174762e 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1,4 +1,4 @@ -import random +import RaceRandom as random import collections import itertools from collections import defaultdict, deque diff --git a/DungeonRandomizer.py b/DungeonRandomizer.py index b1c02c81..d24e81d6 100755 --- a/DungeonRandomizer.py +++ b/DungeonRandomizer.py @@ -3,7 +3,7 @@ import argparse import copy import os import logging -import random +import RaceRandom as random import textwrap import shlex import sys diff --git a/Dungeons.py b/Dungeons.py index e3b76c28..1f38e551 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -1,4 +1,4 @@ -import random +import RaceRandom as random from BaseClasses import Dungeon from Bosses import BossFactory diff --git a/EntranceShuffle.py b/EntranceShuffle.py index cd8df010..1c1a6ab8 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1,4 +1,4 @@ -import random +import RaceRandom as random # ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave. from collections import defaultdict @@ -2373,7 +2373,7 @@ 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)']] + ['Hookshot Cave Back Exit', 'Hookshot Cave Front Exit']] Cave_Exits_Base += [('Superbunny Cave Exit (Bottom)', 'Superbunny Cave Exit (Top)'), ('Spiral Cave Exit (Top)', 'Spiral Cave Exit')] @@ -3115,6 +3115,10 @@ mandatory_connections = [('Links House S&Q', 'Links House'), ('Dark Death Mountain Drop (West)', 'Dark Death Mountain (West Bottom)'), ('East Death Mountain (Top) Mirror Spot', 'East Death Mountain (Top)'), ('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)'), ('Turtle Rock Teleporter', 'Turtle Rock (Top)'), ('Turtle Rock Drop', 'Dark Death Mountain (Top)'), ('Floating Island Drop', 'Dark Death Mountain (Top)'), @@ -3233,6 +3237,10 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'), ('Turtle Rock Tail Drop', 'Turtle Rock (Top)'), ('Turtle Rock Drop', 'Dark Death Mountain'), ('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)'), ('Desert Ledge Drop', 'Light World'), ('Floating Island Drop', 'Dark Death Mountain'), ('Dark Lake Hylia Central Island Teleporter', 'Lake Hylia Central Island'), @@ -3428,16 +3436,16 @@ default_connections = [('Links House', 'Links House'), ('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)', 'Dark Death Mountain (Top)'), ('Superbunny Cave Exit (Bottom)', 'Dark Death Mountain (East Bottom)'), - ('Hookshot Cave Exit (South)', 'Dark Death Mountain (Top)'), - ('Hookshot Cave Exit (North)', 'Death Mountain Floating Island (Dark World)'), - ('Hookshot Cave Back Entrance', 'Hookshot Cave'), + ('Hookshot Cave Front Exit', 'Dark Death Mountain (Top)'), + ('Hookshot Cave Back Exit', 'Death Mountain Floating Island (Dark World)'), + ('Hookshot Cave Back Entrance', 'Hookshot Cave (Back)'), ('Mimic Cave', 'Mimic Cave'), ('Pyramid Hole', 'Pyramid'), @@ -3562,13 +3570,13 @@ inverted_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)'), ('Superbunny Cave (Bottom)', 'Superbunny Cave (Bottom)'), ('Superbunny Cave Exit (Bottom)', 'Dark Death Mountain (East Bottom)'), - ('Hookshot Cave Exit (North)', 'Death Mountain Floating Island (Dark World)'), - ('Hookshot Cave Back Entrance', 'Hookshot Cave'), + ('Hookshot Cave Back Exit', 'Death Mountain Floating Island (Dark World)'), + ('Hookshot Cave Back Entrance', 'Hookshot Cave (Back)'), ('Mimic Cave', 'Mimic Cave'), ('Inverted Pyramid Hole', 'Pyramid'), ('Inverted Links House', 'Inverted Links House'), @@ -3589,7 +3597,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing' ('Death Mountain Return Cave (East)', 'Death Mountain Return Cave'), ('Death Mountain Return Cave Exit (West)', 'Death Mountain'), ('Death Mountain Return Cave Exit (East)', 'Death Mountain'), - ('Hookshot Cave Exit (South)', 'Dark Death Mountain'), + ('Hookshot Cave Front Exit', 'Dark Death Mountain'), ('Superbunny Cave Exit (Top)', 'Dark Death Mountain'), ('Pyramid Exit', 'Light World'), ('Inverted Pyramid Entrance', 'Bottom of Pyramid')] @@ -3937,8 +3945,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), 'Inverted Ganons Tower Exit': (0x37, 0x38), 'Pyramid Exit': (0x36, 0x37), diff --git a/Fill.py b/Fill.py index d75cf080..a6ef6cf4 100644 --- a/Fill.py +++ b/Fill.py @@ -1,4 +1,4 @@ -import random +import RaceRandom as random import logging from BaseClasses import CollectionState diff --git a/Gui.py b/Gui.py index 4cc5b106..55defa7a 100755 --- a/Gui.py +++ b/Gui.py @@ -24,6 +24,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): @@ -188,6 +195,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/InvertedRegions.py b/InvertedRegions.py index d935ed28..589f6f87 100644 --- a/InvertedRegions.py +++ b/InvertedRegions.py @@ -199,8 +199,11 @@ def create_inverted_regions(world, player): 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, 'Spike Cave', 'Spike Cave', ['Spike Cave']), - 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_dw_region(player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance']), create_cave_region(player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']), diff --git a/ItemList.py b/ItemList.py index 62648862..e79392b8 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1,7 +1,7 @@ from collections import namedtuple import logging import math -import random +import RaceRandom as random from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState from Bosses import place_bosses @@ -37,7 +37,7 @@ Difficulty = namedtuple('Difficulty', ['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield', 'basicshield', 'progressivearmor', 'basicarmor', 'swordless', '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']) @@ -61,6 +61,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, @@ -86,6 +87,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, @@ -111,6 +113,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, @@ -251,10 +254,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.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.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] @@ -284,7 +287,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) @@ -315,6 +318,11 @@ def generate_itempool(world, player): p_item = next(item for item in items if item.name == potion and item.player == player) p_item.priority = True # don't beemize one of each potion + if world.bomblogic[player]: + for item in items: + if item.name == 'Bomb Upgrade (+10)' and item.player == player: + item.advancement = True + world.lamps_needed_for_dark_rooms = lamps_needed_for_dark_rooms if clock_mode is not None: @@ -520,6 +528,15 @@ def set_up_shops(world, player): rss.locked = True cap_shop = world.get_region('Capacity Upgrade', player).shop cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro + if world.bomblogic[player]: + 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): @@ -561,7 +578,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)']: @@ -709,7 +726,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, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, door_shuffle, logic): +def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bomblogic, door_shuffle, logic): pool = [] placed_items = {} precollected_items = [] @@ -756,6 +773,11 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, 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: @@ -850,7 +872,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, pool.extend(['Small Key (Universal)']) 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 = [] @@ -966,20 +988,21 @@ def test(): for shuffle in ['full', 'insanity_legacy']: for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']: for retro in [True, False]: - for door_shuffle in ['basic', 'crossed', 'vanilla']: - out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, door_shuffle, logic) - count = len(out[0]) + len(out[1]) + for bomblogic in [True, False]: + for door_shuffle in ['basic', 'crossed', 'vanilla']: + out = get_pool_core(progressive, 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 - if goal == 'pedestal' and swords != 'vanilla': - # pedestal goals generate one extra item - correct_count += 1 - 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)) - except AssertionError as e: - print(e) + correct_count = total_items_to_place + if goal == 'pedestal' and swords != 'vanilla': + # pedestal goals generate one extra item + correct_count += 1 + 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, bomblogic)) + except AssertionError as e: + print(e) if __name__ == '__main__': test() diff --git a/Main.py b/Main.py index 0bf1e50a..b43d099a 100644 --- a/Main.py +++ b/Main.py @@ -4,7 +4,8 @@ from itertools import zip_longest import json import logging import os -import random +import RaceRandom as random +import string import time import zlib @@ -27,22 +28,32 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.4.1.0-u' +__version__ = '0.5.1.0-u' + +from source.classes.BabelFish import BabelFish class EnemizerError(RuntimeError): pass +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 start = time.perf_counter() - # if args.securerandom: - # random.use_secure() + if args.securerandom: + random.use_secure() # initialize the world if args.code: @@ -61,13 +72,14 @@ def main(args, seed=None, fish=None): random.seed(world.seed) if args.securerandom: - world.seed = None + world.seed = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(9)) world.remote_items = args.remote_items.copy() world.mapshuffle = args.mapshuffle.copy() 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() @@ -257,11 +269,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) @@ -281,7 +293,8 @@ def main(args, seed=None, fish=None): apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player], - args.ow_palettes[player], args.uw_palettes[player], args.reduce_flashing[player]) + args.ow_palettes[player], args.uw_palettes[player], args.reduce_flashing[player], + args.shuffle_sfx[player]) if args.jsonout: jsonout[f'patch_t{team}_p{player}'] = rom.patches @@ -331,7 +344,7 @@ def main(args, seed=None, fish=None): logger.info(world.fish.translate("cli","cli","made.playthrough") % (YES if (args.calc_playthrough) else NO)) logger.info(world.fish.translate("cli","cli","made.spoiler") % (YES if (not args.jsonout and args.create_spoiler) else NO)) logger.info(world.fish.translate("cli","cli","used.enemizer") % (YES if enemized else NO)) - logger.info(world.fish.translate("cli","cli","seed") + ": %d", world.seed) + logger.info(world.fish.translate("cli","cli","seed") + ": %s", world.seed) logger.info(world.fish.translate("cli","cli","total.time"), time.perf_counter() - start) # print_wiki_doors_by_room(dungeon_regions,world,1) @@ -371,6 +384,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() diff --git a/Mystery.py b/Mystery.py index f82e64d2..306b5c27 100644 --- a/Mystery.py +++ b/Mystery.py @@ -1,6 +1,6 @@ import argparse import logging -import random +import RaceRandom as random import urllib.request import urllib.parse import yaml @@ -176,6 +176,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', @@ -226,11 +228,13 @@ 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) ret.ow_palettes = get_choice('ow_palettes', romweights) ret.uw_palettes = get_choice('uw_palettes', romweights) + ret.uw_palettes = get_choice('shuffle_sfx', romweights) == 'on' return ret diff --git a/Plando.py b/Plando.py index 5b66876e..c32c75cd 100755 --- a/Plando.py +++ b/Plando.py @@ -3,7 +3,7 @@ import argparse import hashlib import logging import os -import random +import RaceRandom as random import time import sys diff --git a/PotShuffle.py b/PotShuffle.py index e8e6d3ce..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')], @@ -275,7 +275,7 @@ vanilla_pots = { def shuffle_pots(world, player): - import random + import RaceRandom as random new_pot_contents = {} diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d55904b8..b6be375d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,108 +1,29 @@ # New Features -## Maiden Hint for Theives Town Attic +## Shuffle SFX -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. +Shuffles a large portion of the sounds effects. Can be used with the adjuster. -## Shuffle Links House +CLI: ```--shuffle_sfx``` + +## Bomb Logic -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 +When enabling this option, you do not start with bomb capacity but rather you must find 1 of 2 bomb bags. (They are represented by the +10 capacity item.) Bomb capacity upgrades are otherwise unavailable. + +CLI: ```--bomblogic``` -## 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. # Bug Fixes and Notes. -* 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.2 + * --shuffle_sfx option 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 @@ -110,21 +31,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 b2af454a..35a7eda3 100644 --- a/Regions.py +++ b/Regions.py @@ -190,8 +190,11 @@ def create_regions(world, player): 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, 'Spike Cave', 'Spike Cave', ['Spike Cave']), - 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_dw_region(player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance', 'Floating Island Mirror Spot']), create_lw_region(player, 'Death Mountain Floating Island (Light World)', ['Floating Island']), create_dw_region(player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']), diff --git a/Rom.py b/Rom.py index 6c351828..8d86a16e 100644 --- a/Rom.py +++ b/Rom.py @@ -5,7 +5,7 @@ import json import hashlib import logging import os -import random +import RaceRandom as random import struct import sys import subprocess @@ -28,9 +28,11 @@ from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_ from Items import ItemFactory from EntranceShuffle import door_addresses, exit_ids +from source.classes.SFX import randomize_sfx + JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'df3386b7a48d79950a1432b8bbaafde1' +RANDOMIZERBASEHASH = '988f1546b14d8f2e6ee30b9de44882da' class JsonRom(object): @@ -86,10 +88,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() @@ -187,12 +191,21 @@ 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 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')) @@ -336,6 +349,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: @@ -807,17 +826,17 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): write_int16(rom, 0x187010, credits_total) # dynamic credits if credits_total != 216: - # collection rate address: - cr_address = 0x2391F2 + # collection rate address (hi): + cr_address = 0x238057 cr_pc = cr_address - 0x120000 # convert to pc mid_top, mid_bot = credits_digit((credits_total // 10) % 10) last_top, last_bot = credits_digit(credits_total % 10) # top half - rom.write_byte(cr_pc+0x1c, mid_top) - rom.write_byte(cr_pc+0x1d, last_top) + rom.write_byte(cr_pc+0x1, mid_top) + rom.write_byte(cr_pc+0x2, last_top) # bottom half - rom.write_byte(cr_pc+0x3a, mid_bot) - rom.write_byte(cr_pc+0x3b, last_bot) + rom.write_byte(cr_pc+0x1f, mid_bot) + rom.write_byte(cr_pc+0x20, last_bot) # patch medallion requirements if world.required_medallions[player][0] == 'Bombos': @@ -1032,7 +1051,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 @@ -1169,7 +1188,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) @@ -1461,7 +1483,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) @@ -1529,8 +1551,9 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # set rom name # 21 bytes from Main import __version__ + seedstring = f'{world.seed:09}' if isinstance(world.seed, int) else world.seed # todo: change to DR when Enemizer is okay with DR - rom.name = bytearray(f'ER{__version__.split("-")[0].replace(".","")[0:3]}_{team+1}_{player}_{world.seed:09}\0', 'utf8')[:21] + rom.name = bytearray(f'ER{__version__.split("-")[0].replace(".","")[0:3]}_{team+1}_{player}_{seedstring}\0', 'utf8')[:21] rom.name.extend([0] * (21 - len(rom.name))) rom.write_bytes(0x7FC0, rom.name) @@ -1624,7 +1647,7 @@ def hud_format_text(text): def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, - ow_palettes, uw_palettes, reduce_flashing): + ow_palettes, uw_palettes, reduce_flashing, shuffle_sfx): if not os.path.exists("data/sprites/official/001.link.1.zspr") and rom.orig_buffer: dump_zspr(rom.orig_buffer[0x80000:0x87000], rom.orig_buffer[0xdd308:0xdd380], @@ -1727,6 +1750,9 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr elif uw_palettes == 'blackout': blackout_uw_palettes(rom) + if shuffle_sfx: + randomize_sfx(rom) + if isinstance(rom, LocalRom): rom.write_crc() diff --git a/Rules.py b/Rules.py index 3cd18b2b..c8404ff5 100644 --- a/Rules.py +++ b/Rules.py @@ -561,7 +561,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: @@ -576,9 +577,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: @@ -1149,7 +1151,7 @@ def standard_rules(world, player): set_rule(entrance, lambda state: state.has('Zelda Delivered', player)) set_rule(world.get_entrance('Sanctuary Exit', player), lambda state: state.has('Zelda Delivered', player)) # zelda should be saved before agahnim is in play - set_rule(world.get_location('Agahnim 1', player), lambda state: state.has('Zelda Delivered', player)) + add_rule(world.get_location('Agahnim 1', player), lambda state: state.has('Zelda Delivered', player)) # too restrictive for crossed? def uncle_item_rule(item): @@ -1160,7 +1162,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() @@ -1671,7 +1673,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/asm/doortables.asm b/asm/doortables.asm index db1b6711..8ee0dd60 100644 --- a/asm/doortables.asm +++ b/asm/doortables.asm @@ -580,8 +580,8 @@ dw $00bc, $00a2, $00a3, $00c2, $001a, $0049, $0014, $008c ; Ice Many Pots, Swamp Waterfall, GT Gauntlet 3, Eastern Push Block, Eastern Courtyard, Eastern Map Valley ; Eastern Cannonball, HC East Hall dw $009f, $0066, $005d, $00a8, $00a9, $00aa, $00b9, $0052 -; HC West Hall, TR Dash Bridge, TR Hub, Pod Arena, GT Petting Zoo -dw $0050, $00c5, $00c6, $0009, $0003, $002a, $007d +; HC West Hall, TR Dash Bridge, TR Hub, Pod Arena, GT Petting Zoo, Ice Spike Cross +dw $0050, $00c5, $00c6, $0009, $0003, $002a, $007d, $005e dw $ffff ; dungeon tables diff --git a/asm/drhooks.asm b/asm/drhooks.asm index 2824771e..86ae1ff1 100644 --- a/asm/drhooks.asm +++ b/asm/drhooks.asm @@ -179,6 +179,17 @@ JSL BlindsAtticHint : NOP #2 org $1cfd69 Main_ShowTextMessage: +; Conditionally disable UW music changes in Door Rando +org $028ADB ; <- Bank02.asm:2088-2095 (LDX.b #$14 : LDA $A0 ...) +JSL.l Underworld_DoorDown_Entry : CPX #$10 +db $B0, $21 ; BCS $028B04 +BRA + : NOP #6 : + + +org $02C3F2 ; <- Bank02.asm:10521 Unused call +Underworld_DoorDown_Call: +org $02C3F3 +dw $8AD9 ; address of Bank02.asm:2085 + ; These two, if enabled together, have implications for vanilla BK doors in IP/Hera/Mire ; IPBJ is common enough to consider not doing this. Mire is not a concern for vanilla - maybe glitched modes ; Hera BK door back can be seen with Pot clipping - likely useful for no logic seeds diff --git a/asm/normal.asm b/asm/normal.asm index a8ab9597..aabb24de 100644 --- a/asm/normal.asm +++ b/asm/normal.asm @@ -398,7 +398,7 @@ StraightStairsTrapDoor: .animateTraps lda #$05 : sta $11 inc $0468 : stz $068e : stz $0690 - ++ rtl + ++ JSL Underworld_DoorDown_Call : rtl + JML Dungeon_ApproachFixedColor ; what we wrote over } diff --git a/data/base2current.bps b/data/base2current.bps index 2ef0bf7d..5f411710 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/mystery_example.yml b/mystery_example.yml index e349063d..028e7110 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -132,3 +132,6 @@ half: 0 quarter: 1 off: 0 + shuffle_sfx: + on: 1 + off: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 47bb3987..f749e20a 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -199,6 +199,10 @@ "action": "store_true", "type": "bool" }, + "shuffle_sfx": { + "action": "store_true", + "type": "bool" + }, "mapshuffle": { "action": "store_true", "type": "bool" @@ -220,6 +224,10 @@ "type": "bool", "help": "suppress" }, + "bomblogic": { + "action": "store_true", + "type": "bool" + }, "retro": { "action": "store_true", "type": "bool" diff --git a/resources/app/cli/lang/de.json b/resources/app/cli/lang/de.json index d2c5d523..6a72d688 100644 --- a/resources/app/cli/lang/de.json +++ b/resources/app/cli/lang/de.json @@ -1,6 +1,6 @@ { "cli": { - "app.title": "ALttP Tür Randomisier Version %s - Nummer: %d, Code: %s", + "app.title": "ALttP Tür Randomisier Version %s - Nummer: %s, Code: %s", "shuffling.world": "Welt wird durchmischt.", "generating.itempool": "Generier Gegenstandsbasis.", "calc.access.rules": "Berechne Zugriffsregeln.", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 373c69cc..f6cdf23e 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -2,7 +2,7 @@ "cli": { "yes": "Yes", "no": "No", - "app.title": "ALttP Door Randomizer Version %s : --seed %d --code %s", + "app.title": "ALttP Door Randomizer Version %s : --seed %s --code %s", "version": "Version", "seed": "Seed", "player": "Player", @@ -52,7 +52,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" ], @@ -263,6 +264,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." ], @@ -295,6 +297,7 @@ "sprite that will be extracted." ], "reduce_flashing": [ "Reduce some in-game flashing (default: %(default)s)" ], + "shuffle_sfx": [ "Shuffle sounds effects (default: %(default)s)" ], "create_rom": [ "Create an output rom file. (default: %(default)s)" ], "gui": [ "Launch the GUI. (default: %(default)s)" ], "jsonout": [ diff --git a/resources/app/cli/lang/es.json b/resources/app/cli/lang/es.json index def18849..8bff9dd0 100644 --- a/resources/app/cli/lang/es.json +++ b/resources/app/cli/lang/es.json @@ -1,6 +1,6 @@ { "cli": { - "app.title": "ALttP Puerta Aleatorizador Versión %s - Número: %d, Código: %s", + "app.title": "ALttP Puerta Aleatorizador Versión %s - Número: %s, Código: %s", "player": "Jugador", "shuffling.world": "Barajando el Mundo", "shuffling.dungeons": "Barajando Mazmorras", diff --git a/resources/app/gui/adjust/overview/widgets.json b/resources/app/gui/adjust/overview/widgets.json index b61fff0e..85efcf1f 100644 --- a/resources/app/gui/adjust/overview/widgets.json +++ b/resources/app/gui/adjust/overview/widgets.json @@ -2,7 +2,8 @@ "checkboxes": { "nobgm": { "type": "checkbox" }, "quickswap": { "type": "checkbox" }, - "reduce_flashing": {"type": "checkbox"} + "reduce_flashing": {"type": "checkbox"}, + "shuffle_sfx": {"type": "checkbox"} }, "leftAdjustFrame": { "heartcolor": { diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 0d9e3836..fe7fa9d6 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -3,6 +3,7 @@ "adjust.nobgm": "Disable Music & MSU-1", "adjust.quickswap": "L/R Quickswapping", "adjust.reduce_flashing": "Reduce Flashing", + "adjust.shuffle_sfx": "Shuffle Sound Effects", "adjust.heartcolor": "Heart Color", "adjust.heartcolor.red": "Red", @@ -134,6 +135,7 @@ "randomizer.gameoptions.nobgm": "Disable Music & MSU-1", "randomizer.gameoptions.quickswap": "L/R Quickswapping", "randomizer.gameoptions.reduce_flashing": "Reduce Flashing", + "randomizer.gameoptions.shuffle_sfx": "Shuffle Sound Effects", "randomizer.gameoptions.heartcolor": "Heart Color", "randomizer.gameoptions.heartcolor.red": "Red", @@ -190,6 +192,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/gameoptions/widgets.json b/resources/app/gui/randomize/gameoptions/widgets.json index 63556e0f..6efe32c8 100644 --- a/resources/app/gui/randomize/gameoptions/widgets.json +++ b/resources/app/gui/randomize/gameoptions/widgets.json @@ -2,7 +2,8 @@ "checkboxes": { "nobgm": { "type": "checkbox" }, "quickswap": { "type": "checkbox" }, - "reduce_flashing": {"type": "checkbox"} + "reduce_flashing": {"type": "checkbox"}, + "shuffle_sfx": {"type": "checkbox"} }, "leftRomOptionsFrame": { "heartcolor": { diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index a6f10a14..15d049c8 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/SFX.py b/source/classes/SFX.py new file mode 100644 index 00000000..ed6d93f6 --- /dev/null +++ b/source/classes/SFX.py @@ -0,0 +1,191 @@ +import random +from Utils import int16_as_bytes + + +class SFX(object): + + def __init__(self, name, sfx_set, orig_id, addr, chain, accomp=False): + self.name = name + self.sfx_set = sfx_set + self.orig_id = orig_id + self.addr = addr + self.chain = chain + self.accomp = accomp + + self.target_set = None + self.target_id = None + self.target_chain = None + + +def init_sfx_data(): + sfx_pool = [SFX('Slash1', 0x02, 0x01, 0x2614, []), SFX('Slash2', 0x02, 0x02, 0x2625, []), + SFX('Slash3', 0x02, 0x03, 0x2634, []), SFX('Slash4', 0x02, 0x04, 0x2643, []), + SFX('Wall clink', 0x02, 0x05, 0x25DD, []), SFX('Bombable door clink', 0x02, 0x06, 0x25D7, []), + SFX('Fwoosh shooting', 0x02, 0x07, 0x25B7, []), SFX('Arrow hitting wall', 0x02, 0x08, 0x25E3, []), + SFX('Boomerang whooshing', 0x02, 0x09, 0x25AD, []), SFX('Hookshot', 0x02, 0x0A, 0x25C7, []), + SFX('Placing bomb', 0x02, 0x0B, 0x2478, []), + SFX('Bomb exploding/Quake/Bombos/Exploding wall', 0x02, 0x0C, 0x269C, []), + SFX('Powder', 0x02, 0x0D, 0x2414, [0x3f]), SFX('Fire rod shot', 0x02, 0x0E, 0x2404, []), + SFX('Ice rod shot', 0x02, 0x0F, 0x24C3, []), SFX('Hammer use', 0x02, 0x10, 0x23FA, []), + SFX('Hammering peg', 0x02, 0x11, 0x23F0, []), SFX('Digging', 0x02, 0x12, 0x23CD, []), + SFX('Flute use', 0x02, 0x13, 0x23A0, [0x3e]), SFX('Cape on', 0x02, 0x14, 0x2380, []), + SFX('Cape off/Wallmaster grab', 0x02, 0x15, 0x2390, []), SFX('Staircase', 0x02, 0x16, 0x232C, []), + SFX('Staircase', 0x02, 0x17, 0x2344, []), SFX('Staircase', 0x02, 0x18, 0x2356, []), + SFX('Staircase', 0x02, 0x19, 0x236E, []), SFX('Tall grass/Hammer hitting bush', 0x02, 0x1A, 0x2316, []), + SFX('Mire shallow water', 0x02, 0x1B, 0x2307, []), SFX('Shallow water', 0x02, 0x1C, 0x2301, []), + SFX('Lifting object', 0x02, 0x1D, 0x22BB, []), SFX('Cutting grass', 0x02, 0x1E, 0x2577, []), + SFX('Item breaking', 0x02, 0x1F, 0x22E9, []), SFX('Item falling in pit', 0x02, 0x20, 0x22DA, []), + SFX('Bomb hitting ground/General bang', 0x02, 0x21, 0x22CF, []), + SFX('Pushing object/Armos bounce', 0x02, 0x22, 0x2107, []), SFX('Boots dust', 0x02, 0x23, 0x22B1, []), + SFX('Splashing', 0x02, 0x24, 0x22A5, [0x3d]), SFX('Mire shallow water again?', 0x02, 0x25, 0x2296, []), + SFX('Link taking damage', 0x02, 0x26, 0x2844, []), SFX('Fainting', 0x02, 0x27, 0x2252, []), + SFX('Item splash', 0x02, 0x28, 0x2287, []), SFX('Rupee refill', 0x02, 0x29, 0x243F, [0x3b]), + SFX('Fire rod shot hitting wall/Bombos spell', 0x02, 0x2A, 0x2033, []), + SFX('Heart beep/Text box', 0x02, 0x2B, 0x1FF2, []), SFX('Sword up', 0x02, 0x2C, 0x1FD9, [0x3a]), + SFX('Magic drain', 0x02, 0x2D, 0x20A6, []), SFX('GT opening', 0x02, 0x2E, 0x1FCA, [0x39]), + SFX('GT opening/Water drain', 0x02, 0x2F, 0x1F47, [0x38]), SFX('Cucco', 0x02, 0x30, 0x1EF1, []), + SFX('Fairy', 0x02, 0x31, 0x20CE, []), SFX('Bug net', 0x02, 0x32, 0x1D47, []), + SFX('Teleport2', 0x02, 0x33, 0x1CDC, [], True), SFX('Teleport1', 0x02, 0x34, 0x1F6F, [0x33]), + SFX('Quake/Vitreous/Zora king/Armos/Pyramid/Lanmo', 0x02, 0x35, 0x1C67, [0x36]), + SFX('Mire entrance (extends above)', 0x02, 0x36, 0x1C64, [], True), + SFX('Spin charged', 0x02, 0x37, 0x1A43, []), SFX('Water sound', 0x02, 0x38, 0x1F6F, [], True), + SFX('GT opening thunder', 0x02, 0x39, 0x1F9C, [], True), SFX('Sword up', 0x02, 0x3A, 0x1FE7, [], True), + SFX('Quiet rupees', 0x02, 0x3B, 0x2462, [], True), SFX('Error beep', 0x02, 0x3C, 0x1A37, []), + SFX('Big splash', 0x02, 0x3D, 0x22AB, [], True), SFX('Flute again', 0x02, 0x3E, 0x23B5, [], True), + SFX('Powder paired', 0x02, 0x3F, 0x2435, [], True), + + SFX('Sword beam', 0x03, 0x01, 0x1A18, []), + SFX('TR opening', 0x03, 0x02, 0x254E, []), SFX('Pyramid hole', 0x03, 0x03, 0x224A, []), + SFX('Angry soldier', 0x03, 0x04, 0x220E, []), SFX('Lynel shot/Javelin toss', 0x03, 0x05, 0x25B7, []), + SFX('BNC swing/Phantom ganon/Helma tail/Arrghus swoosh', 0x03, 0x06, 0x21F5, []), + SFX('Cannon fire', 0x03, 0x07, 0x223D, []), SFX('Damage to enemy; $0BEX.4=1', 0x03, 0x08, 0x21E6, []), + SFX('Enemy death', 0x03, 0x09, 0x21C1, []), SFX('Collecting rupee', 0x03, 0x0A, 0x21A9, []), + SFX('Collecting heart', 0x03, 0x0B, 0x2198, []), + SFX('Non-blank text character', 0x03, 0x0C, 0x218E, []), + SFX('HUD heart (used explicitly by sanc heart?)', 0x03, 0x0D, 0x21B5, []), + SFX('Opening chest', 0x03, 0x0E, 0x2182, []), + SFX('♪Do do do doooooo♫', 0x03, 0x0F, 0x24B9, [0x3C, 0x3D, 0x3E, 0x3F]), + SFX('Opening/Closing map (paired)', 0x03, 0x10, 0x216D, [0x3b]), + SFX('Opening item menu/Bomb shop guy breathing', 0x03, 0x11, 0x214F, []), + SFX('Closing item menu/Bomb shop guy breathing', 0x03, 0x12, 0x215E, []), + SFX('Throwing object (sprites use it as well)/Stalfos jump', 0x03, 0x13, 0x213B, []), + SFX('Key door/Trinecks/Dash key landing/Stalfos Knight collapse', 0x03, 0x14, 0x246C, []), + SFX('Door closing/OW door opening/Chest opening (w/ $29 in $012E)', 0x03, 0x15, 0x212F, []), + SFX('Armos Knight thud', 0x03, 0x16, 0x2123, []), SFX('Rat squeak', 0x03, 0x17, 0x25A6, []), + SFX('Dragging/Mantle moving', 0x03, 0x18, 0x20DD, []), + SFX('Fireball/Laser shot; Somehow used by Trinexx???', 0x03, 0x19, 0x250A, []), + SFX('Chest reveal jingle ', 0x03, 0x1A, 0x1E8A, [0x38]), + SFX('Puzzle jingle', 0x03, 0x1B, 0x20B6, [0x3a]), SFX('Damage to enemy', 0x03, 0x1C, 0x1A62, []), + SFX('Potion refill/Magic drain', 0x03, 0x1D, 0x20A6, []), + SFX('Flapping (Duck/Cucco swarm/Ganon bats/Keese/Raven/Vulture)', 0x03, 0x1E, 0x2091, []), + SFX('Link falling', 0x03, 0x1F, 0x204B, []), SFX('Menu/Text cursor moved', 0x03, 0x20, 0x276C, []), + SFX('Damage to boss', 0x03, 0x21, 0x27E2, []), SFX('Boss dying/Deleting file', 0x03, 0x22, 0x26CF, []), + SFX('Spin attack/Medallion swoosh', 0x03, 0x23, 0x2001, [0x39]), + SFX('OW map perspective change', 0x03, 0x24, 0x2043, []), + SFX('Pressure switch', 0x03, 0x25, 0x1E9D, []), + SFX('Lightning/Game over/Laser/Ganon bat/Trinexx lunge', 0x03, 0x26, 0x1E7B, []), + SFX('Agahnim charge', 0x03, 0x27, 0x1E40, []), SFX('Agahnim/Ganon teleport', 0x03, 0x28, 0x26F7, []), + SFX('Agahnim shot', 0x03, 0x29, 0x1E21, []), + SFX('Somaria/Byrna/Ether spell/Helma fire ball', 0x03, 0x2A, 0x1E12, []), + SFX('Electrocution', 0x03, 0x2B, 0x1DF3, []), SFX('Bees', 0x03, 0x2C, 0x1DC0, []), + SFX('Milestone, also via text', 0x03, 0x2D, 0x1DA9, [0x37]), + SFX('Collecting heart container', 0x03, 0x2E, 0x1D5D, [0x35, 0x34]), + SFX('Collecting absorbable key', 0x03, 0x2F, 0x1D80, [0x33]), + SFX('Byrna spark/Item plop/Magic bat zap/Blob emerge', 0x03, 0x30, 0x1B53, []), + SFX('Sprite falling/Moldorm shuffle', 0x03, 0x31, 0x1ACA, []), + SFX('Bumper boing/Somaria punt/Blob transmutation/Sprite boings', 0x03, 0x32, 0x1A78, []), + SFX('Jingle (paired $2F→$33)', 0x03, 0x33, 0x1D93, [], True), + SFX('Depressing jingle (paired $2E→$35→$34)', 0x03, 0x34, 0x1D66, [], True), + SFX('Ugly jingle (paired $2E→$35→$34)', 0x03, 0x35, 0x1D73, [], True), + SFX('Wizzrobe shot/Helma fireball split/Mothula beam/Blue balls', 0x03, 0x36, 0x1AA7, []), + SFX('Dinky jingle (paired $2D→$37)', 0x03, 0x37, 0x1DB4, [], True), + SFX('Apathetic jingle (paired $1A→$38)', 0x03, 0x38, 0x1E93, [], True), + SFX('Quiet swish (paired $23→$39)', 0x03, 0x39, 0x2017, [], True), + SFX('Defective jingle (paired $1B→$3A)', 0x03, 0x3A, 0x20C0, [], True), + SFX('Petulant jingle (paired $10→$3B)', 0x03, 0x3B, 0x2176, [], True), + SFX('Triumphant jingle (paired $0F→$3C→$3D→$3E→$3F)', 0x03, 0x3C, 0x248A, [], True), + SFX('Less triumphant jingle ($0F→$3C→$3D→$3E→$3F)', 0x03, 0x3D, 0x2494, [], True), + SFX('"You tried, I guess" jingle (paired $0F→$3C→$3D→$3E→$3F)', 0x03, 0x3E, 0x249E, [], True), + SFX('"You didn\'t really try" jingle (paired $0F→$3C→$3D→$3E→$3F)', 0x03, 0x3F, 0x2480, [], True)] + return sfx_pool + + +def shuffle_sfx_data(): + sfx_pool = init_sfx_data() + sfx_map = {2: {}, 3: {}} + accompaniment_map = {2: set(), 3: set()} + candidates = [] + for sfx in sfx_pool: + sfx_map[sfx.sfx_set][sfx.orig_id] = sfx + if not sfx.accomp: + candidates.append((sfx.sfx_set, sfx.orig_id)) + else: + accompaniment_map[sfx.sfx_set].add(sfx.orig_id) + chained_sfx = [x for x in sfx_pool if len(x.chain) > 0] + + random.shuffle(candidates) + + # place chained sfx first + random.shuffle(chained_sfx) # todo: sort largest to smallest + chained_sfx = sorted(chained_sfx, key=lambda x: len(x.chain), reverse=True) + for chained in chained_sfx: + chosen_slot = next(x for x in candidates if len(accompaniment_map[x[0]]) - len(chained.chain) >= 0) + if chosen_slot is None: + raise Exception('Something went wrong with sfx chains') + chosen_set, chosen_id = chosen_slot + chained.target_set, chained.target_id = chosen_slot + chained.target_chain = [] + for downstream in chained.chain: + next_slot = accompaniment_map[chosen_set].pop() + ds_acc = sfx_map[chained.sfx_set][downstream] + ds_acc.target_set, ds_acc.target_id = chosen_set, next_slot + chained.target_chain.append(next_slot) + candidates.remove(chosen_slot) + sfx_pool.remove(chained) + + unchained_sfx = [x for x in sfx_pool if not x.accomp] + # do the rest + for sfx in unchained_sfx: + chosen_slot = candidates.pop() + sfx.target_set, sfx.target_id = chosen_slot + + return sfx_map + + +sfx_table = { + 2: 0x1a8c29, + 3: 0x1A8D25 +} + +# 0x1a8c29 +# d8059 + +sfx_accompaniment_table = { + 2: 0x1A8CA7, + 3: 0x1A8DA3 +} + + +def randomize_sfx(rom): + sfx_map = shuffle_sfx_data() + + for shuffled_sfx in sfx_map.values(): + for sfx in shuffled_sfx.values(): + base_address = sfx_table[sfx.target_set] + rom.write_bytes(base_address + (sfx.target_id * 2) - 2, int16_as_bytes(sfx.addr)) + ac_base = sfx_accompaniment_table[sfx.target_set] + last = sfx.target_id + if sfx.target_chain: + for chained in sfx.target_chain: + rom.write_byte(ac_base + last - 1, chained) + last = chained + rom.write_byte(ac_base + last - 1, 0) + + + + + + + + + diff --git a/source/classes/constants.py b/source/classes/constants.py index 04cbde2e..90411547 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", @@ -107,7 +108,8 @@ SETTINGSTOPROCESS = { "menuspeed": "fastmenu", "owpalettes": "ow_palettes", "uwpalettes": "uw_palettes", - "reduce_flashing": "reduce_flashing" + "reduce_flashing": "reduce_flashing", + "shuffle_sfx": "shuffle_sfx", }, "generation": { "createspoiler": "create_spoiler", diff --git a/source/gui/adjust/overview.py b/source/gui/adjust/overview.py index 4ae57e2e..7e16b1a9 100644 --- a/source/gui/adjust/overview.py +++ b/source/gui/adjust/overview.py @@ -103,6 +103,7 @@ def adjust_page(top, parent, settings): "quickswap": "quickswap", "nobgm": "disablemusic", "reduce_flashing": "reduce_flashing", + "shuffle_sfx": "shuffle_sfx", } guiargs = Namespace() for option in options: