diff --git a/BaseClasses.py b/BaseClasses.py index 8014605a..8529e601 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -134,6 +134,8 @@ class World(object): set_player_attr('triforce_pieces_available', 30) set_player_attr('triforce_pieces_required', 20) set_player_attr('shop_shuffle', 'off') + set_player_attr('shop_shuffle_slots', 0) + set_player_attr('potion_shop_shuffle', 'none') set_player_attr('shuffle_prizes', "g") set_player_attr('sprite_pool', []) set_player_attr('dark_room_logic', "lamp") @@ -1158,7 +1160,8 @@ class Shop(): 'max': max, 'replacement': replacement, 'replacement_price': replacement_price, - 'create_location': create_location + 'create_location': create_location, + 'player': 0 } def push_inventory(self, slot: int, item: str, price: int, max: int = 1): @@ -1171,7 +1174,8 @@ class Shop(): 'max': max, 'replacement': self.inventory[slot]["item"], 'replacement_price': self.inventory[slot]["price"], - 'create_location': self.inventory[slot]["create_location"] + 'create_location': self.inventory[slot]["create_location"], + 'player': self.inventory[slot]["player"] } @@ -1257,6 +1261,10 @@ class Spoiler(object): if item is None: continue shopdata['item_{}'.format(index)] = "{} — {}".format(item['item'], item['price']) if item['price'] else item['item'] + + if item['player'] > 0: + shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('—', '(Player {}) — '.format(item['player'])) + if item['max'] == 0: continue shopdata['item_{}'.format(index)] += " x {}".format(item['max']) @@ -1330,6 +1338,8 @@ class Spoiler(object): 'triforce_pieces_available': self.world.triforce_pieces_available, 'triforce_pieces_required': self.world.triforce_pieces_required, 'shop_shuffle': self.world.shop_shuffle, + 'shop_shuffle_slots': self.world.shop_shuffle_slots, + 'potion_shop_shuffle': self.world.potion_shop_shuffle, 'shuffle_prizes': self.world.shuffle_prizes, 'sprite_pool': self.world.sprite_pool, 'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index 5b77e29a..2bfe47f4 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -326,10 +326,21 @@ def parse_arguments(argv, no_defaults=False): parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4)) parser.add_argument('--shop_shuffle', default='', help='''\ combine letters for options: - i: shuffle the inventories of the shops around + g: generate default inventories for light and dark world shops, and unique shops + f: generate default inventories for each shop individually + i: shuffle the default inventories of the shops around p: randomize the prices of the items in shop inventories u: shuffle capacity upgrades into the item pool ''') + parser.add_argument('--shop_shuffle_slots', default=defval(0), + type=lambda value: min(max(int(value), 1), 96), + help=''' + Maximum amount of shop slots able to be filled by items from the item pool. + ''') + parser.add_argument('--potion_shop_shuffle', default=defval('none'), choices=['none', 'a'], help='''\ + Determine if potion shop shuffle items should be affected by the rules of shop shuffle. + Value `none` will only allow prices to be shuffled, `a` will allow any items to be shuffled. + ''') parser.add_argument('--shuffle_prizes', default=defval('g'), choices=['', 'g', 'b', 'gb']) parser.add_argument('--sprite_pool', help='''\ Specifies a colon separated list of sprites used for random/randomonevent. If not specified, the full sprite pool is used.''') @@ -390,7 +401,8 @@ def parse_arguments(argv, no_defaults=False): 'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', "skip_progression_balancing", "triforce_pieces_available", - "triforce_pieces_required", "shop_shuffle", "required_medallions", + "triforce_pieces_required", "shop_shuffle", "shop_shuffle_slots", "potion_shop_shuffle", + "required_medallions", "plando_items", "plando_texts", "plando_connections", 'remote_items', 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves', 'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic', diff --git a/Fill.py b/Fill.py index 218c81c2..e851208d 100644 --- a/Fill.py +++ b/Fill.py @@ -171,6 +171,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None fill_locations.remove(spot_to_fill) world.random.shuffle(fill_locations) + prioitempool, fill_locations = fast_fill(world, prioitempool, fill_locations) restitempool, fill_locations = fast_fill(world, restitempool, fill_locations) @@ -247,6 +248,7 @@ def flood_items(world): itempool.remove(item_to_place) break + def balance_multiworld_progression(world): balanceable_players = {player for player in range(1, world.players + 1) if world.progression_balancing[player]} if not balanceable_players: diff --git a/ItemPool.py b/ItemPool.py index 6e9462a9..cab11009 100644 --- a/ItemPool.py +++ b/ItemPool.py @@ -481,18 +481,23 @@ def shuffle_shops(world, items, player: int): shops = [] upgrade_shops = [] total_inventory = [] + potion_option = world.potion_shop_shuffle[player] for shop in world.shops: if shop.region.player == player: if shop.type == ShopType.UpgradeShop: upgrade_shops.append(shop) - elif shop.type == ShopType.Shop and shop.region.name != 'Potion Shop': - shops.append(shop) - total_inventory.extend(shop.inventory) + elif shop.type == ShopType.Shop: + if shop.region.name == 'Potion Shop' and potion_option in [None, '', 'none']: + upgrade_shops.append(shop) # just put it with the upgrade shops/caves so we don't shuffle the items, just prices + else: + shops.append(shop) + total_inventory.extend(shop.inventory) if 'p' in option: def price_adjust(price: int) -> int: # it is important that a base price of 0 always returns 0 as new price! - return int(price * (0.5 + world.random.random() * 1.5)) + adjust = 2 if price < 100 else 5 + return int((price / adjust) * (0.5 + world.random.random() * 1.5)) * adjust def adjust_item(item): if item: @@ -507,6 +512,7 @@ def shuffle_shops(world, items, player: int): if 'i' in option: world.random.shuffle(total_inventory) + i = 0 for shop in shops: slots = shop.slots @@ -577,13 +583,13 @@ def create_dynamic_shop_locations(world, player): if item is None: continue if item['create_location']: - loc = Location(player, "{} Item {}".format(shop.region.name, i+1), parent=shop.region) + loc = Location(player, "{} Shop Slot {}".format(shop.region.name, i+1), parent=shop.region) shop.region.locations.append(loc) world.dynamic_locations.append(loc) world.clear_location_cache() - world.push_item(loc, ItemFactory(item['item'], player), False) + world.push_item(loc, ItemFactory(item['item'], player), False) loc.event = True loc.locked = True diff --git a/Main.py b/Main.py index 5d6577bc..09b21766 100644 --- a/Main.py +++ b/Main.py @@ -83,6 +83,8 @@ def main(args, seed=None): world.triforce_pieces_available = args.triforce_pieces_available.copy() world.triforce_pieces_required = args.triforce_pieces_required.copy() world.shop_shuffle = args.shop_shuffle.copy() + world.shop_shuffle_slots = args.shop_shuffle_slots.copy() + world.potion_shop_shuffle = args.potion_shop_shuffle.copy() world.progression_balancing = {player: not balance for player, balance in args.skip_progression_balancing.items()} world.shuffle_prizes = args.shuffle_prizes.copy() world.sprite_pool = args.sprite_pool.copy() @@ -205,10 +207,74 @@ def main(args, seed=None): distribute_items_restrictive(world, True) elif args.algorithm == 'balanced': distribute_items_restrictive(world, True) - + if world.players > 1: balance_multiworld_progression(world) + candidates = [l for l in world.get_locations() if l.item.name in ['Bee Trap', 'Shovel', 'Bug Catching Net', 'Cane of Byrna', 'Triforce Piece'] or + any([x in l.item.name for x in ['Key', 'Map', 'Compass', 'Clock', 'Heart', 'Sword', 'Shield', 'Bomb', 'Arrow', 'Mail']])] + world.random.shuffle(candidates) + shop_slots = [item for sublist in [shop.region.locations for shop in world.shops] for item in sublist if item.name != 'Potion Shop'] + shop_slots_adjusted = [] + shop_items = [] + + for location in shop_slots: + slot_num = int(location.name[-1]) - 1 + shop_item = location.parent_region.shop.inventory[slot_num] + item = location.item + # if item is a rupee or single bee, or identical, swap it out + if (shop_item is not None and shop_item['item'] == item.name) or 'Rupee' in item.name or (item.name in ['Bee']): + for c in candidates: # chosen item locations + if 'Rupee' in c.item.name or c.item.name in 'Bee': continue + if (shop_item is not None and shop_item['item'] == c.item.name): continue + if c.item_rule(location.item): # if rule is good... + logging.debug('Swapping {} with {}:: {} ||| {}'.format(c, location, c.item, location.item)) + c.item, location.item = location.item, c.item + if not world.can_beat_game(): + c.item, location.item = location.item, c.item + else: + shop_slots_adjusted.append(location) + break + # update table to location data + item = location.item + if (shop_item is not None and shop_item['item'] == item.name) or 'Rupee' in item.name or (item.name in ['Bee']): + # this will filter items that match the item in the shop or Rupees, or single bees + # really not a way for the player to know a renewable item from a player pool item + # bombs can be sitting on top of arrows or a potion refill, but dunno if that's a big deal + # this should rarely happen with the above code in place, and could be an option in config if necessary + logging.debug('skipping item shop {}'.format(item.name)) + else: + if shop_item is None: + location.parent_region.shop.add_inventory(slot_num, 'None', 0) + shop_item = location.parent_region.shop.inventory[slot_num] + else: + shop_item['replacement'] = shop_item['item'] + shop_item['replacement_price'] = shop_item['price'] + shop_item['item'] = item.name + if any([x in shop_item['item'] for x in ['Single Bomb', 'Single Arrow']]): + shop_item['price'] = world.random.randrange(5,35) + elif any([x in shop_item['item'] for x in ['Arrows', 'Bombs', 'Clock']]): + shop_item['price'] = world.random.randrange(20,120) + elif any([x in shop_item['item'] for x in ['Universal Key', 'Compass', 'Map', 'Small Key', 'Piece of Heart']]): + shop_item['price'] = world.random.randrange(50,150) + else: + shop_item['price'] = world.random.randrange(50,300) + + shop_item['max'] = 1 + shop_item['player'] = item.player if item.player != location.player else 0 + shop_items.append(shop_item) + + if len(shop_items) > 0: + my_prices = [my_item['price'] for my_item in shop_items] + price_scale = (80*max(8, len(my_prices)+2))/sum(my_prices) + for i in shop_items: + i['price'] *= price_scale + if i['price'] < 5: i['price'] = 5 + else: i['price'] = int((i['price']//5)*5) + + logging.debug('Adjusting {} of {} shop slots'.format(len(shop_slots_adjusted), len(shop_slots))) + logging.debug('Adjusted {} into shops'.format([x.item.name for x in shop_slots_adjusted])) + logger.info('Patching ROM.') outfilebase = 'BM_%s' % (args.outputname if args.outputname else world.seed) @@ -336,6 +402,7 @@ def main(args, seed=None): main_entrance = get_entrance_to_region(region) for location in region.locations: if type(location.address) == int: # skips events and crystals + if location.address >= 0x400000: continue if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name: er_hint_data[region.player][location.address] = main_entrance.name diff --git a/MultiClient.py b/MultiClient.py index f93544eb..5364d799 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -158,6 +158,11 @@ SCOUT_LOCATION_ADDR = SAVEDATA_START + 0x4D7 # 1 byte SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8 # 1 byte SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte +SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes + + +location_shop_order = [ name for name, info in Regions.shop_table.items() ] # probably don't leave this here. This relies on python 3.6+ dictionary keys having defined order +location_shop_ids = set([info[0] for name, info in Regions.shop_table.items()]) location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10), "Blind's Hideout - Left": (0x11d, 0x20), @@ -1158,6 +1163,18 @@ async def track_locations(ctx : Context, roomid, roomdata): ctx.unsafe_locations_checked.add(location) ctx.ui_node.log_info("New check: %s (%d/216)" % (location, len(ctx.unsafe_locations_checked))) ctx.ui_node.send_location_check(ctx, location) + + try: + if roomid in location_shop_ids: + misc_data = await snes_read(ctx, SHOP_ADDR, len(location_shop_order)*3) + for cnt, b in enumerate(misc_data): + my_check = Regions.shop_table_by_location_id[0x400000 + cnt] + if int(b) > 0 and my_check not in ctx.unsafe_locations_checked: + new_check(my_check) + except Exception as e: + print(e) + ctx.ui_node.log_info(f"Exception: {e}") + for location, (loc_roomid, loc_mask) in location_table_uw.items(): try: @@ -1217,7 +1234,13 @@ async def track_locations(ctx : Context, roomid, roomdata): for location in ctx.unsafe_locations_checked: if (location in ctx.items_missing and location not in ctx.locations_checked) or ctx.send_unsafe: ctx.locations_checked.add(location) - new_locations.append(Regions.lookup_name_to_id[location]) + try: + my_id = Regions.lookup_name_to_id.get(location, Regions.shop_table_by_location.get(location, -1)) + new_locations.append(my_id) + except Exception as e: + print(e) + ctx.ui_node.log_info(f"Exception: {e}") + await ctx.send_msgs([['LocationChecks', new_locations]]) diff --git a/MultiServer.py b/MultiServer.py index e9d55490..ccdd6c24 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -969,7 +969,7 @@ def get_missing_checks(ctx: Context, client: Client) -> list: #for location_id in [k[0] for k, v in ctx.locations if k[1] == client.slot]: # if location_id not in ctx.location_checks[client.team, client.slot]: # locations.append(Regions.lookup_id_to_name.get(location_id, f'Unknown Location ID: {location_id}')) - for location_id, location_name in Regions.lookup_id_to_name.items(): # cheat console is -1, keep in mind + for location_id, location_name in {**Regions.lookup_id_to_name, **Regions.shop_table_by_location_id}.items(): # cheat console is -1, keep in mind if location_id != -1 and location_id not in ctx.location_checks[client.team, client.slot] and (location_id, client.slot) in ctx.locations: locations.append(location_name) return locations diff --git a/Mystery.py b/Mystery.py index a954c326..a984e51c 100644 --- a/Mystery.py +++ b/Mystery.py @@ -405,10 +405,16 @@ def roll_settings(weights, plando_options: typing.Set[str] = frozenset(("bosses" # change minimum to required pieces to avoid problems ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90) + ret.shop_shuffle_slots = int(get_choice('shop_shuffle_slots', weights, '0')) + ret.shop_shuffle = get_choice('shop_shuffle', weights, '') if not ret.shop_shuffle: ret.shop_shuffle = '' + ret.potion_shop_shuffle = get_choice('potion_shop_shuffle', weights, '') + if not ret.potion_shop_shuffle: + ret.potion_shop_shuffle = '' + ret.mode = get_choice('world_state', weights, None) # legacy support if ret.mode == 'retro': ret.mode = 'open' diff --git a/Regions.py b/Regions.py index 4326e7d1..07b9b079 100644 --- a/Regions.py +++ b/Regions.py @@ -369,7 +369,39 @@ def create_shops(world, player: int): cls_mapping = {ShopType.UpgradeShop: UpgradeShop, ShopType.Shop: Shop, ShopType.TakeAny: TakeAny} - for region_name, (room_id, type, shopkeeper, custom, locked, inventory) in shop_table.items(): + option = world.shop_shuffle[player] + potion_option = world.potion_shop_shuffle[player] + my_shop_table = dict(shop_table) + + num_slots = int(world.shop_shuffle_slots[player]) + + my_shop_slots = ([True] * num_slots + [False] * (len(shop_table) * 3))[:len(shop_table)*3 - 2] + + world.random.shuffle(my_shop_slots) + + from Items import ItemFactory + if 'g' in option or 'f' in option: + new_basic_shop = world.random.sample(shop_generation_types['default'], k=3) + new_dark_shop = world.random.sample(shop_generation_types['default'], k=3) + for name, shop in my_shop_table.items(): + typ, shop_id, keeper, custom, locked, items = shop + new_items = world.random.sample(shop_generation_types['default'], k=3) + if 'f' not in option: + if items == _basic_shop_defaults: + new_items = new_basic_shop + elif items == _dark_world_shop_defaults: + new_items = new_dark_shop + if name == 'Capacity Upgrade': + continue + if name == 'Potion Shop': + if 'b' in potion_option: + new_items = world.random.sample(shop_generation_types['potion_discount'] + shop_generation_types['bottle'], k=3) + elif 'a' not in potion_option: + new_items = items + keeper = world.random.choice([0xA0, 0xC1, 0xFF]) + my_shop_table[name] = (typ, shop_id, keeper, custom, locked, new_items) + + for region_name, (room_id, type, shopkeeper, custom, locked, inventory) in my_shop_table.items(): if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop': locked = True inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)] @@ -379,6 +411,21 @@ def create_shops(world, player: int): world.shops.append(shop) for index, item in enumerate(inventory): shop.add_inventory(index, *item) + if region_name == 'Potion Shop' and 'a' not in potion_option: + pass + elif region_name == 'Capacity Upgrade': + pass + else: + if my_shop_slots.pop(): + additional_item = world.random.choice(['Rupees (50)', 'Rupees (100)', 'Rupees (300)']) + world.itempool.append(ItemFactory(additional_item, player)) + slot_name = "{} Shop Slot {}".format(shop.region.name, index+1) + loc = Location(player, slot_name, address=shop_table_by_location[slot_name], parent=shop.region) + shop.region.locations.append(loc) + world.dynamic_locations.append(loc) + + world.clear_location_cache() + # (type, room_id, shopkeeper, custom, locked, [items]) # item = (item, price, max=0, replacement=None, replacement_price=0) @@ -394,10 +441,21 @@ shop_table = { 'Light World Death Mountain Shop': (0x00FF, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults), 'Kakariko Shop': (0x011F, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults), 'Cave Shop (Lake Hylia)': (0x0112, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults), - 'Potion Shop': (0x0109, ShopType.Shop, 0xFF, False, True, [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)]), + 'Potion Shop': (0x0109, ShopType.Shop, 0xA0, True, False, [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)]), 'Capacity Upgrade': (0x0115, ShopType.UpgradeShop, 0x04, True, True, [('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)]) } +shop_table_by_location_id = {0x400000 + cnt: s for cnt, s in enumerate( [item for sublist in [ ["{} Shop Slot {}".format(name, num + 1) for num in range(3)] for name in shop_table ] for item in sublist])} +shop_table_by_location = {y:x for x,y in shop_table_by_location_id.items()} + +shop_generation_types = { + 'default': _basic_shop_defaults + [('Bombs (3)', 20), ('Green Potion', 90), ('Blue Potion', 190), ('Bee', 10), ('Single Arrow', 5), ('Single Bomb', 10)] + [('Red Shield', 500), ('Blue Shield', 50)], + 'potion': [('Red Potion', 150), ('Green Potion', 90), ('Blue Potion', 190)], + 'discount_potion': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)], + 'bottle': [('Bee', 10)], + 'time': [('Red Clock', 100), ('Blue Clock', 200), ('Green Clock', 300)], +} + old_location_address_to_new_location_address = { 0x2eb18: 0x18001b, # Bottle Merchant 0x33d68: 0x18001a, # Purple Chest @@ -705,8 +763,10 @@ location_table: typing.Dict[str, lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int} lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}, -1: "cheat console"} +lookup_id_to_name.update(shop_table_by_location_id) lookup_name_to_id = {name: data[0] for name, data in location_table.items() if type(data[0]) == int} lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()}, "cheat console": -1} +lookup_name_to_id.update(shop_table_by_location) lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 191256: 'Kings Grave Inner Rocks', 1573194: 'Kings Grave Inner Rocks', 1573189: 'Kings Grave Inner Rocks', diff --git a/Rom.py b/Rom.py index cc5d14cb..d77685e2 100644 --- a/Rom.py +++ b/Rom.py @@ -1,7 +1,7 @@ from __future__ import annotations JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '5a607e36a82bbd14180536c8ec3ae49b' +RANDOMIZERBASEHASH = '0954a778832b76ba4f96b10eb527ee83' import io import json @@ -123,6 +123,9 @@ class LocalRom(object): Patch.create_patch_file(local_path('basepatch.sfc')) return + if not os.path.isfile(local_path('data', 'basepatch.bmbp')): + raise RuntimeError('Base patch unverified. Unable to continue.') + if os.path.isfile(local_path('data', 'basepatch.bmbp')): _, target, buffer = Patch.create_rom_bytes(local_path('data', 'basepatch.bmbp')) if self.verify(buffer): @@ -130,6 +133,7 @@ class LocalRom(object): with open(local_path('basepatch.sfc'), 'wb') as stream: stream.write(buffer) return + raise RuntimeError('Base patch unverified. Unable to continue.') raise RuntimeError('Could not find Base Patch. Unable to continue.') @@ -677,6 +681,7 @@ def patch_rom(world, rom, player, team, enemized): distinguished_prog_bow_loc.item.code = 0x65 # patch items + for location in world.get_locations(): if location.player != player: continue @@ -686,6 +691,9 @@ def patch_rom(world, rom, player, team, enemized): if location.address is None: continue + if 'Shop Slot' in location.name and location.parent_region.shop is not None: + continue + if not location.crystal: if location.item is not None: # Keys in their native dungeon should use the orignal item code for keys @@ -724,6 +732,7 @@ def patch_rom(world, rom, player, team, enemized): for music_address in music_addresses: rom.write_byte(music_address, music) + if world.mapshuffle[player]: rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle @@ -1564,14 +1573,13 @@ def write_custom_shops(rom, world, player): else: sram_offset += shop.item_count shop_data.extend(bytes) - # [id][item][price-low][price-high][max][repl_id][repl_price-low][repl_price-high] + # [id][item][price-low][price-high][max][repl_id][repl_price-low][repl_price-high][player] for item in shop.inventory: if item is None: break - item_data = [shop_id, ItemFactory(item['item'], player).code] + int16_as_bytes(item['price']) + [ - item['max'], + item_data = [shop_id, ItemFactory(item['item'], player).code] + int16_as_bytes(item['price']) + [ item['max'],\ ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + int16_as_bytes( - item['replacement_price']) + item['replacement_price']) + [item['player']] items_data.extend(item_data) rom.write_bytes(0x184800, shop_data) diff --git a/Rules.py b/Rules.py index 02bd8545..99d1c6a1 100644 --- a/Rules.py +++ b/Rules.py @@ -85,7 +85,7 @@ def set_rules(world, player): add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.world.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or') set_bunny_rules(world, player, world.mode[player] == 'inverted') - + def mirrorless_path_to_castle_courtyard(world, player): # If Agahnim is defeated then the courtyard needs to be accessible without using the mirror for the mirror offset glitch. diff --git a/WebHostLib/static/static/playerSettings.json b/WebHostLib/static/static/playerSettings.json index d57fb589..ca9ae329 100644 --- a/WebHostLib/static/static/playerSettings.json +++ b/WebHostLib/static/static/playerSettings.json @@ -477,9 +477,23 @@ "name": "None", "value": "none" }, - { - "name": "Inventory", - "value": "i" + "g": { + "keyString": "shop_shuffle.g", + "friendlyName": "Inventory Generate", + "description": "Generates new default base inventories of overworld and underworld shops.", + "defaultValue": 0 + }, + "f": { + "keyString": "shop_shuffle.f", + "friendlyName": "Full Inventory Generate", + "description": "Generates new base inventories of each individual shop.", + "defaultValue": 0 + }, + "i": { + "keyString": "shop_shuffle.i", + "friendlyName": "Inventory Shuffle", + "description": "Shuffles the inventories of shops between each other.", + "defaultValue": 0 }, { "name": "Prices", @@ -493,9 +507,269 @@ "name": "Inventory and Prices", "value": "ip" }, - { - "name": "Inventory, Prices, and Upgrades", - "value": "ipu" + "uip": { + "keyString": "shop_shuffle.uip", + "friendlyName": "Full Shuffle", + "description": "Shuffles the inventory and randomizes the prices of items in shops. Also distributes capacity upgrades throughout the world.", + "defaultValue": 0 + } + } + }, + "shop_shuffle_slots": { + "keyString": "shop_shuffle_slots", + "friendlyName": "Shop Shuffle Slots", + "description": "How Many Slots in Shops are dedicated to items from the item pool", + "inputType": "range", + "subOptions": { + "0": { + "keyString": "shop_shuffle_slots.0", + "friendlyName": 0, + "description": "0 slots", + "defaultValue": 50 + }, + "15": { + "keyString": "shop_shuffle_slots.3", + "friendlyName": 3, + "description": "3 slots", + "defaultValue": 0 + }, + "20": { + "keyString": "shop_shuffle_slots.6", + "friendlyName": 6, + "description": "6 slots", + "defaultValue": 0 + }, + "30": { + "keyString": "shop_shuffle_slots.12", + "friendlyName": 12, + "description": "12 slots", + "defaultValue": 0 + }, + "40": { + "keyString": "shop_shuffle_slots.96", + "friendlyName": 96, + "description": "96 slots", + "defaultValue": 0 + } + } + }, + "potion_shop_shuffle": { + "keyString": "potion_shop_shuffle", + "friendlyName": "Potion Shop Shuffle Rules", + "description": "Influence on potion shop by shop shuffle options", + "inputType": "range", + "subOptions": { + "none": { + "keyString": "potion_shop_shuffle.none", + "friendlyName": "Vanilla Shops", + "description": "Shop contents are left unchanged, only prices.", + "defaultValue": 50 + }, + "a": { + "keyString": "potion_shop_shuffle.a", + "friendlyName": "Any Items can be shuffled in and out of the shop", + "description": "", + "defaultValue": 0 + } + } + }, + "shuffle_prizes": { + "keyString": "shuffle_prizes", + "friendlyName": "Prize Shuffle", + "description": "Alters the Prizes from pulling, bonking, enemy kills, digging, and hoarders", + "inputType": "range", + "subOptions": { + "none": { + "keyString": "shuffle_prizes.none", + "friendlyName": "None", + "description": "All prizes from pulling, bonking, enemy kills, digging, hoarders are vanilla.", + "defaultValue": 0 + }, + "g": { + "keyString": "shuffle_prizes.g", + "friendlyName": "\"General\" prize shuffle", + "description": "Shuffles the prizes from pulling, enemy kills, digging, hoarders", + "defaultValue": 50 + }, + "b": { + "keyString": "shuffle_prizes.b", + "friendlyName": "Bonk prize shuffle", + "description": "Shuffles the prizes from bonking into trees.", + "defaultValue": 0 + }, + "bg": { + "keyString": "shuffle_prizes.bg", + "friendlyName": "Both", + "description": "Shuffles both of the options.", + "defaultValue": 0 + } + } + }, + "timer": { + "keyString": "timer", + "friendlyName": "Timed Modes", + "description": "Add a timer to the game UI, and cause it to have various effects.", + "inputType": "range", + "subOptions": { + "none": { + "keyString": "timer.none", + "friendlyName": "Disabled", + "description": "No timed mode is applied to the game.", + "defaultValue": 50 + }, + "timed": { + "keyString": "timer.timed", + "friendlyName": "Timed Mode", + "description": "Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end.", + "defaultValue": 0 + }, + "timed_ohko": { + "keyString": "timer.timed_ohko", + "friendlyName": "Timed OHKO", + "description": "Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit.", + "defaultValue": 0 + }, + "ohko": { + "keyString": "timer.ohko", + "friendlyName": "One-Hit KO", + "description": "Timer always at zero. Permanent OHKO.", + "defaultValue": 0 + }, + "timed_countdown": { + "keyString": "timer.timed_countdown", + "friendlyName": "Timed Countdown", + "description": "Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.", + "defaultValue": 0 + }, + "display": { + "keyString": "timer.display", + "friendlyName": "Timer Only", + "description": "Displays a timer, but otherwise does not affect gameplay or the item pool.", + "defaultValue": 0 + } + } + }, + "countdown_start_time": { + "keyString": "countdown_start_time", + "friendlyName": "Countdown Starting Time", + "description": "The amount of time, in minutes, to start with in Timed Countdown and Timed OHKO modes.", + "inputType": "range", + "subOptions": { + "0": { + "keyString": "countdown_start_time.0", + "friendlyName": 0, + "description": "Start with no time on the timer. In Timed OHKO mode, start in OHKO mode.", + "defaultValue": 0 + }, + "10": { + "keyString": "countdown_start_time.10", + "friendlyName": 10, + "description": "Start with 10 minutes on the timer.", + "defaultValue": 50 + }, + "20": { + "keyString": "countdown_start_time.20", + "friendlyName": 20, + "description": "Start with 20 minutes on the timer.", + "defaultValue": 0 + }, + "30": { + "keyString": "countdown_start_time.30", + "friendlyName": 30, + "description": "Start with 30 minutes on the timer.", + "defaultValue": 0 + }, + "60": { + "keyString": "countdown_start_time.60", + "friendlyName": 60, + "description": "Start with an hour on the timer.", + "defaultValue": 0 + } + } + }, + "red_clock_time": { + "keyString": "red_clock_time", + "friendlyName": "Red Clock Time", + "description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a red clock.", + "inputType": "range", + "subOptions": { + "-2": { + "keyString": "red_clock_time.-2", + "friendlyName": -2, + "description": "Subtract 2 minutes from the timer upon picking up a red clock.", + "defaultValue": 0 + }, + "1": { + "keyString": "red_clock_time.1", + "friendlyName": 1, + "description": "Add a minute to the timer upon picking up a red clock.", + "defaultValue": 50 + } + } + }, + "blue_clock_time": { + "keyString": "blue_clock_time", + "friendlyName": "Blue Clock Time", + "description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a blue clock.", + "inputType": "range", + "subOptions": { + "1": { + "keyString": "blue_clock_time.1", + "friendlyName": 1, + "description": "Add a minute to the timer upon picking up a blue clock.", + "defaultValue": 0 + }, + "2": { + "keyString": "blue_clock_time.2", + "friendlyName": 2, + "description": "Add 2 minutes to the timer upon picking up a blue clock.", + "defaultValue": 50 + } + } + }, + "green_clock_time": { + "keyString": "green_clock_time", + "friendlyName": "Green Clock Time", + "description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a green clock.", + "inputType": "range", + "subOptions": { + "4": { + "keyString": "green_clock_time.4", + "friendlyName": 4, + "description": "Add 4 minutes to the timer upon picking up a green clock.", + "defaultValue": 50 + }, + "10": { + "keyString": "green_clock_time.10", + "friendlyName": 10, + "description": "Add 10 minutes to the timer upon picking up a green clock.", + "defaultValue": 0 + }, + "15": { + "keyString": "green_clock_time.15", + "friendlyName": 15, + "description": "Add 15 minutes to the timer upon picking up a green clock.", + "defaultValue": 0 + } + } + }, + "glitch_boots": { + "keyString": "glitch_boots", + "friendlyName": "Glitch Boots", + "description": "Start with Pegasus Boots in any glitched logic mode that makes use of them.", + "inputType": "range", + "subOptions": { + "on": { + "keyString": "glitch_boots.on", + "friendlyName": "On", + "description": "Enable glitch boots.", + "defaultValue": 50 + }, + "off": { + "keyString": "glitch_boots.off", + "friendlyName": "Off", + "description": "Disable glitch boots.", + "defaultValue": 0 } ] } diff --git a/data/basepatch.bmbp b/data/basepatch.bmbp index 564edb17..6cba8ef7 100644 Binary files a/data/basepatch.bmbp and b/data/basepatch.bmbp differ diff --git a/playerSettings.yaml b/playerSettings.yaml index 22b40aa9..396fc930 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -211,12 +211,24 @@ beemizer: # Remove items from the global item pool and replace them with single 2: 0 # 60% of the non-essential item pool is replaced with bee traps, of which 20% could be single bees 3: 0 # 100% of the non-essential item pool is replaced with bee traps, of which 50% could be single bees 4: 0 # 100% of the non-essential item pool is replaced with bee traps +### Item Shuffle (shop) +shop_shuffle_slots: # Maximum amount of allowed shop slots to place item pool items + 0: 50 + 5: 0 + 15: 0 + 999: 0 +potion_shop_shuffle: # influence of potion shop by shop shuffle + none: 50 # only shuffle price + a: 0 # generate/shuffle in any items shop_shuffle: none: 50 - i: 0 # Shuffle the inventories of the shops around + g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops + f: 0 # Generate new default inventories for every shop independently + i: 0 # Shuffle default inventories of the shops around p: 0 # Randomize the prices of the items in shop inventories u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld) ip: 0 # Shuffle inventories and randomize prices + fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool # You can add more combos shuffle_prizes: # aka drops @@ -253,11 +265,6 @@ green_clock_time: # For all timer modes, the amount of time in minutes to gain o # - "Small Keys" # - "Big Keys" # Can be uncommented to use it -# non_local_items: # Force certain items to appear outside your world only, always across the multiworld. Recognizes some group names, like "Swords" -# - "Moon Pearl" -# - "Small Keys" -# - "Big Keys" -# Can be uncommented to use it # startinventory: # Begin the file with the listed items/upgrades # Pegasus Boots: on # Bomb Upgrade (+10): 4