diff --git a/BaseClasses.py b/BaseClasses.py index 5651be42..b08fdb23 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -981,7 +981,7 @@ class Dungeon(object): def __str__(self): return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})' -class Boss(object): +class Boss(): def __init__(self, name, enemizer_name, defeat_rule, player: int): self.name = name self.enemizer_name = enemizer_name @@ -991,7 +991,12 @@ class Boss(object): def can_defeat(self, state) -> bool: return self.defeat_rule(state, self.player) -class Location(object): + +class Location(): + shop_slot: bool = False + event: bool = False + locked: bool = False + def __init__(self, player: int, name: str = '', address=None, crystal: bool = False, hint_text: Optional[str] = None, parent=None, player_address=None): @@ -1004,8 +1009,6 @@ class Location(object): self.spot_type = 'Location' self.hint_text: str = hint_text if hint_text else name self.recursion_count = 0 - self.event = False - self.locked = False self.always_allow = lambda item, state: False self.access_rule = lambda state: True self.item_rule = lambda item: True @@ -1152,7 +1155,8 @@ class Shop(): self.inventory = [None] * self.slots 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): + replacement: Optional[str] = None, replacement_price: int = 0, create_location: bool = False, + player: int = 0): self.inventory[slot] = { 'item': item, 'price': price, @@ -1160,10 +1164,10 @@ class Shop(): 'replacement': replacement, 'replacement_price': replacement_price, 'create_location': create_location, - 'player': 0 + 'player': player } - def push_inventory(self, slot: int, item: str, price: int, max: int = 1): + def push_inventory(self, slot: int, item: str, price: int, max: int = 1, player: int = 0): if not self.inventory[slot]: raise ValueError("Inventory can't be pushed back if it doesn't exist") @@ -1174,9 +1178,11 @@ class Shop(): 'replacement': self.inventory[slot]["item"], 'replacement_price': self.inventory[slot]["price"], 'create_location': self.inventory[slot]["create_location"], - 'player': self.inventory[slot]["player"] + 'player': player } + def can_push_inventory(self, slot: int): + return self.inventory[slot] and not self.inventory[slot]["replacement"] class TakeAny(Shop): type = ShopType.TakeAny diff --git a/ItemPool.py b/ItemPool.py index 2a92239a..582e7794 100644 --- a/ItemPool.py +++ b/ItemPool.py @@ -589,7 +589,8 @@ def create_dynamic_shop_locations(world, player): world.clear_location_cache() - world.push_item(loc, ItemFactory(item['item'], player), False) + world.push_item(loc, ItemFactory(item['item'], player), False) + loc.shop_slot = True loc.event = True loc.locked = True diff --git a/Main.py b/Main.py index 1364340d..4b5a3757 100644 --- a/Main.py +++ b/Main.py @@ -9,7 +9,7 @@ import time import zlib import concurrent.futures -from BaseClasses import World, CollectionState, Item, Region, Location, PlandoItem +from BaseClasses import World, CollectionState, Item, Region, Location, Shop from Items import ItemFactory, item_table, item_name_groups from Regions import create_regions, create_shops, mark_light_world_regions, lookup_vanilla_location_to_entrance, SHOP_ID_START from InvertedRegions import create_inverted_regions, mark_dark_world_regions @@ -215,69 +215,47 @@ def main(args, seed=None): blacklist_word in item_name for blacklist_word in blacklist_words)} blacklist_words.add("Bee") candidates = [location for location in world.get_locations() if - not location.locked and not location.item.name in blacklist_words] + not location.locked and + not location.shop_slot and + not location.item.name in blacklist_words] + 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'] # TODO: "w" in shop_shuffle options? - shop_slots_adjusted = [] - shop_items = [] - + shop_slots = [item for sublist in (shop.region.locations for shop in world.shops) for item in sublist if + item.shop_slot] + 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']): + shop: Shop = location.parent_region.shop + if shop.can_push_inventory(slot_num): for c in candidates: # chosen item locations - if c.parent_region.shop or c is location: 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(): + if not world.can_beat_game(): c.item, location.item = location.item, c.item - else: - shop_slots_adjusted.append(location) + else: + # we use this candidate + candidates.remove(c) break - # update table to location data - item = location.item - if (shop_item and shop_item['item'] == item.name) or 'Rupee' in item.name or item.name == '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 ['Compass', 'Map', 'Small Key', 'Piece of Heart']): - shop_item['price'] = world.random.randrange(50, 150) + logging.warning("Ran out of ShopShuffle Item candidate locations.") + break # we ran out of candidates + + # update table to location data + item_name = location.item.name + if any(x in item_name for x in ['Single Bomb', 'Single Arrow']): + price = world.random.randrange(1, 7) + elif any(x in item_name for x in ['Arrows', 'Bombs', 'Clock']): + price = world.random.randrange(4, 24) + elif any(x in item_name for x in ['Compass', 'Map', 'Small Key', 'Piece of Heart']): + price = world.random.randrange(10, 30) else: - shop_item['price'] = world.random.randrange(50,300) + price = world.random.randrange(10, 60) - shop_item['max'] = 1 - shop_item['player'] = item.player if item.player != location.player else 0 - shop_items.append(shop_item) + price *= 5 - 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])) + shop.push_inventory(slot_num, item_name, price, 1, + location.item.player if location.item.player != location.player else 0) logger.info('Patching ROM.') diff --git a/Regions.py b/Regions.py index 90111abb..6f239244 100644 --- a/Regions.py +++ b/Regions.py @@ -415,9 +415,11 @@ def create_shops(world, player: int): 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 = "{} Slot {}".format(shop.region.name, index + 1) loc = Location(player, slot_name, address=shop_table_by_location[slot_name], parent=shop.region) + loc.shop_slot = True + loc.locked = True + loc.item = ItemFactory(additional_item, player) shop.region.locations.append(loc) world.dynamic_locations.append(loc) diff --git a/Rom.py b/Rom.py index e435f6b7..ae42dbd1 100644 --- a/Rom.py +++ b/Rom.py @@ -683,17 +683,11 @@ def patch_rom(world, rom, player, team, enemized): # patch items for location in world.get_locations(): - if location.player != player: + if location.player != player or location.address is None or location.shop_slot: continue itemid = location.item.code if location.item is not None else 0x5A - if location.address is None: - continue - - if location.parent_region.shop and 'Slot' in location.name: - continue - if not location.crystal: if location.item is not None: # Keys in their native dungeon should use the orignal item code for keys