diff --git a/.gitignore b/.gitignore index 5c04b745..f731841f 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ weights/ /Players/ /QUsb2Snes/ /output/ +/enemizer/ base2current.json diff --git a/BaseClasses.py b/BaseClasses.py index 7f5c25e0..4691ef4b 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -168,6 +168,7 @@ class World(object): set_player_attr('potshuffle', False) set_player_attr('pot_contents', None) set_player_attr('pseudoboots', False) + set_player_attr('mirrorscroll', False) set_player_attr('collection_rate', False) set_player_attr('colorizepots', True) set_player_attr('pot_pool', {}) @@ -177,6 +178,7 @@ class World(object): set_player_attr('trap_door_mode', 'optional') set_player_attr('key_logic_algorithm', 'partial') set_player_attr('aga_randomness', True) + set_player_attr('money_balance', 100) set_player_attr('shopsanity', False) set_player_attr('mixed_travel', 'prevent') @@ -3059,6 +3061,7 @@ class Spoiler(object): 'potshuffle': self.world.potshuffle, 'shopsanity': self.world.shopsanity, 'pseudoboots': self.world.pseudoboots, + 'mirrorscroll': self.world.mirrorscroll, 'triforcegoal': self.world.treasure_hunt_count, 'triforcepool': self.world.treasure_hunt_total, 'race': self.world.settings.world_rep['meta']['race'], @@ -3307,6 +3310,7 @@ class Spoiler(object): outfile.write('Enemy Logic:'.ljust(line_width) + '%s\n' % self.metadata['any_enemy_logic'][player]) outfile.write('\n') outfile.write('Pseudoboots:'.ljust(line_width) + '%s\n' % yn(self.metadata['pseudoboots'][player])) + outfile.write('Mirror Scroll:'.ljust(line_width) + '%s\n' % yn(self.metadata['mirrorscroll'][player])) outfile.write('Hints:'.ljust(line_width) + '%s\n' % yn(self.metadata['hints'][player])) outfile.write('Race:'.ljust(line_width) + '%s\n' % yn(self.world.settings.world_rep['meta']['race'])) @@ -3681,7 +3685,7 @@ overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2} trap_door_mode = {'vanilla': 0, 'optional': 1, 'boss': 2, 'oneway': 3} key_logic_algo = {'dangerous': 0, 'partial': 1, 'strict': 2} -# byte 15: SSLL ??DD (skullwoods, linked_drops, 2 free bytes, door_type) +# byte 15: SSLL M?DD (skullwoods, linked_drops, mirrorscroll, 1 free byte, door_type) skullwoods_mode = {'original': 0, 'restricted': 1, 'loose': 2, 'followlinked': 3} linked_drops_mode = {'unset': 0, 'linked': 1, 'independent': 2} door_type_mode = {'original': 0, 'big': 1, 'all': 2, 'chaos': 3} @@ -3742,7 +3746,7 @@ class Settings(object): | trap_door_mode[w.trap_door_mode[p]] << 3 | key_logic_algo[w.key_logic_algorithm[p]]), (skullwoods_mode[w.skullwoods[p]] << 6 | linked_drops_mode[w.linked_drops[p]] << 4 - | door_type_mode[w.door_type_mode[p]]), + | (0x8 if w.mirrorscroll[p] else 0) | door_type_mode[w.door_type_mode[p]]), ]) return base64.b64encode(code, "+-".encode()).decode() @@ -3835,6 +3839,7 @@ class Settings(object): if len(settings) > 15: args.skullwoods[p] = r(skullwoods_mode)[(settings[15] & 0xc0) >> 6] args.linked_drops[p] = r(linked_drops_mode)[(settings[15] & 0x30) >> 4] + args.mirrorscroll[p] = True if settings[15] & 0x8 else False args.door_type_mode[p] = r(door_type_mode)[(settings[15] & 0x3)] diff --git a/CHANGELOG.md b/CHANGELOG.md index 534152af..07eeb96f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.5.1.4 +- Fixed incorrect ganon silvers hint +- \~Merged in DR v1.4.8.1~ + ## 0.5.1.3 - Fixed some minor issues with mystery multiworld - Corrected some generation issues with Nearby dungeon item shuffles diff --git a/CLI.py b/CLI.py index 2d4f898e..8fc7263e 100644 --- a/CLI.py +++ b/CLI.py @@ -139,13 +139,14 @@ def parse_cli(argv, no_defaults=False): 'triforce_max_difference', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern', 'skullwoods', 'linked_drops', - 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', + 'pseudoboots', 'mirrorscroll', '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', 'dropshuffle', 'pottery', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', 'shuffle_sfxinstruments', 'shuffle_songinstruments', 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', - 'bonk_drops', 'trap_door_mode', 'key_logic_algorithm', 'door_self_loops', 'any_enemy_logic', 'aga_randomness']: + 'bonk_drops', 'trap_door_mode', 'key_logic_algorithm', 'door_self_loops', 'any_enemy_logic', 'aga_randomness', + 'money_balance']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -208,6 +209,7 @@ def parse_settings(): "overworld_map": "default", "take_any": "none", "pseudoboots": False, + "mirrorscroll": False, "shuffleenemies": "none", "shufflebosses": "none", @@ -239,6 +241,7 @@ def parse_settings(): "mixed_travel": "prevent", "standardize_palettes": "standardize", 'aga_randomness': True, + 'money_balance': 100, "triforce_pool": 0, "triforce_goal": 0, diff --git a/DoorShuffle.py b/DoorShuffle.py index 71c90884..16ad43af 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -3884,8 +3884,10 @@ logical_connections = [ ('TR Crystaroller Chest to Middle Barrier - Blue', 'TR Crystaroller Middle'), ('TR Crystaroller Middle Ranged Crystal Exit', 'TR Crystaroller Middle'), ('TR Crystaroller Bottom Ranged Crystal Exit', 'TR Crystaroller Bottom'), - ('TR Dark Ride Path', 'TR Dark Ride Ledges'), - ('TR Dark Ride Ledges Path', 'TR Dark Ride'), + ('TR Dark Ride Normal Path', 'TR Dark Ride South Platform'), + ('TR Dark Ride Backward Path', 'TR Dark Ride North Platform'), + ('TR Dark Ride Ledge Path', 'TR Dark Ride Ledges'), + ('TR Dark Ride Return Path', 'TR Dark Ride South Platform'), ('TR Crystal Maze Start to Interior Barrier - Blue', 'TR Crystal Maze Interior'), ('TR Crystal Maze Start to Crystal', 'TR Crystal Maze Start - Crystal'), ('TR Crystal Maze Start Crystal Exit', 'TR Crystal Maze Start'), diff --git a/Doors.py b/Doors.py index 06d8770a..af83f6fc 100644 --- a/Doors.py +++ b/Doors.py @@ -1061,8 +1061,10 @@ def create_doors(world, player): create_door(player, 'TR Crystaroller Down Stairs', Sprl).dir(Dn, 0x04, 0, HTH).ss(A, 0x12, 0x80, True, True).small_key().pos(0), create_door(player, 'TR Dark Ride Up Stairs', Sprl).dir(Up, 0xb5, 0, HTH).ss(A, 0x1b, 0x6c), create_door(player, 'TR Dark Ride SW', Nrml).dir(So, 0xb5, Left, High).trap(0x4).pos(0).portal(Z, 0x22), - create_door(player, 'TR Dark Ride Path', Lgcl), - create_door(player, 'TR Dark Ride Ledges Path', Lgcl), + create_door(player, 'TR Dark Ride Normal Path', Lgcl), + create_door(player, 'TR Dark Ride Ledge Path', Lgcl), + create_door(player, 'TR Dark Ride Backward Path', Lgcl), + create_door(player, 'TR Dark Ride Return Path', Lgcl), create_door(player, 'TR Dash Bridge NW', Nrml).dir(No, 0xc5, Left, High).pos(1), create_door(player, 'TR Dash Bridge SW', Nrml).dir(So, 0xc5, Left, High).pos(2).portal(Z, 0x02), create_door(player, 'TR Dash Bridge WS', Nrml).dir(We, 0xc5, Bot, High).small_key().pos(0), diff --git a/Dungeons.py b/Dungeons.py index 02734038..8fc54156 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -181,7 +181,8 @@ tr_regions = [ 'TR Big View', 'TR Big Chest', 'TR Big Chest Entrance', 'TR Lazy Eyes', 'TR Dash Room', 'TR Tongue Pull', 'TR Rupees', 'TR Crystaroller Bottom', 'TR Crystaroller Middle', 'TR Crystaroller Top', 'TR Crystaroller Top - Crystal', 'TR Crystaroller Chest', 'TR Crystaroller Middle - Ranged Crystal', - 'TR Crystaroller Bottom - Ranged Crystal', 'TR Dark Ride', 'TR Dark Ride Ledges', 'TR Dash Bridge', 'TR Eye Bridge', + 'TR Crystaroller Bottom - Ranged Crystal', 'TR Dark Ride North Platform', 'TR Dark Ride South Platform', + 'TR Dark Ride Ledges', 'TR Dash Bridge', 'TR Eye Bridge', 'TR Crystal Maze Start', 'TR Crystal Maze Start - Crystal', 'TR Crystal Maze Interior', 'TR Crystal Maze End', 'TR Crystal Maze End - Ranged Crystal', 'TR Final Abyss Balcony', 'TR Final Abyss Ledge', 'TR Boss', 'TR Boss Spoils', 'Turtle Rock Main Portal', 'Turtle Rock Lazy Eyes Portal', 'Turtle Rock Chest Portal', diff --git a/Fill.py b/Fill.py index 959b97b4..8ffec2ee 100644 --- a/Fill.py +++ b/Fill.py @@ -79,7 +79,6 @@ def fill_dungeons_restrictive(world, shuffled_locations): break for wix in reversed(to_remove): del smalls[wix] - # remove 2 swamp locations from pool hybrid_locations = [] to_remove = [] @@ -91,7 +90,6 @@ def fill_dungeons_restrictive(world, shuffled_locations): break for i in reversed(to_remove): shuffled_locations.pop(i) - # place 2 HMG keys hybrid_state_base = all_state_base.copy() for x in bigs + smalls + prizes + others: @@ -1122,14 +1120,16 @@ def balance_money_progression(world): solvent = set() insolvent = set() for player in range(1, world.players+1): - if wallet[player] >= sphere_costs[player] >= 0: + modifier = world.money_balance[player]/100 + if wallet[player] >= sphere_costs[player] * modifier >= 0: solvent.add(player) - if sphere_costs[player] > 0 and sphere_costs[player] > wallet[player]: + if sphere_costs[player] > 0 and sphere_costs[player] * modifier > wallet[player]: insolvent.add(player) if len([p for p in solvent if len(locked_by_money[p]) > 0]) == 0: if len(insolvent) > 0: target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p]) - difference = sphere_costs[target_player]-wallet[target_player] + target_modifier = world.money_balance[target_player]/100 + difference = sphere_costs[target_player] * target_modifier - wallet[target_player] logger.debug(f'Money balancing needed: Player {target_player} short {difference}') else: difference = 0 @@ -1169,7 +1169,8 @@ def balance_money_progression(world): solvent.add(target_player) # apply solvency for player in solvent: - wallet[player] -= sphere_costs[player] + modifier = world.money_balance[player]/100 + wallet[player] -= sphere_costs[player] * modifier for location in locked_by_money[player]: if isinstance(location, str) and location == 'Kiki': kiki_paid[player] = True diff --git a/InitialSram.py b/InitialSram.py index 0aa25bc2..7fbb0b3b 100644 --- a/InitialSram.py +++ b/InitialSram.py @@ -124,7 +124,7 @@ class InitialSram: if startingstate.has('Beat Agahnim 1', player): self.pre_open_lumberjack() if world.mode[player] == 'standard': - self.set_progress_indicator(0x80) + self.set_progress_indicator(0x80) # todo: probably missing some code rom side for this else: self.set_progress_indicator(0x03) diff --git a/ItemList.py b/ItemList.py index bed44e64..ef3c7a12 100644 --- a/ItemList.py +++ b/ItemList.py @@ -837,7 +837,8 @@ def customize_shops(world, player): if retro_bow and item.name == 'Single Arrow': price = 80 # randomize price - shop.add_inventory(idx, item.name, randomize_price(price), max_repeat, player=item.player) + price = final_price(loc, price, world, player) + shop.add_inventory(idx, item.name, price, max_repeat, player=item.player) if item.name in cap_replacements and shop_name not in retro_shops and item.player == player: possible_replacements.append((shop, idx, location, item)) # randomize shopkeeper @@ -854,8 +855,10 @@ def customize_shops(world, player): if len(choices) > 0: shop, idx, loc, item = random.choice(choices) upgrade = ItemFactory('Bomb Upgrade (+5)', player) - shop.add_inventory(idx, upgrade.name, randomize_price(upgrade.price), 6, - item.name, randomize_price(item.price), player=item.player) + up_price = final_price(loc.name, upgrade.price, world, player) + rep_price = final_price(loc.name, item.price, world, player) + shop.add_inventory(idx, upgrade.name, up_price, 6, + item.name, rep_price, player=item.player) loc.item = upgrade upgrade.location = loc if not found_arrow_upgrade and len(possible_replacements) > 0: @@ -866,8 +869,10 @@ def customize_shops(world, player): if len(choices) > 0: shop, idx, loc, item = random.choice(choices) upgrade = ItemFactory('Arrow Upgrade (+5)', player) - shop.add_inventory(idx, upgrade.name, randomize_price(upgrade.price), 6, - item.name, randomize_price(item.price), player=item.player) + up_price = final_price(loc.name, upgrade.price, world, player) + rep_price = final_price(loc.name, item.price, world, player) + shop.add_inventory(idx, upgrade.name, up_price, 6, + item.name, rep_price, player=item.player) loc.item = upgrade upgrade.location = loc change_shop_items_to_rupees(world, player, shops_to_customize) @@ -875,6 +880,15 @@ def customize_shops(world, player): check_hints(world, player) +def final_price(location, price, world, player): + if world.customizer and world.customizer.get_prices(player): + custom_prices = world.customizer.get_prices(player) + if location in custom_prices: + # todo: validate valid price + return custom_prices[location] + return randomize_price(price) + + def randomize_price(price): half_price = price // 2 max_price = price - half_price @@ -914,12 +928,16 @@ def balance_prices(world, player): shop_locations = [] for shop, loc_list in shop_to_location_table.items(): for loc in loc_list: + if world.customizer and world.customizer.get_prices(player) and loc in world.customizer.get_prices(player): + needed_money += world.customizer.get_prices(player)[loc] + continue # considered a fixed price and shouldn't be altered loc = world.get_location(loc, player) shop_locations.append(loc) slot = shop_to_location_table[loc.parent_region.name].index(loc.name) needed_money += loc.parent_region.shop.inventory[slot]['price'] - target = available_money - needed_money + modifier = world.money_balance[player]/100 + target = available_money - needed_money * modifier # remove the first set of shops from consideration (or used them for discounting) state, done = CollectionState(world), False unchecked_locations = world.get_locations().copy() @@ -1255,32 +1273,11 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer assert loc not in placed_items placed_items[loc] = item - # Correct for insanely oversized item counts and take initial steps to handle undersized pools. - # Bow to Silver Arrows Upgrade, including Generic Keys & Rupoors - for x in [*range(0, 69)]: - key = CONST.CUSTOMITEMS[x] - if customitemarray[key] > total_items_to_place: - customitemarray[key] = total_items_to_place - - # Triforce - if customitemarray["triforce"] > total_items_to_place: - customitemarray["triforce"] = total_items_to_place - # Triforce Pieces if goal in ['triforcehunt', 'trinity', 'ganonhunt']: g, t = set_default_triforce(goal, customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"]) customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"] = g, t - itemtotal = 0 - # Bow to Silver Arrows Upgrade, including Generic Keys & Rupoors - for x in [*range(0, 66 + 1), 68, 69]: - key = CONST.CUSTOMITEMS[x] - itemtotal = itemtotal + customitemarray[key] - # Triforce - itemtotal = itemtotal + customitemarray["triforce"] - # Generic Keys - itemtotal = itemtotal + customitemarray["generickeys"] - customitems = [ "Bow", "Silver Arrows", "Blue Boomerang", "Red Boomerang", "Hookshot", "Mushroom", "Magic Powder", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake", @@ -1322,7 +1319,6 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer and (goal in ['triforcehunt', 'trinity', 'ganonhunt']) and (customitemarray["triforce"] == 0)): extrapieces = treasure_hunt_count - customitemarray["triforcepieces"] pool.extend(['Triforce Piece'] * extrapieces) - itemtotal = itemtotal + extrapieces if timer in ['display', 'timed', 'timed-countdown']: clock_mode = 'countdown' if timer == 'timed-countdown' else 'stopwatch' @@ -1333,7 +1329,6 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer if goal in ['pedestal', 'trinity']: place_item('Master Sword Pedestal', 'Triforce') - itemtotal = itemtotal + 1 if mode == 'standard': if world.keyshuffle[player] == 'universal': @@ -1350,13 +1345,6 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer pool.extend(['Magic Mirror'] * customitemarray["mirror"]) pool.extend(['Moon Pearl'] * customitemarray["pearl"]) - if world.keyshuffle[player] == 'universal': - itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode - if itemtotal < total_items_to_place: - nothings = total_items_to_place - itemtotal -# print("Placing " + str(nothings) + " Nothings") - pool.extend(['Nothing'] * nothings) - start_inventory = [x for x in world.precollected_items if x.player == player] if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic'] and all(x.name != 'Pegasus Boots' for x in start_inventory): precollected_items.append('Pegasus Boots') diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index d5cebf00..d9c837b9 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -814,7 +814,13 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world, def find_next_counter(new_door, old_counter, key_layout, prize_flag=None): - proposed_doors = {**old_counter.open_doors, **dict.fromkeys([new_door, new_door.dest])} + prop_doors = next((item_or_tuple for item_or_tuple in key_layout.proposal + if new_door == item_or_tuple or (isinstance(item_or_tuple, tuple) and new_door in item_or_tuple)), None) + if prop_doors: + prop_doors = list(prop_doors) if isinstance(prop_doors, tuple) else [prop_doors] + proposed_doors = {**old_counter.open_doors, **dict.fromkeys(prop_doors)} + else: + proposed_doors = {**old_counter.open_doors} bk_open = old_counter.big_key_opened or new_door.bigKey prize_flag = prize_flag if prize_flag else old_counter.prize_doors_opened return find_counter(proposed_doors, bk_open, key_layout, prize_flag) diff --git a/Main.py b/Main.py index 361743cb..e1af57b1 100644 --- a/Main.py +++ b/Main.py @@ -40,7 +40,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.4.7.2' +version_number = '1.4.8.1' version_branch = '-u' __version__ = f'{version_number}{version_branch}' @@ -501,12 +501,14 @@ def init_world(args, fish): world.skullwoods = args.skullwoods.copy() world.linked_drops = args.linked_drops.copy() world.pseudoboots = args.pseudoboots.copy() + world.mirrorscroll = args.mirrorscroll.copy() world.overworld_map = args.overworld_map.copy() world.take_any = args.take_any.copy() world.restrict_boss_items = args.restrict_boss_items.copy() world.collection_rate = args.collection_rate.copy() world.colorizepots = args.colorizepots.copy() world.aga_randomness = args.aga_randomness.copy() + world.money_balance = args.money_balance.copy() return world @@ -589,7 +591,13 @@ def copy_world(world): ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy() ret.open_pyramid = world.open_pyramid.copy() ret.shufflelinks = world.shufflelinks.copy() + ret.shuffletavern = world.shuffletavern.copy() ret.shuffle_ganon = world.shuffle_ganon.copy() + ret.skullwoods = world.skullwoods.copy() + ret.linked_drops = world.linked_drops.copy() + ret.pseudoboots = world.pseudoboots.copy() + ret.mirrorscroll = world.mirrorscroll.copy() + ret.overworld_map = world.overworld_map.copy() ret.take_any = world.take_any.copy() ret.boss_shuffle = world.boss_shuffle.copy() ret.enemy_shuffle = world.enemy_shuffle.copy() @@ -604,6 +612,7 @@ def copy_world(world): ret.trap_door_mode = world.trap_door_mode.copy() ret.key_logic_algorithm = world.key_logic_algorithm.copy() ret.aga_randomness = world.aga_randomness.copy() + ret.money_balance = world.money_balance.copy() ret.experimental = world.experimental.copy() ret.shopsanity = world.shopsanity.copy() ret.dropshuffle = world.dropshuffle.copy() @@ -802,7 +811,13 @@ def copy_world_premature(world, player): ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy() ret.open_pyramid = world.open_pyramid.copy() ret.shufflelinks = world.shufflelinks.copy() + ret.shuffletavern = world.shuffletavern.copy() ret.shuffle_ganon = world.shuffle_ganon.copy() + ret.skullwoods = world.skullwoods.copy() + ret.linked_drops = world.linked_drops.copy() + ret.pseudoboots = world.pseudoboots.copy() + ret.mirrorscroll = world.mirrorscroll.copy() + ret.overworld_map = world.overworld_map.copy() ret.take_any = world.take_any.copy() ret.boss_shuffle = world.boss_shuffle.copy() ret.enemy_shuffle = world.enemy_shuffle.copy() @@ -817,6 +832,7 @@ def copy_world_premature(world, player): ret.trap_door_mode = world.trap_door_mode.copy() ret.key_logic_algorithm = world.key_logic_algorithm.copy() ret.aga_randomness = world.aga_randomness.copy() + ret.money_balance = world.money_balance.copy() ret.experimental = world.experimental.copy() ret.shopsanity = world.shopsanity.copy() ret.dropshuffle = world.dropshuffle.copy() diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 9a3d86ba..70c9d79f 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -8,7 +8,7 @@ from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitType from OverworldGlitchRules import create_owg_connections from Utils import bidict -version_number = '0.5.1.3' +version_number = '0.5.1.4' # branch indicator is intentionally different across branches version_branch = '' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index de9fe6f6..49f286e1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -141,20 +141,18 @@ These are now independent of retro mode and have three options: None, Random, an # Patch Notes -* 1.4.7.2 - - Fixed an issue with shuffle_ganon/fix_gtower_exit causing a generation failure +* 1.4.8.1 + - Fixed broken doors generation + - Fixed bomb/arrow upgrade ignoring custom pricing + - Extended `money_balance` to apply to price balancing for non-custom shops. +* 1.4.8 + - New option: Mirror Scroll - to add the item to the starting inventory in non-doors modes (Thanks Telethar!) + - Customizer: Ability to customize shop prices and control money balancing. `money_balance` is a percentage betwen 0 and 100 that attempts to ensure you have that much percentage of money available for purchases. (100 is default, 0 essentially ignores money considerations) + - Fixed a key logic bug with decoupled doors when a big key door leads to a small key door (the small key door was missing appropriate logic) + - Fixed an ER bug where Bonk Fairy could be used for a mandatory connector in standard mode (boots could allow escape to be skipped) + - Fixed an issue with flute activation in rain mode. (thanks Codemann!) + - Fixed an issue with enemies in TR Dark Ride room not requiring Somaria. (Refactored the room for decoupled logic better) - More HMG fixes by Muffins -* 1.4.7.1 - - Fixed an issue with the repaired "beemizer" setting not being backwards compatible -* 1.4.7 - - Fixed generation error with Big Key in starting inventory (thanks Cody!) - - HMG/NL logic fixes by Muffins - - Enemizer: Disabled Walking Zora in the UW due to crash with Swamola (they ignore a lot of collison anyway) - - Enemizer: Fixed an issue with enemizer bush sprites - - Enemizer: Banned new Mimics from being the randomized bush sprite due to crash - - "Beatable" or "accessibility: none" can now use randomized trap doors to seal off entire parts of dungeons (was intended, bug prevented the logic skip) - - Logic error with enemizer and standard should use new enemy logic rules - - Fixed a bug with the inconsistent treatment of the beemizer setting - - Fixed an issue with returning Blacksmith in Simple shuffle (when blacksmith is at Link's House) - - Fixed an issue with dark sanctuary spawn at tavern north door (thanks Codemann!) - - Various enemy bans for the last few months + - Fixed an issue with multi-player HMG + - Fixed an issue limiting number of items specified in the item pool on the GUI + - Minor documentation fixes (thanks Codemann!) diff --git a/Regions.py b/Regions.py index 205e05d0..7068f0f6 100644 --- a/Regions.py +++ b/Regions.py @@ -907,8 +907,9 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'TR Crystaroller Chest', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['TR Crystaroller Chest to Middle Barrier - Blue']), create_dungeon_region(player, 'TR Crystaroller Middle - Ranged Crystal', 'Turtle Rock', None, ['TR Crystaroller Middle Ranged Crystal Exit']), create_dungeon_region(player, 'TR Crystaroller Bottom - Ranged Crystal', 'Turtle Rock', None, ['TR Crystaroller Bottom Ranged Crystal Exit']), - create_dungeon_region(player, 'TR Dark Ride', 'Turtle Rock', None, ['TR Dark Ride Up Stairs', 'TR Dark Ride SW', 'TR Dark Ride Path']), - create_dungeon_region(player, 'TR Dark Ride Ledges', 'Turtle Rock', None, ['TR Dark Ride Ledges Path']), + create_dungeon_region(player, 'TR Dark Ride North Platform', 'Turtle Rock', None, ['TR Dark Ride Up Stairs', 'TR Dark Ride Normal Path', 'TR Dark Ride Ledge Path']), + create_dungeon_region(player, 'TR Dark Ride South Platform', 'Turtle Rock', None, ['TR Dark Ride SW', 'TR Dark Ride Backward Path']), + create_dungeon_region(player, 'TR Dark Ride Ledges', 'Turtle Rock', None, ['TR Dark Ride Return Path']), create_dungeon_region(player, 'TR Dash Bridge', 'Turtle Rock', None, ['TR Dash Bridge NW', 'TR Dash Bridge SW', 'TR Dash Bridge WS']), create_dungeon_region(player, 'TR Eye Bridge', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'], diff --git a/Rom.py b/Rom.py index 1260f616..7f1dfe90 100644 --- a/Rom.py +++ b/Rom.py @@ -43,7 +43,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'a84f59e5e76492f6a5693835823e6292' +RANDOMIZERBASEHASH = 'b439d99c7f41914eedb9f4097ecc8cc7' class JsonRom(object): @@ -690,7 +690,11 @@ def patch_rom(world, rom, player, team, is_mystery=False): rom.write_byte(0x157D0, exit.target) # setup dr option flags based on experimental, etc. - dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal + dr_flags = DROptions.NoOptions + if world.mirrorscroll[player] or world.doorShuffle[player] != 'vanilla': + dr_flags |= DROptions.Town_Portal + if world.doorShuffle[player] == 'vanilla': + dr_flags |= DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] not in ['vanilla', 'basic']: dr_flags |= DROptions.Map_Info if ((world.collection_rate[player] or world.goal[player] == 'completionist') diff --git a/Rules.py b/Rules.py index d5d14cfb..29c95a9b 100644 --- a/Rules.py +++ b/Rules.py @@ -601,10 +601,11 @@ def global_rules(world, player): lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player)) set_rule(world.get_entrance('TR Big Chest Entrance Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('TR Big Chest Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has_Boots(player)) - set_rule(world.get_entrance('TR Dark Ride Up Stairs', player), lambda state: state.has('Cane of Somaria', player)) - set_rule(world.get_entrance('TR Dark Ride SW', player), lambda state: state.has('Cane of Somaria', player)) - set_rule(world.get_entrance('TR Dark Ride Path', player), lambda state: state.has('Cane of Somaria', player)) - set_rule(world.get_entrance('TR Dark Ride Ledges Path', player), lambda state: state.has('Cane of Somaria', player)) + set_rule(world.get_entrance('TR Dark Ride SW', player), lambda state: state.has('Cane of Somaria', player)) # due to needing the switch + set_rule(world.get_entrance('TR Dark Ride Normal Path', player), lambda state: state.has('Cane of Somaria', player)) + set_rule(world.get_entrance('TR Dark Ride Backward Path', player), lambda state: state.has('Cane of Somaria', player)) + set_rule(world.get_entrance('TR Dark Ride Return Path', player), lambda state: state.has('Cane of Somaria', player)) + set_rule(world.get_entrance('TR Dark Ride Ledge Path', player), lambda state: state.has('Cane of Somaria', player)) for location in world.get_region('TR Dark Ride Ledges', player).locations: set_rule(location, lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Final Abyss Balcony Path', player), lambda state: state.has('Cane of Somaria', player)) @@ -1385,8 +1386,9 @@ def add_conditional_lamps(world, player): add_lamp_requirement(spot, player) dark_rooms = { - 'TR Dark Ride': {'sewer': False, 'entrances': ['TR Dark Ride Up Stairs', 'TR Dark Ride SW', 'TR Dark Ride Path'], 'locations': []}, - 'TR Dark Ride Ledges': {'sewer': False, 'entrances': ['TR Dark Ride Ledges Path'], 'locations': []}, + 'TR Dark Ride North Platform': {'sewer': False, 'entrances': ['TR Dark Ride Up Stairs', 'TR Dark Ride Normal Path', 'TR Dark Ride Ledge Path'], 'locations': []}, + 'TR Dark Ride South Platform': {'sewer': False, 'entrances': ['TR Dark Ride SW', 'TR Dark Ride Backward Path'], 'locations': []}, + 'TR Dark Ride Ledges': {'sewer': False, 'entrances': ['TR Dark Ride Return Path'], 'locations': []}, 'Mire Dark Shooters': {'sewer': False, 'entrances': ['Mire Dark Shooters Up Stairs', 'Mire Dark Shooters SW', 'Mire Dark Shooters SE'], 'locations': []}, 'Mire Key Rupees': {'sewer': False, 'entrances': ['Mire Key Rupees NE'], 'locations': []}, 'Mire Block X': {'sewer': False, 'entrances': ['Mire Block X NW', 'Mire Block X WS'], 'locations': []}, @@ -1920,7 +1922,7 @@ bunny_revivable_entrances = { "Ice Many Pots", "Mire South Fish", "Mire Right Bridge", "Mire Left Bridge", "TR Boss", "Eastern Hint Tile Blocked Path", "Thieves Spike Switch", "Thieves Boss", "Mire Spike Barrier", "Mire Cross", "Mire Hidden Shooters", - "Mire Spikes", "TR Final Abyss Balcony", "TR Dark Ride", "TR Pokey 1", "TR Tile Room", + "Mire Spikes", "TR Final Abyss Balcony", "TR Dark Ride South Platform", "TR Pokey 1", "TR Tile Room", "TR Roller Room", "Eastern Cannonball", "Thieves Hallway", "Ice Switch Room", "Mire Tile Room", "Mire Conveyor Crystal", "Mire Hub", "TR Dash Bridge", "TR Hub", "Eastern Boss", "Eastern Lobby", "Thieves Ambush", @@ -1988,8 +1990,8 @@ bunny_impassible_doors = { 'TR Lobby Ledge Gap', 'TR Hub SW', 'TR Hub SE', 'TR Hub ES', 'TR Hub EN', 'TR Hub NW', 'TR Hub NE', 'TR Hub Path', 'TR Hub Ledges Path', 'TR Torches NW', 'TR Pokey 2 Bottom to Top Barrier - Blue', 'TR Pokey 2 Top to Bottom Barrier - Blue', 'TR Twin Pokeys SW', 'TR Twin Pokeys EN', 'TR Big Chest Gap', - 'TR Big Chest Entrance Gap', 'TR Lazy Eyes ES', 'TR Tongue Pull WS', 'TR Tongue Pull NE', 'TR Dark Ride Up Stairs', - 'TR Dark Ride SW', 'TR Dark Ride Path', 'TR Dark Ride Ledges Path', + 'TR Big Chest Entrance Gap', 'TR Lazy Eyes ES', 'TR Tongue Pull WS', 'TR Tongue Pull NE', 'TR Dark Ride SW', # due to needing the switch + 'TR Dark Ride Normal Path', 'TR Dark Ride Ledge Path', 'TR Dark Ride Backward Path', 'TR Dark Ride Return Path', 'TR Crystal Maze Start to Interior Barrier - Blue', 'TR Crystal Maze End to Interior Barrier - Blue', 'TR Final Abyss Balcony Path', 'TR Final Abyss Ledge Path', 'GT Hope Room EN', 'GT Blocked Stairs Block Path', 'GT Bob\'s Room Hole', 'GT Speed Torch SE', 'GT Speed Torch South Path', 'GT Speed Torch North Path', diff --git a/data/base2current.bps b/data/base2current.bps index 8dc56259..e1a2fc7a 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/docs/Customizer.md b/docs/Customizer.md index 03908ea2..5173fccb 100644 --- a/docs/Customizer.md +++ b/docs/Customizer.md @@ -46,6 +46,13 @@ Player 1's settings will be determined by rolling the mystery weights and player Start inventory is not supported here. It has a separate section. +#### Extra Settings + +Some settings are intended to only be accessed using the customizer: + +* `aga_randomness` setting this to false, turns off blue balls and ganon warp randomness +* `money_balance` this is a percent (0-100). For numbers less than 100, both price balancing and money balancing will only attempt to ensure the player has access to only a percentage of the required funds. When 0, this should effectively disable money balancing. Grinding for rupees may be necessary whenever this is less than 100. + ###### Not Yet Implemented Rom/Adjust flags like sprite, quickswap are not outputing with the print_template_yaml or print_custom_yaml settings @@ -309,4 +316,19 @@ drops: Fish: Big Magic ``` -Prize packs expect a list of eight items each (anything not specified will be whatever randomization would have normally occurred). The special drops expect a single item. Packs 1 through 7 are supported. Prize pack 0 is not customizable. \ No newline at end of file +Prize packs expect a list of eight items each (anything not specified will be whatever randomization would have normally occurred). The special drops expect a single item. Packs 1 through 7 are supported. Prize pack 0 is not customizable. + +## prices + +This must be defined by player. You may have the prices of items in shops defined using the following + +``` +prices: + 1: + Capacity Upgrade - Left: 100 + Capacity Upgrade - Right: 200 + Dark Death Mountain Shop - Left: 100 + Dark Death Mountain Shop - Middle: 150 + Dark Death Mountain Shop - Right: 300 + Dark Lake Hylia Shop - Left: 200 +``` \ No newline at end of file diff --git a/docs/customizer_example.yaml b/docs/customizer_example.yaml index 5473f3d8..882a9e63 100644 --- a/docs/customizer_example.yaml +++ b/docs/customizer_example.yaml @@ -166,6 +166,10 @@ bosses: Palace of Darkness: Arrghus Thieves Town: Blind Ganons Tower (top): Vitreous +prices: + 1: + Capacity Upgrade - Left: 5 + Capacity Upgrade - Right: 216 start_inventory: 1: - Pegasus Boots diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 908ab362..8b9fa77c 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -553,6 +553,10 @@ "action": "store_true", "type": "bool" }, + "mirrorscroll": { + "action": "store_true", + "type": "bool" + }, "calc_playthrough": { "action": "store_false", "type": "bool" @@ -623,6 +627,7 @@ "action": "store_false", "type": "bool" }, + "money_balance": {}, "settingsonload": { "choices": [ "default", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 0ddb3c48..2bf10795 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -394,6 +394,7 @@ ], "collection_rate": [ "Display collection rate (default: %(default)s)" ], "pseudoboots": [ " Start with pseudo boots that allow dashing but no item checks (default: %(default)s)"], + "mirrorscroll": [ " Players starts with mirror scroll that allows mirror in dungeons but not overworld (default: %(default)s"], "bombbag": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ], "any_enemy_logic": [ "How to handle potential traversal between dungeon in Crossed door shuffle", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 5d5624c3..bf4dfa93 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -287,6 +287,7 @@ "randomizer.item.retro": "Retro mode", "randomizer.item.pseudoboots": "Pseudoboots", "randomizer.item.collection_rate": "Display Collection Rate", + "randomizer.item.mirrorscroll": "Mirror Scroll", "randomizer.item.worldstate": "World State", "randomizer.item.worldstate.standard": "Standard", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index bb1d6024..8531cf75 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -2,6 +2,7 @@ "checkboxes": { "hints": { "type": "checkbox" }, "pseudoboots": { "type": "checkbox" }, + "mirrorscroll": { "type": "checkbox" }, "collection_rate": {"type": "checkbox"}, "race": { "type": "checkbox" } }, diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index a636a776..dfc734e5 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -21,11 +21,20 @@ class CustomSettings(object): self.relative_dir = None self.world_rep = {} self.player_range = None + self.player_map = {} # player number to name def load_yaml(self, file): self.file_source = load_yaml(file) head, filename = os.path.split(file) self.relative_dir = head + if 'version' in self.file_source and self.file_source['version'].startswith('2'): + player_number = 1 + for key in self.file_source.keys(): + if key in ['meta', 'version']: + continue + else: + self.player_map[player_number] = key + player_number += 1 def determine_seed(self, default_seed): if 'meta' in self.file_source: @@ -179,6 +188,7 @@ class CustomSettings(object): args.restrict_boss_items[p] = get_setting(settings['restrict_boss_items'], args.restrict_boss_items[p]) args.overworld_map[p] = get_setting(settings['overworld_map'], args.overworld_map[p]) args.pseudoboots[p] = get_setting(settings['pseudoboots'], args.pseudoboots[p]) + args.mirrorscroll[p] = get_setting(settings['mirrorscroll'], args.mirrorscroll[p]) args.triforce_goal[p] = get_setting(settings['triforce_goal'], args.triforce_goal[p]) args.triforce_pool[p] = get_setting(settings['triforce_pool'], args.triforce_pool[p]) args.triforce_goal_min[p] = get_setting(settings['triforce_goal_min'], args.triforce_goal_min[p]) @@ -189,6 +199,7 @@ class CustomSettings(object): args.triforce_max_difference[p] = get_setting(settings['triforce_max_difference'], args.triforce_max_difference[p]) args.beemizer[p] = get_setting(settings['beemizer'], args.beemizer[p]) args.aga_randomness[p] = get_setting(settings['aga_randomness'], args.aga_randomness[p]) + args.money_balance[p] = get_setting(settings['money_balance'], args.money_balance[p]) # mystery usage args.usestartinventory[p] = get_setting(settings['usestartinventory'], args.usestartinventory[p]) @@ -219,6 +230,9 @@ class CustomSettings(object): return self.file_source['placements'] return None + def get_prices(self, player): + return self.get_attribute_by_player_composite('prices', player) + def get_advanced_placements(self): if 'advanced_placements' in self.file_source: return self.file_source['advanced_placements'] @@ -284,6 +298,34 @@ class CustomSettings(object): return self.file_source['enemies'] return None + + def get_attribute_by_player_composite(self, attribute, player): + attempt = self.get_attribute_by_player_new(attribute, player) + if attempt is not None: + return attempt + attempt = self.get_attribute_by_player(attribute, player) + return attempt + + def get_attribute_by_player(self, attribute, player): + if attribute in self.file_source: + if player in self.file_source[attribute]: + return self.file_source[attribute][player] + return None + + def get_attribute_by_player_new(self, attribute, player): + player_id = self.get_player_id(player) + if player_id is not None: + if attribute in self.file_source[player_id]: + return self.file_source[player_id][attribute] + return None + + def get_player_id(self, player): + if player in self.file_source: + return player + if player in self.player_map and self.player_map[player] in self.file_source: + return self.player_map[player] + return None + def create_from_world(self, world, settings): self.player_range = range(1, world.players + 1) settings_dict, meta_dict = {}, {} @@ -355,10 +397,12 @@ class CustomSettings(object): settings_dict[p]['linked_drops'] = world.linked_drops[p] settings_dict[p]['overworld_map'] = world.overworld_map[p] settings_dict[p]['pseudoboots'] = world.pseudoboots[p] + settings_dict[p]['mirrorscroll'] = world.mirrorscroll[p] settings_dict[p]['triforce_goal'] = world.treasure_hunt_count[p] settings_dict[p]['triforce_pool'] = world.treasure_hunt_total[p] settings_dict[p]['beemizer'] = world.beemizer[p] settings_dict[p]['aga_randomness'] = world.aga_randomness[p] + settings_dict[p]['money_balance'] = world.money_balance[p] if world.precollected_items: start_inv[p] = [] for item in world.precollected_items: diff --git a/source/classes/constants.py b/source/classes/constants.py index 14739fb8..7ceb16e3 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -57,6 +57,7 @@ SETTINGSTOPROCESS = { "item": { "hints": "hints", "pseudoboots": "pseudoboots", + "mirrorscroll": "mirrorscroll", 'collection_rate': 'collection_rate', "race": "race", diff --git a/source/dungeon/EnemyList.py b/source/dungeon/EnemyList.py index 315554a4..32909d37 100644 --- a/source/dungeon/EnemyList.py +++ b/source/dungeon/EnemyList.py @@ -1657,9 +1657,9 @@ def init_vanilla_sprites(): create_sprite(0x00b3, EnemySprite.Beamos, 0x00, 0, 0x06, 0x18, 'Mire Spikes') create_sprite(0x00b3, EnemySprite.FourWayShooter, 0x00, 0, 0x0a, 0x1a, 'Mire Spikes') create_sprite(0x00b3, EnemySprite.Stalfos, 0x00, 0, 0x07, 0x1c, 'Mire Spikes') - create_sprite(0x00b5, EnemySprite.FirebarCW, 0x00, 0, 0x16, 0x0a, 'TR Dark Ride') - create_sprite(0x00b5, EnemySprite.FirebarCW, 0x00, 0, 0x09, 0x0f, 'TR Dark Ride') - create_sprite(0x00b5, EnemySprite.FirebarCW, 0x00, 0, 0x16, 0x16, 'TR Dark Ride') + create_sprite(0x00b5, EnemySprite.FirebarCW, 0x00, 0, 0x16, 0x0a, 'TR Dark Ride Ledges') + create_sprite(0x00b5, EnemySprite.FirebarCW, 0x00, 0, 0x09, 0x0f, 'TR Dark Ride Ledges') + create_sprite(0x00b5, EnemySprite.FirebarCW, 0x00, 0, 0x16, 0x16, 'TR Dark Ride Ledges') create_sprite(0x00b6, EnemySprite.Chainchomp, 0x00, 0, 0x06, 0x07, 'TR Chain Chomps Top') create_sprite(0x00b6, EnemySprite.Chainchomp, 0x00, 0, 0x0a, 0x07, 'TR Chain Chomps Top') create_sprite(0x00b6, EnemySprite.CrystalSwitch, 0x00, 0, 0x03, 0x04) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 64f1461b..baa6b2c6 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -275,7 +275,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def): rem_entrances.update(lw_entrances) rem_entrances.update(dw_entrances) else: - # cross world mandantory + # cross world mandatory entrance_list = list(entrances) if avail.swapped: forbidden = [e for e in Forbidden_Swap_Entrances if e in entrance_list] @@ -321,16 +321,13 @@ def do_main_shuffle(entrances, exits, avail, mode_def): avail.decoupled_exits.remove(bomb_shop) rem_exits.remove(bomb_shop) - def bonk_fairy_exception(x): # (Bonk Fairy not eligible in standard) - return not avail.is_standard() or x != 'Bonk Fairy (Light)' - # old man S&Q cave if not cross_world and not avail.assumed_loose_caves: #TODO: Add Swapped ER support for this # OM Cave entrance in lw/dw if cross_world off if 'Old Man Cave Exit (West)' in rem_exits: world_limiter = DW_Entrances if avail.inverted else LW_Entrances - om_cave_options = sorted([x for x in rem_entrances if x in world_limiter and bonk_fairy_exception(x)]) + om_cave_options = sorted([x for x in rem_entrances if x in world_limiter and bonk_fairy_exception(avail, x)]) om_cave_choice = random.choice(om_cave_options) if not avail.coupled: connect_exit('Old Man Cave Exit (West)', om_cave_choice, avail) @@ -344,7 +341,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def): for ext in om_house: if ext in rem_exits: world_limiter = DW_Entrances if avail.inverted else LW_Entrances - om_house_options = [x for x in rem_entrances if x in world_limiter and bonk_fairy_exception(x)] + om_house_options = [x for x in rem_entrances if x in world_limiter and bonk_fairy_exception(avail, x)] om_house_choice = random.choice(om_house_options) if not avail.coupled: connect_exit(ext, om_house_choice, avail) @@ -361,7 +358,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def): lw_entrances, dw_entrances = [], [] left = sorted(rem_entrances) for x in left: - if bonk_fairy_exception(x): + if bonk_fairy_exception(avail, x): lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x) do_same_world_connectors(lw_entrances, dw_entrances, multi_exit_caves, avail) if avail.world.doorShuffle[avail.player] != 'vanilla': @@ -371,7 +368,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def): unused_entrances.update(lw_entrances) unused_entrances.update(dw_entrances) else: - entrance_list = sorted([x for x in rem_entrances if bonk_fairy_exception(x)]) + entrance_list = sorted([x for x in rem_entrances if bonk_fairy_exception(avail, x)]) do_cross_world_connectors(entrance_list, multi_exit_caves, avail) unused_entrances.update(entrance_list) @@ -1527,6 +1524,8 @@ def do_vanilla_connect(pool_def, avail): avail.entrances.remove(entrance) avail.exits.remove(target) +def bonk_fairy_exception(avail, x): # (Bonk Fairy not eligible in standard) + return not avail.is_standard() or x != 'Bonk Fairy (Light)' def do_mandatory_connections(avail, entrances, cave_options, must_exit): if len(must_exit) == 0: @@ -1614,7 +1613,8 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): if len(cave) == 2: entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] and e not in invalid_cave_connections[tuple(cave)] and e not in must_exit - and (not avail.swapped or rnd_cave[0] != avail.combine_map[e])) + and (not avail.swapped or rnd_cave[0] != avail.combine_map[e]) + and bonk_fairy_exception(avail, e)) entrances.remove(entrance) connect_two_way(entrance, rnd_cave[0], avail) if avail.swapped and avail.combine_map[entrance] != rnd_cave[0]: @@ -1639,7 +1639,7 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): cave_entrances.append(entrance) else: entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] and e not in must_exit - and (not avail.swapped or cave_exit != avail.combine_map[e])) + and (not avail.swapped or cave_exit != avail.combine_map[e]) and bonk_fairy_exception(avail, e)) cave_entrances.append(entrance) entrances.remove(entrance) connect_two_way(entrance, cave_exit, avail) @@ -1670,7 +1670,7 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): continue else: entrance = next(e for e in entrances[::-1] if e not in invalid_cave_connections[tuple(cave)] - and (not avail.swapped or cave_exit != avail.combine_map[e])) + and (not avail.swapped or cave_exit != avail.combine_map[e]) and bonk_fairy_exception(avail, e)) invalid_cave_connections[tuple(cave)] = set() entrances.remove(entrance) connect_two_way(entrance, cave_exit, avail) diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 6c347db3..93a92164 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -152,6 +152,7 @@ def roll_settings(weights): ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle != 'none' else 'off' ret.pseudoboots = get_choice_bool('pseudoboots') + ret.mirrorscroll = get_choice_bool('mirrorscroll') ret.shopsanity = get_choice_bool('shopsanity') keydropshuffle = get_choice_bool('keydropshuffle') ret.dropshuffle = get_choice('dropshuffle') if 'dropshuffle' in weights else 'none'