diff --git a/BaseClasses.py b/BaseClasses.py index a2bf19b5..34e1f5f2 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -110,6 +110,7 @@ class World(object): set_player_attr('owwhirlpools', []) set_player_attr('remote_items', False) set_player_attr('required_medallions', ['Ether', 'Quake']) + set_player_attr('bottle_refills', ['Bottle (Green Potion)', 'Bottle (Green Potion)']) set_player_attr('swamp_patch_required', False) set_player_attr('powder_patch_required', False) set_player_attr('ganon_at_pyramid', True) @@ -939,7 +940,7 @@ class CollectionState(object): # try to resolve a name if resolution_hint == 'Location': spot = self.world.get_location(spot, player) - elif resolution_hint in ['Entrance', 'OWEdge']: + elif resolution_hint in ['Entrance', 'OWEdge', 'OWTerrain', 'Ledge', 'Portal', 'Whirlpool', 'Mirror', 'Flute']: spot = self.world.get_entrance(spot, player) else: # default to Region @@ -2561,6 +2562,7 @@ class Spoiler(object): self.doorTypes = {} self.lobbies = {} self.medallions = {} + self.bottles = {} self.playthrough = {} self.unreachables = [] self.startinventory = [] @@ -2660,6 +2662,15 @@ class Spoiler(object): self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0] self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1] + self.bottles = OrderedDict() + if self.world.players == 1: + self.bottles['Waterfall Bottle'] = self.world.bottle_refills[1][0] + self.bottles['Pyramid Bottle'] = self.world.bottle_refills[1][1] + else: + for player in range(1, self.world.players + 1): + self.bottles[f'Waterfall Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][0] + self.bottles[f'Pyramid Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][1] + self.locations = OrderedDict() listed_locations = set() @@ -2742,6 +2753,7 @@ class Spoiler(object): out.update(self.locations) out['Starting Inventory'] = self.startinventory out['Special'] = self.medallions + out['Bottles'] = self.bottles if self.hashes: out['Hashes'] = {f"{self.world.player_names[player][team]} (Team {team+1})": hash for (player, team), hash in self.hashes.items()} if self.shops: @@ -2790,12 +2802,14 @@ class Spoiler(object): outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_whirlpool'][player] else 'No')) outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) - outfile.write('Shuffle GT/Ganon:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shuffleganon'][player] else 'No')) - outfile.write('Shuffle Links:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shufflelinks'][player] else 'No')) + if self.metadata['shuffle'][player] != 'vanilla': + outfile.write('Shuffle GT/Ganon:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shuffleganon'][player] else 'No')) + outfile.write('Shuffle Links:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shufflelinks'][player] else 'No')) outfile.write('Pyramid Hole Pre-opened:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player]) - outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player]) - outfile.write('Experimental:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['experimental'][player] else 'No')) + if self.metadata['door_shuffle'][player] != 'vanilla': + outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player]) + outfile.write('Experimental:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['experimental'][player] else 'No')) outfile.write('Pot Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['potshuffle'][player] else 'No')) outfile.write('Key Drop Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) @@ -2810,7 +2824,7 @@ class Spoiler(object): if self.startinventory: outfile.write('Starting Inventory:'.ljust(line_width)) - outfile.write('\n'.ljust(line_width+1).join(self.startinventory)) + outfile.write('\n'.ljust(line_width+1).join(self.startinventory) + '\n') def to_file(self, filename): self.parse_data() @@ -2824,6 +2838,7 @@ class Spoiler(object): if len(self.hashes) > 0: for team in range(self.world.teams): outfile.write('%s%s\n' % (f"Hash - {self.world.player_names[player][team]} (Team {team+1}): " if self.world.teams > 1 else 'Hash: ', self.hashes[player, team])) + outfile.write('\n\nRequirements:\n\n') for dungeon, medallion in self.medallions.items(): outfile.write(f'{dungeon}:'.ljust(line_width) + '%s Medallion\n' % medallion) @@ -2834,6 +2849,10 @@ class Spoiler(object): if self.world.crystals_ganon_orig[player] == 'random': outfile.write(str('Crystals Required for Ganon' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) + outfile.write('\n\nBottle Refills:\n\n') + for fairy, bottle in self.bottles.items(): + outfile.write(f'\n{fairy}: {bottle}') + if self.overworlds: # overworlds: overworld transitions; outfile.write('\n\nOverworld:\n\n') @@ -3012,7 +3031,7 @@ access_mode = {"items": 0, "locations": 1, "none": 2} boss_mode = {"none": 0, "simple": 1, "full": 2, "random": 3, "chaos": 3} enemy_mode = {"none": 0, "shuffled": 1, "random": 2, "chaos": 2, "legacy": 3} -# byte 7: HHHD DP?? (enemy_health, enemy_dmg, potshuffle, ?) +# byte 7: HHHD DPBS (enemy_health, enemy_dmg, potshuffle, bomb logic, shuffle links) e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4} e_dmg = {"default": 0, "shuffled": 1, "random": 2} @@ -3043,7 +3062,8 @@ class Settings(object): | (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0) | (boss_mode[w.boss_shuffle[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]), - (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0)]) + (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0) + | (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0)]) return base64.b64encode(code, "+-".encode()).decode() @staticmethod @@ -3088,6 +3108,8 @@ class Settings(object): args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5] args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3] args.shufflepots[p] = True if settings[7] & 0x4 else False + args.bombbag[p] = True if settings[7] & 0x2 else False + args.shufflelinks[p] = True if settings[7] & 0x1 else False class KeyRuleType(FastEnum): diff --git a/CHANGELOG.md b/CHANGELOG.md index f5d855fc..b71abfd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +### 0.2.1.3 +- New fake flipper handling to allow S+Q rather than insta-kill +- Fixed whirlpools in Crossed OW +- Spoiler fixes, incl. missing Starting Inventory in Spoiler +- Fixed music track change to Sanc music when Standard mode is delivering Zelda +- Fixed SP flooding issue +- Fixed issue with Shuffle Ganon in CLI/GUI +- ~~Merged DR v0.5.1.5 - Mystery subweights~~ + ### 0.2.1.2 - Fixed issue with whirlpools not changing world when in Crossed OW diff --git a/DoorShuffle.py b/DoorShuffle.py index 0c2a9992..6e80d19d 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -91,8 +91,9 @@ def link_doors_main(world, player): world.get_portal('Desert East', player).destination = True if (world.mode[player] == 'inverted') != (0x30 in world.owswaps[player][0] and world.owMixed[player]): world.get_portal('Desert West', player).destination = True - else: + if (world.mode[player] == 'inverted') == (0x00 in world.owswaps[player][0] and world.owMixed[player]): world.get_portal('Skull 2 West', player).destination = True + if (world.mode[player] == 'inverted') == (0x05 in world.owswaps[player][0] and world.owMixed[player]): world.get_portal('Turtle Rock Lazy Eyes', player).destination = True world.get_portal('Turtle Rock Eye Bridge', player).destination = True else: diff --git a/EntranceShuffle.py b/EntranceShuffle.py index e845587f..e70b20b3 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -284,6 +284,8 @@ def link_entrances(world, player): dw_entrances = list() caves = list(Cave_Exits + Cave_Three_Exits + Old_Man_House) for e in entrance_pool: + if world.mode[player] == 'standard' and e == 'Bonk Fairy (Light)': + continue region = world.get_entrance(e, player).parent_region if region.type == RegionType.LightWorld: lw_entrances.append(e) @@ -338,6 +340,8 @@ def link_entrances(world, player): lw_entrances = list() dw_entrances = list() for e in entrance_pool: + if world.mode[player] == 'standard' and e == 'Bonk Fairy (Light)': + continue if e not in list(zip(*drop_connections + dropexit_connections))[0]: region = world.get_entrance(e, player).parent_region if region.type == RegionType.LightWorld: @@ -539,20 +543,25 @@ def link_entrances(world, player): place_blacksmith(world, links_house, player) # place connectors in inaccessible regions - connect_inaccessible_regions(world, list(entrance_pool), [], caves, player) + pool = list(entrance_pool) + if world.mode[player] == 'standard' and 'Bonk Fairy (Light)' in pool: + pool.remove('Bonk Fairy (Light)') + connect_inaccessible_regions(world, pool, [], caves, player) # place old man, has limited options - place_old_man(world, list(entrance_pool), player) + pool = [e for e in pool if e in entrance_pool] + place_old_man(world, pool, player) # place bomb shop, has limited options bomb_shop_doors = list(entrance_pool) if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] bomb_shop = random.choice(bomb_shop_doors) + pool.remove(bomb_shop) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) # shuffle connectors - connect_caves(world, list(entrance_pool), [], caves, player) + connect_caves(world, pool, [], caves, player) # place remaining doors connect_doors(world, list(entrance_pool), list(exit_pool), player) @@ -616,10 +625,14 @@ def link_entrances(world, player): place_blacksmith(world, links_house, player) # place connectors in inaccessible regions - connect_inaccessible_regions(world, list(entrance_pool), [], caves, player) + pool = list(entrance_pool) + if world.mode[player] == 'standard' and 'Bonk Fairy (Light)' in pool: + pool.remove('Bonk Fairy (Light)') + connect_inaccessible_regions(world, pool, [], caves, player) # place old man, has limited options - place_old_man(world, list(entrance_pool), player) + pool = [e for e in pool if e in entrance_pool] + place_old_man(world, pool, player) caves.append('Old Man Cave Exit (West)') # place bomb shop, has limited options @@ -628,10 +641,13 @@ def link_entrances(world, player): bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() + pool.remove(bomb_shop) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) # shuffle connectors doors = list(entrance_pool) + if world.mode[player] == 'standard' and 'Bonk Fairy (Light)' in doors: + doors.remove('Bonk Fairy (Light)') exit_doors = [e for e in entrance_pool if e not in entrance_exits] random.shuffle(doors) random.shuffle(exit_doors) @@ -658,7 +674,7 @@ def link_entrances(world, player): ignore_pool = True # check for swamp palace fix - if not (world.get_entrance('Dam', player).connected_region.name in ['Dam', 'Swamp Portal'] and world.get_entrance('Swamp Palace', player).connected_region.name == ['Dam', 'Swamp Portal']): + if not (world.get_entrance('Dam', player).connected_region.name in ['Dam', 'Swamp Portal'] and world.get_entrance('Swamp Palace', player).connected_region.name in ['Dam', 'Swamp Portal']): world.swamp_patch_required[player] = True # check for potion shop location diff --git a/ItemList.py b/ItemList.py index 7eb5a11e..817eb72a 100644 --- a/ItemList.py +++ b/ItemList.py @@ -371,6 +371,15 @@ def generate_itempool(world, player): tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)] world.required_medallions[player] = (mm_medallion, tr_medallion) + # shuffle bottle refills + if world.difficulty[player] in ['hard', 'expert']: + waterfall_bottle = hardbottles[random.randint(0, 5)] + pyramid_bottle = hardbottles[random.randint(0, 5)] + else: + waterfall_bottle = normalbottles[random.randint(0, 6)] + pyramid_bottle = normalbottles[random.randint(0, 6)] + world.bottle_refills[player] = (waterfall_bottle, pyramid_bottle) + set_up_shops(world, player) if world.retro[player]: diff --git a/Main.py b/Main.py index b5740bf4..0b6de69e 100644 --- a/Main.py +++ b/Main.py @@ -30,7 +30,7 @@ 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.5.1.4-u' +__version__ = '0.5.1.5-u' from source.classes.BabelFish import BabelFish @@ -136,10 +136,6 @@ def main(args, seed=None, fish=None): else: outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' - if args.create_spoiler and not args.jsonout: - logger.info(world.fish.translate("cli","cli","patching.spoiler")) - world.spoiler.meta_to_file(output_path('%s_Spoiler.txt' % outfilebase)) - for player in range(1, world.players + 1): world.difficulty_requirements[player] = difficulties[world.difficulty[player]] @@ -151,7 +147,12 @@ def main(args, seed=None, fish=None): item = ItemFactory(tok.strip(), player) if item: world.push_precollected(item) + + if args.create_spoiler and not args.jsonout: + logger.info(world.fish.translate("cli","cli","patching.spoiler")) + world.spoiler.meta_to_file(output_path('%s_Spoiler.txt' % outfilebase)) + for player in range(1, world.players + 1): create_regions(world, player) create_dungeon_regions(world, player) create_owedges(world, player) @@ -387,6 +388,7 @@ def copy_world(world): ret.player_names = copy.deepcopy(world.player_names) ret.remote_items = world.remote_items.copy() ret.required_medallions = world.required_medallions.copy() + ret.bottle_refills = world.bottle_refills.copy() ret.swamp_patch_required = world.swamp_patch_required.copy() ret.ganon_at_pyramid = world.ganon_at_pyramid.copy() ret.powder_patch_required = world.powder_patch_required.copy() diff --git a/Mystery.py b/Mystery.py index 711c88db..7008c0d5 100644 --- a/Mystery.py +++ b/Mystery.py @@ -101,7 +101,8 @@ def get_weights(path): raise Exception(f'Failed to read weights file: {e}') def roll_settings(weights): - def get_choice(option, root=weights): + def get_choice(option, root=None): + root = weights if root is None else root if option not in root: return None if type(root[option]) is not dict: @@ -116,13 +117,24 @@ def roll_settings(weights): return default return choice + while True: + subweights = weights.get('subweights', {}) + if len(subweights) == 0: + break + chances = ({k: int(v['chance']) for (k, v) in subweights.items()}) + subweight_name = random.choices(list(chances.keys()), weights=list(chances.values()))[0] + subweights = weights.get('subweights', {}).get(subweight_name, {}).get('weights', {}) + subweights['subweights'] = subweights.get('subweights', {}) + weights = {**weights, **subweights} + ret = argparse.Namespace() glitches_required = get_choice('glitches_required') - if glitches_required not in ['none', 'no_logic']: - print("Only NMG and No Logic supported") - glitches_required = 'none' - ret.logic = {'none': 'noglitches', 'owg': 'owglitches', 'no_logic': 'nologic'}[glitches_required] + if glitches_required is not None: + if glitches_required not in ['none', 'owg', 'no_logic']: + print("Only NMG, OWG, and No Logic supported") + glitches_required = 'none' + ret.logic = {'none': 'noglitches', 'owg': 'owglitches', 'no_logic': 'nologic'}[glitches_required] item_placement = get_choice('item_placement') # not supported in ER @@ -162,12 +174,13 @@ def roll_settings(weights): ret.standardize_palettes = get_choice('standardize_palettes') if 'standardize_palettes' in weights else 'standardize' goal = get_choice('goals') - ret.goal = {'ganon': 'ganon', - 'fast_ganon': 'crystals', - 'dungeons': 'dungeons', - 'pedestal': 'pedestal', - 'triforce-hunt': 'triforcehunt' - }[goal] + if goal is not None: + ret.goal = {'ganon': 'ganon', + 'fast_ganon': 'crystals', + 'dungeons': 'dungeons', + 'pedestal': 'pedestal', + 'triforce-hunt': 'triforcehunt' + }[goal] ret.openpyramid = goal == 'fast_ganon' if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False ret.shuffleganon = get_choice('shuffleganon') == 'on' @@ -175,15 +188,15 @@ def roll_settings(weights): ret.crystals_gt = get_choice('tower_open') ret.crystals_ganon = get_choice('ganon_open') - - if ret.goal == 'triforcehunt': - goal_min = get_choice_default('triforce_goal_min', default=20) - goal_max = get_choice_default('triforce_goal_max', default=20) - pool_min = get_choice_default('triforce_pool_min', default=30) - pool_max = get_choice_default('triforce_pool_max', default=30) - ret.triforce_goal = random.randint(int(goal_min), int(goal_max)) - min_diff = get_choice_default('triforce_min_difference', default=10) - ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max)) + + goal_min = get_choice_default('triforce_goal_min', default=20) + goal_max = get_choice_default('triforce_goal_max', default=20) + pool_min = get_choice_default('triforce_pool_min', default=30) + pool_max = get_choice_default('triforce_pool_max', default=30) + ret.triforce_goal = random.randint(int(goal_min), int(goal_max)) + min_diff = get_choice_default('triforce_min_difference', default=10) + ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max)) + ret.mode = get_choice('world_state') if ret.mode == 'retro': ret.mode = 'open' @@ -194,11 +207,13 @@ def roll_settings(weights): ret.hints = get_choice('hints') == 'on' - ret.swords = {'randomized': 'random', - 'assured': 'assured', - 'vanilla': 'vanilla', - 'swordless': 'swordless' - }[get_choice('weapons')] + swords = get_choice('weapons') + if swords is not None: + ret.swords = {'randomized': 'random', + 'assured': 'assured', + 'vanilla': 'vanilla', + 'swordless': 'swordless' + }[swords] ret.difficulty = get_choice('item_pool') diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 8207d66b..d34be226 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -3,7 +3,7 @@ from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSl from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel -__version__ = '0.2.1.2-u' +__version__ = '0.2.1.3-u' def link_overworld(world, player): # setup mandatory connections diff --git a/Plando.py b/Plando.py index fa9fa848..77e1d77c 100755 --- a/Plando.py +++ b/Plando.py @@ -58,7 +58,7 @@ def main(args): fill_world(world, args.plando) - if world.get_entrance('Dam', 1).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', 1).connected_region.name != 'Swamp Palace (Entrance)': + if not (world.get_entrance('Dam', 1).connected_region.name in ['Dam', 'Swamp Portal'] and world.get_entrance('Swamp Palace', 1).connected_region.name in ['Swamp Portal', 'Dam']): world.swamp_patch_required[1] = True logger.info('Calculating playthrough.') diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7098107e..c1a7a62c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,17 @@ CLI: ```--bombbag``` # Bug Fixes and Notes. +* 0.5.1.5 + * Fix for hard pool capacity upgrades missing + * Bonk Fairy (Light) is no longer in logic for ER Standard and is forbidden to be a connector, so rain state isn't exitable + * Bug fix for retro + enemizer and arrows appearing under pots + * Added bombbag and shufflelinks to settings code + * Catobat fixes: + * Fairy refills in spoiler + * Subweights support in mystery + * More defaults for mystery weights + * Less camera jank for straight stair transitions + * Bug with Straight stairs with vanilla doors where Link's walking animation stopped early is fixed * 0.5.1.4 * Revert quadrant glitch fix for baserom * Fix for inverted diff --git a/Rom.py b/Rom.py index bdb02639..f4dd88af 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '77b7b9a93f622b0cd08c49e9ee4eac4a' +RANDOMIZERBASEHASH = 'e3373be98af9d6de1cb1ab12176ecb0e' class JsonRom(object): @@ -1155,12 +1155,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): ]) # set Fountain bottle exchange items - if world.difficulty[player] in ['hard', 'expert']: - rom.write_byte(0x348FF, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x48][random.randint(0, 5)]) - rom.write_byte(0x3493B, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x48][random.randint(0, 5)]) - else: - rom.write_byte(0x348FF, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][random.randint(0, 6)]) - rom.write_byte(0x3493B, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][random.randint(0, 6)]) + rom.write_byte(0x348FF, ItemFactory(world.bottle_refills[player][0], player).code) + rom.write_byte(0x3493B, ItemFactory(world.bottle_refills[player][1], player).code) #enable Fat Fairy Chests rom.write_bytes(0x1FC16, [0xB1, 0xC6, 0xF9, 0xC9, 0xC6, 0xF9]) @@ -1543,6 +1539,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(0x180176, 0x0A if world.retro[player] else 0x00) # wood arrow cost rom.write_byte(0x180178, 0x32 if world.retro[player] else 0x00) # silver arrow cost rom.write_byte(0x301FC, 0xDA if world.retro[player] else 0xE1) # rupees replace arrows under pots + if enemized: + rom.write_byte(0x1B152e, 0xDA if world.retro[player] else 0xE1) rom.write_byte(0x30052, 0xDB if world.retro[player] else 0xE2) # replace arrows in fish prize from bottle merchant rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if world.retro[player] else [0xAF, 0x77, 0xF3, 0x7E]) # Thief steals rupees instead of arrows rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if world.retro[player] else [0xAF, 0x77, 0xF3, 0x7E]) # Pikit steals rupees instead of arrows @@ -1708,13 +1706,16 @@ def write_custom_shops(rom, world, player): loc_item = ItemFactory(item['item'], player) if (not world.shopsanity[player] and shop.region.name == 'Capacity Upgrade' and world.difficulty[player] != 'normal'): - continue # skip cap upgrades except in normal/shopsanity - item_id = loc_item.code - price = int16_as_bytes(item['price']) - replace = ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF - replace_price = int16_as_bytes(item['replacement_price']) + # really should be 5A instead of B0 -- surprise!!! + item_id, price, replace, replace_price, item_max = 0xB0, [0, 0], 0xFF, [0, 0], 1 + else: + item_id = loc_item.code + price = int16_as_bytes(item['price']) + replace = ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF + replace_price = int16_as_bytes(item['replacement_price']) + item_max = item['max'] item_player = 0 if item['player'] == player else item['player'] - item_data = [shop_id, item_id] + price + [item['max'], replace] + replace_price + [item_player] + item_data = [shop_id, item_id] + price + [item_max, replace] + replace_price + [item_player] items_data.extend(item_data) rom.write_bytes(0x184800, shop_data) diff --git a/Rules.py b/Rules.py index ef61752b..f02796be 100644 --- a/Rules.py +++ b/Rules.py @@ -1605,6 +1605,7 @@ def standard_rules(world, player): add_rule(world.get_entrance('Hyrule Castle Main Gate (South)', player), lambda state: state.has('Zelda Delivered', player)) add_rule(world.get_entrance('Hyrule Castle Main Gate (North)', player), lambda state: state.has('Zelda Delivered', player)) add_rule(world.get_entrance('Hyrule Castle Ledge Drop', player), lambda state: state.has('Zelda Delivered', player)) + add_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Zelda Delivered', player)) # don't allow bombs to get past here before zelda is rescued set_rule(world.get_entrance('GT Hookshot South Entry to Ranged Crystal', player), lambda state: (state.can_use_bombs(player) and state.has('Zelda Delivered', player)) or state.has('Blue Boomerang', player) or state.has('Red Boomerang', player)) # or state.has('Cane of Somaria', player)) diff --git a/Utils.py b/Utils.py index b35e6f4c..deed090d 100644 --- a/Utils.py +++ b/Utils.py @@ -318,6 +318,10 @@ def update_deprecated_args(args): if args: argVars = vars(args) truthy = [1, True, "True", "true"] + if "multi" in argVars: + players = int(args.multi) + else: + players = 1 # Hints default to FALSE # Don't do: Yes # Do: No @@ -355,11 +359,11 @@ def update_deprecated_args(args): # Don't do: Yes # Do: No if "no_shuffleganon" in argVars: - args.shuffleganon = not args.no_shuffleganon in truthy - # Don't do: No - # Do: Yes - if "shuffleganon" in argVars: - args.no_shuffleganon = not args.shuffleganon in truthy + if isinstance(args.shuffleganon, dict): + for player in range(1, players + 1): + args.shuffleganon[player] = not args.no_shuffleganon in truthy + else: + args.shuffleganon = not args.no_shuffleganon in truthy # Playthrough defaults to TRUE # Don't do: Yes diff --git a/asm/doorrando.asm b/asm/doorrando.asm index fe0cc415..7a6c926c 100644 --- a/asm/doorrando.asm +++ b/asm/doorrando.asm @@ -9,6 +9,7 @@ ; Normal doors use $FE to store the trap door indicator ; Normal doors use $045e to store Y coordinate when transitioning to in-room stairs ; Normal doors use $045f to determine the order in which supertile quadrants are drawn +; Straight stairs use $046d to store X coordinate on animation start ; Spiral doors use $045e to store stair type ; Gfx uses $b1 to for sub-sub-sub-module thing diff --git a/asm/doortables.asm b/asm/doortables.asm index eae16430..90678ca3 100644 --- a/asm/doortables.asm +++ b/asm/doortables.asm @@ -681,6 +681,8 @@ db $00,$07,$20,$20,$07,$07,$07,$07,$07,$20,$20,$07,$20,$20,$20,$20 db $07,$07,$02,$02,$02,$02,$07,$07,$07,$20,$20,$07,$20,$20,$20,$07 ;27f300 +DungeonTilesets: +db $04,$04,$05,$12,$04,$08,$07,$0C,$09,$0B,$05,$0A,$0D,$0E,$06,$06 ; ;org $27ff00 diff --git a/asm/normal.asm b/asm/normal.asm index 3bdf9622..13323d88 100644 --- a/asm/normal.asm +++ b/asm/normal.asm @@ -150,15 +150,14 @@ LoadRoomVert: .notEdge lda $01 : and #$03 : cmp #$03 : bne .normal jsr ScrollToInroomStairs + stz $046d bra .end .normal ldy #$01 : jsr ShiftVariablesMainDir jsr PrepScrollToNormal .scroll - lda $01 : and #$40 : pha + lda $01 : and #$40 : sta $046d jsr ScrollX - pla : beq .end - ldy #$00 : jsr ApplyScroll .end plb ; restore db register rts @@ -291,6 +290,11 @@ StraightStairsAdj: stx $0464 : sty $012e ; what we wrote over lda.l DRMode : beq + lda $045e : bne .toInroom + lda $046d : beq .noScroll + sta $22 + ldy #$00 : jsr ApplyScroll + stz $046d + .noScroll jsr GetTileAttribute : tax lda $11 : cmp #$12 : beq .goingNorth lda $a2 : cmp #$51 : bne ++ @@ -338,9 +342,10 @@ db $d0, $f6, $10, $1a, $f0, $00 StraightStairsFix: { + pha lda.l DRMode : bne + - !add $20 : sta $20 ;what we wrote over - + rtl + pla : !add $20 : sta $20 : rtl ;what we wrote over + + pla : rtl } StraightStairLayerFix: diff --git a/asm/owrando.asm b/asm/owrando.asm index d4a5ebd1..283c358f 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -130,9 +130,11 @@ OWWorldCheck16: OWWhirlpoolUpdate: { jsl $02ea6c ; what we wrote over - lda.l OWFlags : and #$01 : beq + - ldx $8a : jsr OWWorldUpdate - + rtl + lda.l OWFlags : and #$01 : bne + + lda.l OWMode+1 : and #$02 : beq .return + + ldx $8a : jsr OWWorldUpdate + .return + rtl } OWFluteCancel: @@ -358,6 +360,11 @@ OWNewDestination: .return lda $05 : sta $8a + ;bra + + ; nop #8 + ; jsl $02EA41 + ; nop #8 + ;+ rep #$30 : rts } OWWorldUpdate: ; x = owid of destination screen diff --git a/asm/scroll.asm b/asm/scroll.asm index f66918c8..435b5c98 100644 --- a/asm/scroll.asm +++ b/asm/scroll.asm @@ -168,7 +168,11 @@ ScrollX: ;change the X offset variables pla : sta $00 sep #$30 - lda $04 : sta $22 + lda $04 : ldx $046d : bne .straight + sta $22 : bra + + .straight + sta $046d ; set X position later + + lda $00 : sta $23 : sta $0609 : sta $060d lda $01 : sta $a9 lda $0e : asl : ora $ac : sta $ac diff --git a/data/base2current.bps b/data/base2current.bps index a552143f..be48a7a2 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/mystery_example.yml b/mystery_example.yml index 3ac5950b..d6fa1eb5 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -50,6 +50,7 @@ restricted: 2 full: 2 lite: 2 + lean: 2 crossed: 3 insanity: 1 shufflelinks: diff --git a/mystery_example_subweights.yml b/mystery_example_subweights.yml new file mode 100644 index 00000000..1ec43cac --- /dev/null +++ b/mystery_example_subweights.yml @@ -0,0 +1,57 @@ + description: Example for subweights + glitches_required: none + world_state: open + goals: ganon + weapons: randomized + entrance_shuffle: none + intensity: 3 + subweights: + vanilla: + chance: 25 + weights: + door_shuffle: vanilla + keydropshuffle: + on: 40 + off: 60 + basic: + chance: 25 + weights: + door_shuffle: basic + keydropshuffle: + on: 70 + off: 30 + crossed: + chance: 25 + weights: + door_shuffle: crossed + keydropshuffle: + on: 90 + off: 10 + chaos: + chance: 25 + weights: + door_shuffle: crossed + entrance_shuffle: + none: 30 + crossed: 70 + keydropshuffle: + on: 90 + off: 10 + shopsanity: + on: 50 + off: 50 + bombbag: + on: 25 + off: 75 + subweights: + normal: + chance: 40 + weights: {} + swordless: + chance: 20 + weights: + weapons: swordless + keysanity: + chance: 40 + weights: + dungeon_items: full diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 05bc7dea..365e40e6 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -358,13 +358,13 @@ "help": "suppress" }, "shuffleganon": { - "action": "store_false", - "type": "bool" + "action": "store_true", + "type": "bool", + "help": "suppress" }, "no_shuffleganon": { "action": "store_true", - "dest": "shuffleganon", - "help": "suppress" + "dest": "shuffleganon" }, "shufflelinks": { "action": "store_true", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 30e46f20..59d9a9f3 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -181,10 +181,10 @@ " connect remaining entrances.", "Full: Mix cave and dungeon entrances freely while limiting", " multi-entrance caves to one world.", - "Lite: Entrances are put into separate pools based on their", - " vanilla location. Dungeons, dropdowns, connector caves,", - " and locations that have an item are shuffled from", - " individual pools. Non-item locations remain vanilla.", + "Lite: Beginner-friendly. Dungeons/connectors, dropdowns, and", + " item locations are shuffled in separate pools. Non-item", + " locations remain vanilla. Connectors are same-world.", + "Lean: Same as Lite, except connectors can travel cross worlds.", "Crossed: Mix cave and dungeon entrances freely while allowing", " caves to cross between worlds.", "Insanity: Decouple entrances and exits from each other and", @@ -301,7 +301,7 @@ "Keys are universal, shooting arrows costs rupees,", "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"], + "pseudoboots": [ " Players starts with pseudo boots that allow dashing but no item checks (default: %(default)s)"], "bombbag": ["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." ], @@ -315,8 +315,8 @@ "None: You will be able to reach enough locations to beat the game." ], "hints": [ "Make telepathic tiles and storytellers give helpful hints. (default: %(default)s)" ], - "shuffleganon": [ - "Include the Ganon's Tower and Pyramid Hole in the", + "no_shuffleganon": [ + "Don't include the Ganon's Tower and Pyramid Hole in the", "entrance shuffle pool. (default: %(default)s)" ], "shufflelinks": [