diff --git a/BaseClasses.py b/BaseClasses.py index 30e6f216..110f003c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -5,7 +5,7 @@ from enum import Enum, unique import logging import json from collections import OrderedDict, Counter, deque -from typing import Union, Optional +from typing import Union, Optional, List import secrets import random @@ -521,6 +521,10 @@ class CollectionState(object): return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for shop in self.world.shops) + def can_buy(self, item: str, player: int) -> bool: + return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(self) for + shop in self.world.shops) + def item_count(self, item, player: int) -> int: return self.prog_items[item, player] @@ -598,7 +602,7 @@ class CollectionState(object): def can_shoot_arrows(self, player: int) -> bool: if self.world.retro[player]: # TODO: Progressive and Non-Progressive silvers work differently (progressive is not usable until the shop arrow is bought) - return (self.has('Bow', player) or self.has('Silver Bow', player)) and self.can_buy_unlimited('Single Arrow', player) + return (self.has('Bow', player) or self.has('Silver Bow', player)) and self.can_buy('Single Arrow', player) return self.has('Bow', player) or self.has('Silver Bow', player) def can_get_good_bee(self, player: int) -> bool: @@ -1054,11 +1058,12 @@ class ShopType(Enum): class Shop(object): slots = 3 - def __init__(self, region, room_id, type, shopkeeper_config, custom, locked): + + def __init__(self, region, room_id, type, shopkeeper_config, custom, locked: bool): self.region = region self.room_id = room_id self.type = type - self.inventory = [None, None, None] + self.inventory: List[Union[None, dict]] = [None, None, None] self.shopkeeper_config = shopkeeper_config self.custom = custom self.locked = locked @@ -1085,20 +1090,31 @@ class Shop(object): config |= 0x10 # Alt. VRAM return [0x00]+int16_as_bytes(self.room_id)+[door_id, 0x00, config, self.shopkeeper_config, 0x00] - def has_unlimited(self, item): + def has_unlimited(self, item: str) -> bool: for inv in self.inventory: if inv is None: continue + if inv['item'] == item: + return True if inv['max'] != 0 and inv['replacement'] is not None and inv['replacement'] == item: return True - elif inv['item'] is not None and inv['item'] == item: + return False + + def has(self, item: str) -> bool: + for inv in self.inventory: + if inv is None: + continue + if inv['item'] == item: + return True + if inv['max'] != 0 and inv['replacement'] == item: return True return False def clear_inventory(self): self.inventory = [None, None, None] - def add_inventory(self, slot, item, price, max=0, replacement=None, replacement_price=0, create_location=False): + def add_inventory(self, slot: int, item: str, price: int, max: int = 0, + replacement: Optional[str] = None, replacement_price: int = 0, create_location: bool = False): self.inventory[slot] = { 'item': item, 'price': price, @@ -1108,6 +1124,18 @@ class Shop(object): 'create_location': create_location } + def push_inventory(self, slot: int, item: str, price: int, max: int = 1): + if not self.inventory[slot]: + raise ValueError("Inventory can't be pushed back if it doesn't exist") + + self.inventory[slot] = { + 'item': item, + 'price': price, + 'max': max, + 'replacement': self.inventory[slot]["item"], + 'replacement_price': self.inventory[slot]["price"], + 'create_location': self.inventory[slot]["create_location"] + } class Spoiler(object): world: World @@ -1243,6 +1271,7 @@ class Spoiler(object): 'progression_balancing': self.world.progression_balancing, 'triforce_pieces_available': self.world.triforce_pieces_available, 'triforce_pieces_required': self.world.triforce_pieces_required, + 'shop_shuffle': self.world.shop_shuffle } def to_json(self): @@ -1290,15 +1319,15 @@ class Spoiler(object): outfile.write('Progression Balanced: %s\n' % ( 'Yes' if self.metadata['progression_balancing'][player] else 'No')) outfile.write('Mode: %s\n' % self.metadata['mode'][player]) - outfile.write( - 'Retro: %s\n' % ('Yes' if self.metadata['retro'][player] else 'No')) + outfile.write('Retro: %s\n' % + ('Yes' if self.metadata['retro'][player] else 'No')) outfile.write('Swords: %s\n' % self.metadata['weapons'][player]) outfile.write('Goal: %s\n' % self.metadata['goal'][player]) if "triforce" in self.metadata["goal"][player]: # triforce hunt - outfile.write( - "Pieces available for Triforce: %s\n" % self.metadata['triforce_pieces_available'][player]) - outfile.write( - "Pieces required for Triforce: %s\n" % self.metadata["triforce_pieces_required"][player]) + outfile.write("Pieces available for Triforce: %s\n" % + self.metadata['triforce_pieces_available'][player]) + outfile.write("Pieces required for Triforce: %s\n" % + self.metadata["triforce_pieces_required"][player]) outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) outfile.write('Item Progression: %s\n' % self.metadata['progressive'][player]) @@ -1308,14 +1337,20 @@ class Spoiler(object): outfile.write('Pyramid hole pre-opened: %s\n' % ( 'Yes' if self.metadata['open_pyramid'][player] else 'No')) outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) - outfile.write( - 'Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) - outfile.write('Compass shuffle: %s\n' % ( - 'Yes' if self.metadata['compassshuffle'][player] else 'No')) + outfile.write('Map shuffle: %s\n' % + ('Yes' if self.metadata['mapshuffle'][player] else 'No')) + outfile.write('Compass shuffle: %s\n' % + ('Yes' if self.metadata['compassshuffle'][player] else 'No')) outfile.write( 'Small Key shuffle: %s\n' % (bool_to_text(self.metadata['keyshuffle'][player]))) outfile.write('Big Key shuffle: %s\n' % ( 'Yes' if self.metadata['bigkeyshuffle'][player] else 'No')) + outfile.write('Shop inventory shuffle: %s\n' % + bool_to_text("i" in self.metadata["shop_shuffle"][player])) + outfile.write('Shop price shuffle: %s\n' % + bool_to_text("p" in self.metadata["shop_shuffle"][player])) + outfile.write('Shop upgrade shuffle: %s\n' % + bool_to_text("u" in self.metadata["shop_shuffle"][player])) outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player]) outfile.write( 'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player])) @@ -1331,7 +1366,11 @@ class Spoiler(object): 'Pot shuffle %s\n' % ('Yes' if self.metadata['shufflepots'][player] else 'No')) if self.entrances: outfile.write('\n\nEntrances:\n\n') - outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.entrances.values()])) + outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' + if self.world.players > 1 else '', entry['entrance'], + '<=>' if entry['direction'] == 'both' else + '<=' if entry['direction'] == 'exit' else '=>', + entry['exit']) for entry in self.entrances.values()])) outfile.write('\n\nMedallions:\n') for dungeon, medallion in self.medallions.items(): outfile.write(f'\n{dungeon}: {medallion}') diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index 77741199..97cd0946 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -312,7 +312,12 @@ def parse_arguments(argv, no_defaults=False): parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos']) parser.add_argument('--shufflepots', default=defval(False), action='store_true') parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4)) - parser.add_argument('--shop_shuffle', default='off', choices=['off', 'inventory', 'price', 'inventory_price']) + parser.add_argument('--shop_shuffle', default='', help='''\ + combine letters for options: + i: shuffle the 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('--remote_items', default=defval(False), action='store_true') parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--names', default=defval('')) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 516a1f3c..c2e21968 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1061,7 +1061,8 @@ def link_entrances(world, player): # place remaining doors connect_doors(world, single_doors, door_targets, player) else: - raise NotImplementedError(f'{world.shuffle[player]} Shuffling not supported yet. Player {player}') + raise NotImplementedError( + f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_names(player)}') # check for swamp palace fix if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': diff --git a/Gui.py b/Gui.py index bcb0317c..a7755f80 100755 --- a/Gui.py +++ b/Gui.py @@ -75,14 +75,14 @@ def guiMain(args=None): compassshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Compasses", variable=compassshuffleVar) bigkeyshuffleVar = IntVar() - bigkeyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="BigKeys", variable=bigkeyshuffleVar) + bigkeyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Big Keys", variable=bigkeyshuffleVar) keyshuffleFrame = Frame(checkBoxFrame) keyshuffleVar = StringVar() keyshuffleVar.set('off') modeOptionMenu = OptionMenu(keyshuffleFrame, keyshuffleVar, 'off', 'universal', 'on') modeOptionMenu.pack(side=LEFT) - modeLabel = Label(keyshuffleFrame, text='Key Shuffle') + modeLabel = Label(keyshuffleFrame, text='Small Key Shuffle') modeLabel.pack(side=LEFT) retroVar = IntVar() @@ -447,6 +447,20 @@ def guiMain(args=None): killable_thievesShuffleButton = Checkbutton(enemizerFrame, text="Killable Thieves", variable=killableThievesVar) killable_thievesShuffleButton.grid(row=2, column=3, sticky=W) + shopframe = LabelFrame(randomizerWindow, text="Shops", padx=5, pady=2) + + shopPriceShuffleVar = IntVar() + shopPriceShuffleButton = Checkbutton(shopframe, text="Random Prices", variable=shopPriceShuffleVar) + shopPriceShuffleButton.grid(row=0, column=0, sticky=W) + + shopShuffleVar = IntVar() + shopShuffleButton = Checkbutton(shopframe, text="Shuffle Inventories", variable=shopShuffleVar) + shopShuffleButton.grid(row=0, column=1, sticky=W) + + shopUpgradeShuffleVar = IntVar() + shopUpgradeShuffleButton = Checkbutton(shopframe, text="Lootable Upgrades", variable=shopUpgradeShuffleVar) + shopUpgradeShuffleButton.grid(row=0, column=2, sticky=W) + multiworldframe = LabelFrame(randomizerWindow, text="Multiworld", padx=5, pady=2) worldLabel = Label(multiworldframe, text='Worlds') @@ -521,6 +535,13 @@ def guiMain(args=None): guiargs.custom = bool(customVar.get()) guiargs.triforce_pieces_required = min(90, int(triforcecountVar.get())) guiargs.triforce_pieces_available = min(90, int(triforcepieceVar.get())) + guiargs.shop_shuffle = "" + if shopShuffleVar.get(): + guiargs.shop_shuffle += "i" + if shopPriceShuffleVar.get(): + guiargs.shop_shuffle += "p" + if shopUpgradeShuffleVar.get(): + guiargs.shop_shuffle += "u" guiargs.customitemarray = [int(bowVar.get()), int(silverarrowVar.get()), int(boomerangVar.get()), int(magicboomerangVar.get()), int(hookshotVar.get()), int(mushroomVar.get()), int(magicpowderVar.get()), int(firerodVar.get()), @@ -567,9 +588,9 @@ def guiMain(args=None): main(seed=guiargs.seed, args=guiargs) except Exception as e: logging.exception(e) - messagebox.showerror(title="Error while creating seed", message=str(e)) + messagebox.showerror(title="Error while creating multiworld", message=str(e)) else: - messagebox.showinfo(title="Success", message="Rom patched successfully") + messagebox.showinfo(title="Success", message="Multiworld created successfully") generateButton = Button(farBottomFrame, text='Generate Patched Rom', command=generateRom) @@ -590,6 +611,7 @@ def guiMain(args=None): topFrame.pack(side=TOP) multiworldframe.pack(side=BOTTOM, expand=True, fill=X) enemizerFrame.pack(side=BOTTOM, fill=BOTH) + shopframe.pack(side=BOTTOM, expand=True, fill=X) # Adjuster Controls diff --git a/ItemPool.py b/ItemPool.py index 89a54cf1..8db1c53c 100644 --- a/ItemPool.py +++ b/ItemPool.py @@ -311,8 +311,6 @@ def generate_itempool(world, player: int): progressionitems += [ItemFactory("Triforce Piece", player)] * (triforce_pieces - 30) nonprogressionitems = nonprogressionitems[(triforce_pieces - 30):] - world.itempool += progressionitems + nonprogressionitems - # shuffle medallions mm_medallion = ['Ether', 'Quake', 'Bombos'][world.random.randint(0, 2)] tr_medallion = ['Ether', 'Quake', 'Bombos'][world.random.randint(0, 2)] @@ -323,43 +321,64 @@ def generate_itempool(world, player: int): if world.retro[player]: set_up_take_anys(world, player) - if world.shop_shuffle[player] != 'off': - shuffle_shops(world, player) + if world.shop_shuffle[player]: + shuffle_shops(world, nonprogressionitems, player) create_dynamic_shop_locations(world, player) + world.itempool += progressionitems + nonprogressionitems -def shuffle_shops(world, player: int): + +def shuffle_shops(world, items, player: int): option = world.shop_shuffle[player] - shops = [] - total_inventory = [] - for shop in world.shops: - if shop.type == ShopType.Shop and shop.region.player == player: - shops.append(shop) - total_inventory.extend(shop.inventory) - - if 'price' 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() * 2)) - - for item in total_inventory: - if item: - item["price"] = price_adjust(item["price"]) - item['replacement_price'] = price_adjust(item["price"]) + if 'u' in option: + if world.retro[player]: + new_items = ["Bomb Upgrade (+5)"] * 7 + else: + new_items = ["Bomb Upgrade (+5)", "Arrow Upgrade (+5)"] * 7 for shop in world.shops: - if shop.type == ShopType.UpgradeShop and shop.region.player == player: - for item in shop.inventory: - if item: - item['price'] = price_adjust(item["price"]) - item['replacement_price'] = price_adjust(item["price"]) + if shop.type == ShopType.UpgradeShop and shop.region.player == player and \ + shop.region.name == "Capacity Upgrade": + shop.clear_inventory() - if 'inventory' in option: - world.random.shuffle(total_inventory) - i = 0 - for shop in shops: - slots = shop.slots - shop.inventory = total_inventory[i:i + slots] - i += slots + for i, item in enumerate(items): + if item.name.startswith(("Bombs", "Arrows", "Rupees")): + items[i] = ItemFactory(new_items.pop(), player) + if not new_items: + break + else: + logging.warning(f"Not all upgrades put into Player{player}' item pool. Still missing: {new_items}") + + if 'p' in option or 'i' in option: + shops = [] + total_inventory = [] + for shop in world.shops: + if shop.type == ShopType.Shop and shop.region.player == player: + 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() * 2)) + + for item in total_inventory: + if item: + item["price"] = price_adjust(item["price"]) + item['replacement_price'] = price_adjust(item["price"]) + for shop in world.shops: + if shop.type == ShopType.UpgradeShop and shop.region.player == player: + for item in shop.inventory: + if item: + item['price'] = price_adjust(item["price"]) + item['replacement_price'] = price_adjust(item["price"]) + + if 'i' in option: + world.random.shuffle(total_inventory) + i = 0 + for shop in shops: + slots = shop.slots + shop.inventory = total_inventory[i:i + slots] + i += slots take_any_locations = [ @@ -466,21 +485,21 @@ def set_up_shops(world, player: int): if world.retro[player]: rss = world.get_region('Red Shield Shop', player).shop - if not rss.locked: - rss.add_inventory(2, 'Single Arrow', 80) + rss.add_inventory(2, 'Single Arrow', 80) rss.locked = True - if world.keyshuffle[player] == "universal": + if world.keyshuffle[player] == "universal" or world.retro[player]: for shop in world.random.sample([s for s in world.shops if s.custom and not s.locked and s.type == ShopType.Shop and s.region.player == player], 5): shop.locked = True + slots = [0, 1, 2] + world.random.shuffle(slots) + slots = iter(slots) if world.retro[player]: - shop.add_inventory(0, 'Single Arrow', 80) - else: - shop.add_inventory(0, "Red Potion", 150) - shop.add_inventory(1, 'Small Key (Universal)', 100) - shop.add_inventory(2, 'Bombs (10)', 50) + shop.push_inventory(next(slots), 'Single Arrow', 80) + if world.keyshuffle[player] == "universal": + shop.add_inventory(next(slots), 'Small Key (Universal)', 100) def get_pool_core(world, player: int): diff --git a/Mystery.py b/Mystery.py index 652c5070..4158ad98 100644 --- a/Mystery.py +++ b/Mystery.py @@ -312,7 +312,7 @@ def roll_settings(weights): ret.shop_shuffle = get_choice('shop_shuffle', weights, False) if not ret.shop_shuffle: - ret.shop_shuffle = 'off' + ret.shop_shuffle = '' ret.mode = get_choice('world_state', weights, None) # legacy support if ret.mode == 'retro': diff --git a/Rom.py b/Rom.py index fa16109d..5be942dd 100644 --- a/Rom.py +++ b/Rom.py @@ -1157,20 +1157,27 @@ def patch_rom(world, rom, player, team, enemized): assert equip[:0x340] == [0] * 0x340 rom.write_bytes(0x183000, equip[0x340:]) - rom.write_bytes(0x271A6, equip[0x340:0x340+60]) + rom.write_bytes(0x271A6, equip[0x340:0x340 + 60]) rom.write_byte(0x18004A, 0x00 if world.mode[player] != 'inverted' else 0x01) # Inverted mode - rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier - rom.write_byte(0x2AF79, 0xD0 if world.mode[player] != 'inverted' else 0xF0) # vortexes: Normal (D0=light to dark, F0=dark to light, 42 = both) - rom.write_byte(0x3A943, 0xD0 if world.mode[player] != 'inverted' else 0xF0) # Mirror: Normal (D0=Dark to Light, F0=light to dark, 42 = both) - rom.write_byte(0x3A96D, 0xF0 if world.mode[player] != 'inverted' else 0xD0) # Residual Portal: Normal (F0= Light Side, D0=Dark Side, 42 = both (Darth Vader)) - rom.write_byte(0x3A9A7, 0xD0) # Residual Portal: Normal (D0= Light Side, F0=Dark Side, 42 = both (Darth Vader)) - - rom.write_bytes(0x180080, [50, 50, 70, 70]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10) + rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier + rom.write_byte(0x2AF79, 0xD0 if world.mode[ + player] != 'inverted' else 0xF0) # vortexes: Normal (D0=light to dark, F0=dark to light, 42 = both) + rom.write_byte(0x3A943, 0xD0 if world.mode[ + player] != 'inverted' else 0xF0) # Mirror: Normal (D0=Dark to Light, F0=light to dark, 42 = both) + rom.write_byte(0x3A96D, 0xF0 if world.mode[ + player] != 'inverted' else 0xD0) # Residual Portal: Normal (F0= Light Side, D0=Dark Side, 42 = both (Darth Vader)) + rom.write_byte(0x3A9A7, 0xD0) # Residual Portal: Normal (D0= Light Side, F0=Dark Side, 42 = both (Darth Vader)) + if 'u' in world.shop_shuffle[player]: + rom.write_bytes(0x180080, + [5, 10, 5, 10]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10) + else: + rom.write_bytes(0x180080, + [50, 50, 70, 70]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10) rom.write_byte(0x18004D, ((0x01 if 'arrows' in world.escape_assist[player] else 0x00) | (0x02 if 'bombs' in world.escape_assist[player] else 0x00) | - (0x04 if 'magic' in world.escape_assist[player] else 0x00))) # Escape assist + (0x04 if 'magic' in world.escape_assist[player] else 0x00))) # Escape assist if world.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt']: rom.write_byte(0x18003E, 0x01) # make ganon invincible diff --git a/playerSettings.yaml b/playerSettings.yaml index 58181f6d..f5d3e02b 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -190,10 +190,13 @@ beemizer: # Remove items from the global item pool and replace them with single 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 shop_shuffle: - off: 1 - inventory: 0 # shuffle the inventories of the shops around - price: 0 # randomize the prices of the items in shop inventories - inventory_price: 0 # shuffle inventories and randomize prices + none: 1 + i: 0 # shuffle the 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 + uip: 0 # shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool + # you can add more combos timer: none: 1 timed: 0 @@ -245,7 +248,6 @@ linked_options: full: 1 random: 1 singularity: 1 - duality: 1 enemy_damage: shuffled: 1 random: 1