Merge remote-tracking branch 'pepper/multishop-all' into multishop
# Conflicts: # EntranceRandomizer.py # Rom.py # WebHostLib/static/static/playerSettings.json # data/basepatch.bmbp
This commit is contained in:
commit
e36c6e97c1
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
2
Fill.py
2
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:
|
||||
|
|
18
ItemPool.py
18
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
|
||||
|
||||
|
|
69
Main.py
69
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
|
||||
|
||||
|
|
|
@ -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]])
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
64
Regions.py
64
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',
|
||||
|
|
18
Rom.py
18
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)
|
||||
|
|
2
Rules.py
2
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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue