diff --git a/BaseClasses.py b/BaseClasses.py index f84abd59..e43c9ea9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -5,12 +5,11 @@ from enum import Enum, unique import logging import json from collections import OrderedDict, Counter, deque -from typing import Union, Optional, List, Set, Dict, NamedTuple, Iterable +from typing import Union, Optional, List, Dict, NamedTuple, Iterable import secrets import random -from EntranceShuffle import door_addresses, indirect_connections -from Utils import int16_as_bytes +from EntranceShuffle import indirect_connections from Items import item_name_groups @@ -1148,110 +1147,6 @@ class Item(object): class Crystal(Item): pass -@unique -class ShopType(Enum): - Shop = 0 - TakeAny = 1 - UpgradeShop = 2 - -class Shop(): - slots = 3 # slot count is not dynamic in asm, however inventory can have None as empty slots - blacklist = set() # items that don't work, todo: actually check against this - type = ShopType.Shop - - def __init__(self, region: Region, room_id: int, shopkeeper_config: int, custom: bool, locked: bool): - self.region = region - self.room_id = room_id - self.inventory: List[Union[None, dict]] = [None] * self.slots - self.shopkeeper_config = shopkeeper_config - self.custom = custom - self.locked = locked - - @property - def item_count(self) -> int: - for x in range(self.slots - 1, -1, -1): # last x is 0 - if self.inventory[x]: - return x + 1 - return 0 - - def get_bytes(self) -> List[int]: - # [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index] - entrances = self.region.entrances - config = self.item_count - if len(entrances) == 1 and entrances[0].name in door_addresses: - door_id = door_addresses[entrances[0].name][0] + 1 - else: - door_id = 0 - config |= 0x40 # ignore door id - if self.type == ShopType.TakeAny: - config |= 0x80 - elif self.type == ShopType.UpgradeShop: - 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: 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 - 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] * 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, - player: int = 0): - self.inventory[slot] = { - 'item': item, - 'price': price, - 'max': max, - 'replacement': replacement, - 'replacement_price': replacement_price, - 'create_location': create_location, - 'player': player - } - - 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") - - 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"], - '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 - - -class UpgradeShop(Shop): - type = ShopType.UpgradeShop - # Potions break due to VRAM flags set in UpgradeShop. - # Didn't check for more things breaking as not much else can be shuffled here currently - blacklist = item_name_groups["Potions"] - class Spoiler(object): world: World @@ -1314,6 +1209,7 @@ class Spoiler(object): listed_locations.update(other_locations) self.shops = [] + from Shops import ShopType for shop in self.world.shops: if not shop.custom: continue diff --git a/ItemPool.py b/ItemPool.py index 2f65d085..cc002ac1 100644 --- a/ItemPool.py +++ b/ItemPool.py @@ -1,7 +1,8 @@ from collections import namedtuple import logging -from BaseClasses import Region, RegionType, ShopType, Shop, Location, TakeAny +from BaseClasses import Region, RegionType, Location +from Shops import ShopType, Shop, TakeAny from Bosses import place_bosses from Dungeons import get_dungeon_item_pool from EntranceShuffle import connect_entrance diff --git a/Main.py b/Main.py index a79f46fe..0e03ff96 100644 --- a/Main.py +++ b/Main.py @@ -8,19 +8,17 @@ import random import time import zlib import concurrent.futures -import typing -from BaseClasses import World, CollectionState, Item, Region, Location, Shop +from BaseClasses import World, CollectionState, Item, Region, Location +from Shops import ShopSlotFill, create_shops, SHOP_ID_START 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 Regions import create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance from InvertedRegions import create_inverted_regions, mark_dark_world_regions from EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string from Rules import set_rules from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive -from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned, \ - swap_location_item +from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned from ItemPool import generate_itempool, difficulties, fill_prizes from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple import Patch @@ -213,77 +211,13 @@ def main(args, seed=None): if world.players > 1: balance_multiworld_progression(world) - shop_slots: typing.List[Location] = [location for shop_locations in (shop.region.locations for shop in world.shops) - for location in shop_locations if location.shop_slot] + logger.info("Filling Shop Slots") - if shop_slots: - # TODO: allow each game to register a blacklist to be used here? - blacklist_words = {"Rupee"} - blacklist_words = {item_name for item_name in item_table if any( - blacklist_word in item_name for blacklist_word in blacklist_words)} - blacklist_words.add("Bee") - candidates: typing.List[Location] = [location for location in world.get_locations() if - not location.locked and - not location.shop_slot and - not location.item.name in blacklist_words] - - world.random.shuffle(candidates) - - if not world.fulfills_accessibility(): - logger.warning("World does not fulfill accessibility rules as is, " - "only using \"beatable only\" for shop logic.") - shuffle_condition = world.can_beat_game - else: - shuffle_condition = world.fulfills_accessibility - - # currently special care needs to be taken so that Shop.region.locations.item is identical to Shop.inventory - # Potentially create Locations as needed and make inventory the only source, to prevent divergence - - for location in shop_slots: - slot_num = int(location.name[-1]) - 1 - shop: Shop = location.parent_region.shop - if shop.can_push_inventory(slot_num): - for c in candidates: # chosen item locations - if c.item_rule(location.item) and location.item_rule(c.item): # if rule is good... - - swap_location_item(c, location, check_locked=False) - candidates.remove(c) - if not shuffle_condition(): - swap_location_item(c, location, check_locked=False) - continue - - logger.debug(f'Swapping {c} into {location}:: {location.item}') - break - - else: - # This *should* never happen. But let's fail safely just in case. - logger.warning("Ran out of ShopShuffle Item candidate locations.") - shop.region.locations.remove(location) - continue - - 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: - price = world.random.randrange(10, 60) - - price *= 5 - - shop.push_inventory(slot_num, item_name, price, 1, - location.item.player if location.item.player != location.player else 0) - else: - shop.region.locations.remove(location) + ShopSlotFill(world) logger.info('Patching ROM.') - # remove locations that may no longer exist from caches, by flushing them entirely - if shop_slots: - world.clear_location_cache() - world._location_cache = {} + outfilebase = 'BM_%s' % (args.outputname if args.outputname else world.seed) diff --git a/MultiClient.py b/MultiClient.py index 988b5fe0..09d7b96a 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -16,6 +16,7 @@ import subprocess from random import randrange +import Shops from Utils import get_item_name_from_id, get_location_name_from_address, ReceivedItem exit_func = atexit.register(input, "Press enter to close.") @@ -159,8 +160,8 @@ 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_shop_order = [name for name, info in Shops.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 Shops.shop_table.items()]) location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10), "Blind's Hideout - Left": (0x11d, 0x20), @@ -1154,7 +1155,7 @@ async def track_locations(ctx : Context, roomid, roomdata): if roomid in location_shop_ids: misc_data = await snes_read(ctx, SHOP_ADDR, (len(location_shop_order)*3)+5) for cnt, b in enumerate(misc_data): - my_check = Regions.shop_table_by_location_id[Regions.SHOP_ID_START + cnt] + my_check = Shops.shop_table_by_location_id[Shops.SHOP_ID_START + cnt] if int(b) > 0 and my_check not in ctx.locations_checked: new_check(my_check) except Exception as e: @@ -1219,7 +1220,7 @@ async def track_locations(ctx : Context, roomid, roomdata): for location in ctx.locations_checked: try: - my_id = Regions.lookup_name_to_id.get(location, Regions.shop_table_by_location.get(location, -1)) + my_id = Regions.lookup_name_to_id.get(location, Shops.shop_table_by_location.get(location, -1)) new_locations.append(my_id) except Exception as e: print(e) diff --git a/Regions.py b/Regions.py index 94eac4b3..da04b359 100644 --- a/Regions.py +++ b/Regions.py @@ -1,7 +1,8 @@ import collections import typing -from BaseClasses import Region, Location, Entrance, RegionType, Shop, TakeAny, UpgradeShop, ShopType +from BaseClasses import Region, Location, Entrance, RegionType + def create_regions(world, player): @@ -365,104 +366,6 @@ def mark_light_world_regions(world, player: int): queue.append(exit.connected_region) -def create_shops(world, player: int): - cls_mapping = {ShopType.UpgradeShop: UpgradeShop, - ShopType.Shop: Shop, - ShopType.TakeAny: TakeAny} - option = world.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 - if name == 'Capacity Upgrade': - pass - elif name == 'Potion Shop' and not "w" in option: - pass - else: - 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 - 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)] - region = world.get_region(region_name, player) - shop = cls_mapping[type](region, room_id, shopkeeper, custom, locked) - region.shop = shop - world.shops.append(shop) - for index, item in enumerate(inventory): - shop.add_inventory(index, *item) - if region_name == 'Potion Shop' and 'w' not in option: - pass - elif region_name == 'Capacity Upgrade': - pass - else: - if my_shop_slots.pop(): - additional_item = 'Rupees (50)' # world.random.choice(['Rupees (50)', 'Rupees (100)', 'Rupees (300)']) - slot_name = "{} Slot {}".format(shop.region.name, index + 1) - loc = Location(player, slot_name, address=shop_table_by_location[slot_name], - parent=shop.region, hint_text="for sale") - loc.shop_slot = True - loc.locked = True - loc.item = ItemFactory(additional_item, player) - 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) -_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)] -_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)] -shop_table = { - 'Cave Shop (Dark Death Mountain)': (0x0112, ShopType.Shop, 0xC1, True, False, _basic_shop_defaults), - 'Red Shield Shop': (0x0110, ShopType.Shop, 0xC1, True, False, [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)]), - 'Dark Lake Hylia Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults), - 'Dark World Lumberjack Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults), - 'Village of Outcasts Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults), - 'Dark World Potion Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults), - '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, 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_ID_START = 0x400000 -shop_table_by_location_id = {SHOP_ID_START + cnt: s for cnt, s in enumerate( - [item for sublist in [["{} Slot {}".format(name, num + 1) for num in range(3)] for name in shop_table] for item in - sublist])} -shop_table_by_location_id[(SHOP_ID_START + len(shop_table)*3)] = "Old Man Sword Cave" -shop_table_by_location_id[(SHOP_ID_START + len(shop_table)*3 + 1)] = "Take-Any #1" -shop_table_by_location_id[(SHOP_ID_START + len(shop_table)*3 + 2)] = "Take-Any #2" -shop_table_by_location_id[(SHOP_ID_START + len(shop_table)*3 + 3)] = "Take-Any #3" -shop_table_by_location_id[(SHOP_ID_START + len(shop_table)*3 + 4)] = "Take-Any #4" -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 @@ -769,6 +672,7 @@ location_table: typing.Dict[str, 'Turtle Rock - Prize': ( [0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')} +from Shops import shop_table_by_location_id, shop_table_by_location 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) diff --git a/Rom.py b/Rom.py index d7f063dd..9cae3e5b 100644 --- a/Rom.py +++ b/Rom.py @@ -17,7 +17,8 @@ import xxtea import concurrent.futures from typing import Optional -from BaseClasses import CollectionState, ShopType, Region, Location +from BaseClasses import CollectionState, Region, Location +from Shops import ShopType from Dungeons import dungeon_music_addresses from Regions import location_table, old_location_address_to_new_location_address from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable diff --git a/Shops.py b/Shops.py new file mode 100644 index 00000000..86f6b49d --- /dev/null +++ b/Shops.py @@ -0,0 +1,289 @@ +from __future__ import annotations +from enum import unique, Enum +from typing import List, Union, Optional +import logging + +from BaseClasses import Location +from EntranceShuffle import door_addresses +from Items import item_name_groups, item_table +from Utils import int16_as_bytes + +logger = logging.getLogger("Shops") + +@unique +class ShopType(Enum): + Shop = 0 + TakeAny = 1 + UpgradeShop = 2 + + +class Shop(): + slots = 3 # slot count is not dynamic in asm, however inventory can have None as empty slots + blacklist = set() # items that don't work, todo: actually check against this + type = ShopType.Shop + + def __init__(self, region, room_id: int, shopkeeper_config: int, custom: bool, locked: bool): + self.region = region + self.room_id = room_id + self.inventory: List[Union[None, dict]] = [None] * self.slots + self.shopkeeper_config = shopkeeper_config + self.custom = custom + self.locked = locked + + @property + def item_count(self) -> int: + for x in range(self.slots - 1, -1, -1): # last x is 0 + if self.inventory[x]: + return x + 1 + return 0 + + def get_bytes(self) -> List[int]: + # [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index] + entrances = self.region.entrances + config = self.item_count + if len(entrances) == 1 and entrances[0].name in door_addresses: + door_id = door_addresses[entrances[0].name][0] + 1 + else: + door_id = 0 + config |= 0x40 # ignore door id + if self.type == ShopType.TakeAny: + config |= 0x80 + elif self.type == ShopType.UpgradeShop: + 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: 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 + 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] * 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, + player: int = 0): + self.inventory[slot] = { + 'item': item, + 'price': price, + 'max': max, + 'replacement': replacement, + 'replacement_price': replacement_price, + 'create_location': create_location, + 'player': player + } + + 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") + + 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"], + '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 + + +class UpgradeShop(Shop): + type = ShopType.UpgradeShop + # Potions break due to VRAM flags set in UpgradeShop. + # Didn't check for more things breaking as not much else can be shuffled here currently + blacklist = item_name_groups["Potions"] + +def ShopSlotFill(world): + shop_slots: List[Location] = [location for shop_locations in (shop.region.locations for shop in world.shops) + for location in shop_locations if location.shop_slot] + + if shop_slots: + from Fill import swap_location_item + # TODO: allow each game to register a blacklist to be used here? + blacklist_words = {"Rupee"} + blacklist_words = {item_name for item_name in item_table if any( + blacklist_word in item_name for blacklist_word in blacklist_words)} + blacklist_words.add("Bee") + candidates: List[Location] = [location for location in world.get_locations() if + not location.locked and + not location.shop_slot and + not location.item.name in blacklist_words] + + world.random.shuffle(candidates) + + if not world.fulfills_accessibility(): + logger.warning("World does not fulfill accessibility rules as is, " + "only using \"beatable only\" for shop logic.") + shuffle_condition = world.can_beat_game + else: + shuffle_condition = world.fulfills_accessibility + + # currently special care needs to be taken so that Shop.region.locations.item is identical to Shop.inventory + # Potentially create Locations as needed and make inventory the only source, to prevent divergence + + for location in shop_slots: + slot_num = int(location.name[-1]) - 1 + shop: Shop = location.parent_region.shop + if shop.can_push_inventory(slot_num): + for c in candidates: # chosen item locations + if c.item_rule(location.item) and location.item_rule(c.item): # if rule is good... + + swap_location_item(c, location, check_locked=False) + candidates.remove(c) + if not shuffle_condition(): + swap_location_item(c, location, check_locked=False) + continue + + logger.debug(f'Swapping {c} into {location}:: {location.item}') + break + + else: + # This *should* never happen. But let's fail safely just in case. + logger.warning("Ran out of ShopShuffle Item candidate locations.") + shop.region.locations.remove(location) + continue + + 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: + price = world.random.randrange(10, 60) + + price *= 5 + + shop.push_inventory(slot_num, item_name, price, 1, + location.item.player if location.item.player != location.player else 0) + else: + shop.region.locations.remove(location) + + # remove locations that may no longer exist from caches, by flushing them entirely + if shop_slots: + world.clear_location_cache() + world._location_cache = {} + + +def create_shops(world, player: int): + cls_mapping = {ShopType.UpgradeShop: UpgradeShop, + ShopType.Shop: Shop, + ShopType.TakeAny: TakeAny} + option = world.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 + if name == 'Capacity Upgrade': + pass + elif name == 'Potion Shop' and not "w" in option: + pass + else: + 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 + 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)] + region = world.get_region(region_name, player) + shop = cls_mapping[type](region, room_id, shopkeeper, custom, locked) + region.shop = shop + world.shops.append(shop) + for index, item in enumerate(inventory): + shop.add_inventory(index, *item) + if region_name == 'Potion Shop' and 'w' not in option: + pass + elif region_name == 'Capacity Upgrade': + pass + else: + if my_shop_slots.pop(): + additional_item = 'Rupees (50)' # world.random.choice(['Rupees (50)', 'Rupees (100)', 'Rupees (300)']) + slot_name = "{} Slot {}".format(shop.region.name, index + 1) + loc = Location(player, slot_name, address=shop_table_by_location[slot_name], + parent=shop.region, hint_text="for sale") + loc.shop_slot = True + loc.locked = True + loc.item = ItemFactory(additional_item, player) + 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) +_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)] +_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)] +shop_table = { + 'Cave Shop (Dark Death Mountain)': (0x0112, ShopType.Shop, 0xC1, True, False, _basic_shop_defaults), + 'Red Shield Shop': (0x0110, ShopType.Shop, 0xC1, True, False, [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)]), + 'Dark Lake Hylia Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults), + 'Dark World Lumberjack Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults), + 'Village of Outcasts Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults), + 'Dark World Potion Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults), + '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, 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_ID_START = 0x400000 +shop_table_by_location_id = {SHOP_ID_START + cnt: s for cnt, s in enumerate( + [item for sublist in [["{} Slot {}".format(name, num + 1) for num in range(3)] for name in shop_table] for item in + sublist])} +shop_table_by_location_id[(SHOP_ID_START + len(shop_table)*3)] = "Old Man Sword Cave" +shop_table_by_location_id[(SHOP_ID_START + len(shop_table)*3 + 1)] = "Take-Any #1" +shop_table_by_location_id[(SHOP_ID_START + len(shop_table)*3 + 2)] = "Take-Any #2" +shop_table_by_location_id[(SHOP_ID_START + len(shop_table)*3 + 3)] = "Take-Any #3" +shop_table_by_location_id[(SHOP_ID_START + len(shop_table)*3 + 4)] = "Take-Any #4" +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)], +} + diff --git a/test/dungeons/TestDungeon.py b/test/dungeons/TestDungeon.py index b4a4bb06..6ec685e8 100644 --- a/test/dungeons/TestDungeon.py +++ b/test/dungeons/TestDungeon.py @@ -5,7 +5,8 @@ from Dungeons import create_dungeons, get_dungeon_item_pool from EntranceShuffle import mandatory_connections, connect_simple from ItemPool import difficulties, generate_itempool from Items import ItemFactory -from Regions import create_regions, create_shops +from Regions import create_regions +from Shops import create_shops from Rules import set_rules diff --git a/test/inverted/TestInverted.py b/test/inverted/TestInverted.py index 93a52c57..5ad9de09 100644 --- a/test/inverted/TestInverted.py +++ b/test/inverted/TestInverted.py @@ -4,7 +4,8 @@ from EntranceShuffle import link_inverted_entrances from InvertedRegions import create_inverted_regions from ItemPool import generate_itempool, difficulties from Items import ItemFactory -from Regions import mark_light_world_regions, create_shops +from Regions import mark_light_world_regions +from Shops import create_shops from Rules import set_rules from test.TestBase import TestBase diff --git a/test/inverted_minor_glitches/TestInvertedMinor.py b/test/inverted_minor_glitches/TestInvertedMinor.py index 30f27dce..bbd5ba44 100644 --- a/test/inverted_minor_glitches/TestInvertedMinor.py +++ b/test/inverted_minor_glitches/TestInvertedMinor.py @@ -4,7 +4,8 @@ from EntranceShuffle import link_inverted_entrances from InvertedRegions import create_inverted_regions from ItemPool import generate_itempool, difficulties from Items import ItemFactory -from Regions import mark_light_world_regions, create_shops +from Regions import mark_light_world_regions +from Shops import create_shops from Rules import set_rules from test.TestBase import TestBase diff --git a/test/inverted_owg/TestInvertedOWG.py b/test/inverted_owg/TestInvertedOWG.py index 7cf44c84..bca73b76 100644 --- a/test/inverted_owg/TestInvertedOWG.py +++ b/test/inverted_owg/TestInvertedOWG.py @@ -4,7 +4,8 @@ from EntranceShuffle import link_inverted_entrances from InvertedRegions import create_inverted_regions from ItemPool import generate_itempool, difficulties from Items import ItemFactory -from Regions import mark_light_world_regions, create_shops +from Regions import mark_light_world_regions +from Shops import create_shops from Rules import set_rules from test.TestBase import TestBase diff --git a/test/minor_glitches/TestMinor.py b/test/minor_glitches/TestMinor.py index 23dd387c..4779ca83 100644 --- a/test/minor_glitches/TestMinor.py +++ b/test/minor_glitches/TestMinor.py @@ -4,7 +4,8 @@ from EntranceShuffle import link_entrances from InvertedRegions import mark_dark_world_regions from ItemPool import difficulties, generate_itempool from Items import ItemFactory -from Regions import create_regions, create_shops +from Regions import create_regions +from Shops import create_shops from Rules import set_rules from test.TestBase import TestBase diff --git a/test/owg/TestVanillaOWG.py b/test/owg/TestVanillaOWG.py index bd22db13..4497eba6 100644 --- a/test/owg/TestVanillaOWG.py +++ b/test/owg/TestVanillaOWG.py @@ -4,7 +4,8 @@ from EntranceShuffle import link_entrances from InvertedRegions import mark_dark_world_regions from ItemPool import difficulties, generate_itempool from Items import ItemFactory -from Regions import create_regions, create_shops +from Regions import create_regions +from Shops import create_shops from Rules import set_rules from test.TestBase import TestBase diff --git a/test/vanilla/TestVanilla.py b/test/vanilla/TestVanilla.py index e21b8408..41a72f3d 100644 --- a/test/vanilla/TestVanilla.py +++ b/test/vanilla/TestVanilla.py @@ -4,7 +4,8 @@ from EntranceShuffle import link_entrances from InvertedRegions import mark_dark_world_regions from ItemPool import difficulties, generate_itempool from Items import ItemFactory -from Regions import create_regions, create_shops +from Regions import create_regions +from Shops import create_shops from Rules import set_rules from test.TestBase import TestBase