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/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/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..bc4f90a9 100644 --- a/ItemList.py +++ b/ItemList.py @@ -355,16 +355,6 @@ def generate_itempool(world, player): or (item.map and world.mapshuffle[player] not in ['none', 'nearby']) or (item.compass and world.compassshuffle[player] not in ['none', 'nearby']))]) - if world.logic[player] == 'hybridglitches' and world.pottery[player] not in ['none', 'cave']: - keys_to_remove = 2 - to_remove = [] - for wix, wi in enumerate(world.itempool): - if wi.name == 'Small Key (Swamp Palace)' and wi.player == player: - to_remove.append(wix) - if keys_to_remove == len(to_remove): - break - for wix in reversed(to_remove): - del world.itempool[wix] # logic has some branches where having 4 hearts is one possible requirement (of several alternatives) # rather than making all hearts/heart pieces progression items (which slows down generation considerably) @@ -837,7 +827,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 +845,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, upgrade.price, world, player) + rep_price = final_price(loc, 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 +859,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, upgrade.price, world, player) + rep_price = final_price(loc, 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 +870,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,6 +918,9 @@ 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) @@ -1255,32 +1262,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 +1308,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 +1318,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 +1334,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..ddb9fab9 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' 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 @@ -604,6 +606,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() @@ -817,6 +820,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/RELEASENOTES.md b/RELEASENOTES.md index de9fe6f6..c95e53d0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -141,20 +141,14 @@ 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 + - 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..006d9c73 100644 --- a/Rom.py +++ b/Rom.py @@ -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/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'