Merge branch 'new_shops' into Archipelago_Main

This commit is contained in:
Fabian Dill 2021-10-02 06:58:43 +02:00
commit d1709764ef
4 changed files with 142 additions and 21 deletions

View File

@ -1057,17 +1057,19 @@ class Spoiler():
listed_locations.update(other_locations) listed_locations.update(other_locations)
self.shops = [] self.shops = []
from worlds.alttp.Shops import ShopType from worlds.alttp.Shops import ShopType, price_type_display_name, price_rate_display
for shop in self.world.shops: for shop in self.world.shops:
if not shop.custom: if not shop.custom:
continue continue
shopdata = {'location': str(shop.region), shopdata = {
'location': str(shop.region),
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop' 'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
} }
for index, item in enumerate(shop.inventory): for index, item in enumerate(shop.inventory):
if item is None: if item is None:
continue continue
shopdata['item_{}'.format(index)] = "{}{}".format(item['item'], item['price']) if item['price'] else item['item'] my_price = item['price'] // price_rate_display.get(item['price_type'], 1)
shopdata['item_{}'.format(index)] = f"{item['item']}{my_price} {price_type_display_name[item['price_type']]}"
if item['player'] > 0: if item['player'] > 0:
shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('', '(Player {}) — '.format(item['player'])) shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('', '(Player {}) — '.format(item['player']))

Binary file not shown.

View File

@ -4,7 +4,7 @@ import Utils
from Patch import read_rom from Patch import read_rom
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '13a75c5dd28055fbcf8f69bd8161871d' RANDOMIZERBASEHASH = 'e397fef0e947d1bd760c68c4fe99a600'
import io import io
import json import json
@ -22,7 +22,7 @@ from typing import Optional
from BaseClasses import CollectionState, Region from BaseClasses import CollectionState, Region
from worlds.alttp.SubClasses import ALttPLocation from worlds.alttp.SubClasses import ALttPLocation
from worlds.alttp.Shops import ShopType from worlds.alttp.Shops import ShopType, ShopPriceType
from worlds.alttp.Dungeons import dungeon_music_addresses from worlds.alttp.Dungeons import dungeon_music_addresses
from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address
from worlds.alttp.Text import MultiByteTextMapper, text_addresses, Credits, TextTable from worlds.alttp.Text import MultiByteTextMapper, text_addresses, Credits, TextTable
@ -1707,9 +1707,16 @@ def write_custom_shops(rom, world, player):
# [id][item][price-low][price-high][max][repl_id][repl_price-low][repl_price-high][player] # [id][item][price-low][price-high][max][repl_id][repl_price-low][repl_price-high][player]
for index, item in enumerate(shop.inventory): for index, item in enumerate(shop.inventory):
slot = 0 if shop.type == ShopType.TakeAny else index
if item is None: if item is None:
break break
if item['price_type'] != ShopPriceType.Rupees:
# Set special price flag 0x8000
# Then set the type of price we're setting 0x7F00 (this starts from Hearts, not Rupees, subtract 1)
# Then append the price/index into the second byte 0x00FF
price_data = int16_as_bytes(0x8000 | 0x100 * (item["price_type"] - 1) | item['price'])
else:
price_data = int16_as_bytes(item['price'])
slot = 0 if shop.type == ShopType.TakeAny else index
if not item['item'] in item_table: # item not native to ALTTP if not item['item'] in item_table: # item not native to ALTTP
item_code = get_nonnative_item_sprite(item['item']) item_code = get_nonnative_item_sprite(item['item'])
else: else:
@ -1717,7 +1724,7 @@ def write_custom_shops(rom, world, player):
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]: if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]:
rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask) rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask)
item_data = [shop_id, item_code] + int16_as_bytes(item['price']) + \ item_data = [shop_id, item_code] + price_data + \
[item['max'], ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + \ [item['max'], ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + \
int16_as_bytes(item['replacement_price']) + [0 if item['player'] == player else item['player']] int16_as_bytes(item['replacement_price']) + [0 if item['player'] == player else item['player']]
items_data.extend(item_data) items_data.extend(item_data)

View File

@ -1,5 +1,5 @@
from __future__ import annotations from __future__ import annotations
from enum import unique, Enum from enum import unique, IntEnum
from typing import List, Optional, Set, NamedTuple, Dict from typing import List, Optional, Set, NamedTuple, Dict
import logging import logging
@ -13,12 +13,27 @@ logger = logging.getLogger("Shops")
@unique @unique
class ShopType(Enum): class ShopType(IntEnum):
Shop = 0 Shop = 0
TakeAny = 1 TakeAny = 1
UpgradeShop = 2 UpgradeShop = 2
@unique
class ShopPriceType(IntEnum):
Rupees = 0
Hearts = 1
Magic = 2
Bombs = 3
Arrows = 4
HeartContainer = 5
BombUpgrade = 6
ArrowUpgrade = 7
Keys = 8
Potion = 9
Item = 10
class Shop(): class Shop():
slots: int = 3 # slot count is not dynamic in asm, however inventory can have None as empty slots slots: int = 3 # slot count is not dynamic in asm, however inventory can have None as empty slots
blacklist: Set[str] = set() # items that don't work, todo: actually check against this blacklist: Set[str] = set() # items that don't work, todo: actually check against this
@ -87,10 +102,11 @@ class Shop():
def add_inventory(self, slot: int, item: str, price: int, max: int = 0, 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): player: int = 0, price_type: int = ShopPriceType.Rupees):
self.inventory[slot] = { self.inventory[slot] = {
'item': item, 'item': item,
'price': price, 'price': price,
'price_type': price_type,
'max': max, 'max': max,
'replacement': replacement, 'replacement': replacement,
'replacement_price': replacement_price, 'replacement_price': replacement_price,
@ -98,7 +114,8 @@ class Shop():
'player': player 'player': player
} }
def push_inventory(self, slot: int, item: str, price: int, max: int = 1, player: int = 0): def push_inventory(self, slot: int, item: str, price: int, max: int = 1, player: int = 0,
price_type: int = ShopPriceType.Rupees):
if not self.inventory[slot]: if not self.inventory[slot]:
raise ValueError("Inventory can't be pushed back if it doesn't exist") raise ValueError("Inventory can't be pushed back if it doesn't exist")
@ -108,6 +125,7 @@ class Shop():
self.inventory[slot] = { self.inventory[slot] = {
'item': item, 'item': item,
'price': price, 'price': price,
'price_type': price_type,
'max': max, 'max': max,
'replacement': self.inventory[slot]["item"], 'replacement': self.inventory[slot]["item"],
'replacement_price': self.inventory[slot]["price"], 'replacement_price': self.inventory[slot]["price"],
@ -170,7 +188,8 @@ def ShopSlotFill(world):
blacklist_word in item_name for blacklist_word in blacklist_words)} blacklist_word in item_name for blacklist_word in blacklist_words)}
blacklist_words.add("Bee") blacklist_words.add("Bee")
locations_per_sphere = list(sorted(sphere, key=lambda location: location.name) for sphere in world.get_spheres()) locations_per_sphere = list(
sorted(sphere, key=lambda location: location.name) for sphere in world.get_spheres())
# currently special care needs to be taken so that Shop.region.locations.item is identical to Shop.inventory # 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 # Potentially create Locations as needed and make inventory the only source, to prevent divergence
@ -226,7 +245,8 @@ def ShopSlotFill(world):
item_name = location.item.name item_name = location.item.name
if location.item.game != "A Link to the Past": if location.item.game != "A Link to the Past":
price = world.random.randrange(1, 28) price = world.random.randrange(1, 28)
elif any(x in item_name for x in ['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']): elif any(x in item_name for x in
['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
price = world.random.randrange(1, 7) price = world.random.randrange(1, 7)
elif any(x in item_name for x in ['Arrow', 'Bomb', 'Clock']): elif any(x in item_name for x in ['Arrow', 'Bomb', 'Clock']):
price = world.random.randrange(2, 14) price = world.random.randrange(2, 14)
@ -254,7 +274,9 @@ def create_shops(world, player: int):
world.random.shuffle(single_purchase_slots) world.random.shuffle(single_purchase_slots)
if 'g' in option or 'f' in option: if 'g' in option or 'f' in option:
default_shop_table = [i for l in [shop_generation_types[x] for x in ['arrows', 'bombs', 'potions', 'shields', 'bottle'] if not world.retro[player] or x != 'arrows'] for i in l] default_shop_table = [i for l in
[shop_generation_types[x] for x in ['arrows', 'bombs', 'potions', 'shields', 'bottle'] if
not world.retro[player] or x != 'arrows'] for i in l]
new_basic_shop = world.random.sample(default_shop_table, k=3) new_basic_shop = world.random.sample(default_shop_table, k=3)
new_dark_shop = world.random.sample(default_shop_table, k=3) new_dark_shop = world.random.sample(default_shop_table, k=3)
for name, shop in player_shop_table.items(): for name, shop in player_shop_table.items():
@ -272,7 +294,8 @@ def create_shops(world, player: int):
# make sure that blue potion is available in inverted, special case locked = None; lock when done. # make sure that blue potion is available in inverted, special case locked = None; lock when done.
player_shop_table["Dark Lake Hylia Shop"] = \ player_shop_table["Dark Lake Hylia Shop"] = \
player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None) player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None)
chance_100 = int(world.retro[player])*0.25+int(world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) * 0.5 chance_100 = int(world.retro[player]) * 0.25 + int(
world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) * 0.5
for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram_offset) in player_shop_table.items(): for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram_offset) in player_shop_table.items():
region = world.get_region(region_name, player) region = world.get_region(region_name, player)
shop: Shop = shop_class_mapping[type](region, room_id, shopkeeper, custom, locked, sram_offset) shop: Shop = shop_class_mapping[type](region, room_id, shopkeeper, custom, locked, sram_offset)
@ -344,7 +367,8 @@ total_dynamic_shop_slots = sum(3 for shopname, data in shop_table.items() if not
SHOP_ID_START = 0x400000 SHOP_ID_START = 0x400000
shop_table_by_location_id = dict(enumerate( shop_table_by_location_id = dict(enumerate(
(f"{name} {Shop.slot_names[num]}" for name, shop_data in sorted(shop_table.items(), key=lambda item: item[1].sram_offset) (f"{name} {Shop.slot_names[num]}" for name, shop_data in
sorted(shop_table.items(), key=lambda item: item[1].sram_offset)
for num in range(3)), start=SHOP_ID_START)) for num in range(3)), start=SHOP_ID_START))
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots)] = "Old Man Sword Cave" shop_table_by_location_id[(SHOP_ID_START + total_shop_slots)] = "Old Man Sword Cave"
@ -371,7 +395,8 @@ def set_up_shops(world, player: int):
if world.retro[player]: if world.retro[player]:
rss = world.get_region('Red Shield Shop', player).shop rss = world.get_region('Red Shield Shop', player).shop
replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50], replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50],
['Blue Shield', 50], ['Small Heart', 10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them. ['Blue Shield', 50], ['Small Heart',
10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them.
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal: if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
replacement_items.append(['Small Key (Universal)', 100]) replacement_items.append(['Small Key (Universal)', 100])
replacement_item = world.random.choice(replacement_items) replacement_item = world.random.choice(replacement_items)
@ -421,7 +446,8 @@ def shuffle_shops(world, items, player: int):
if not new_items: if not new_items:
break break
else: else:
logging.warning(f"Not all upgrades put into Player{player}' item pool. Putting remaining items in Capacity Upgrade shop instead.") logging.warning(
f"Not all upgrades put into Player{player}' item pool. Putting remaining items in Capacity Upgrade shop instead.")
bombupgrades = sum(1 for item in new_items if 'Bomb Upgrade' in item) bombupgrades = sum(1 for item in new_items if 'Bomb Upgrade' in item)
arrowupgrades = sum(1 for item in new_items if 'Arrow Upgrade' in item) arrowupgrades = sum(1 for item in new_items if 'Arrow Upgrade' in item)
if bombupgrades: if bombupgrades:
@ -432,7 +458,7 @@ def shuffle_shops(world, items, player: int):
for item in new_items: for item in new_items:
world.push_precollected(ItemFactory(item, player)) world.push_precollected(ItemFactory(item, player))
if 'p' in option or 'i' in option: if any(setting in option for setting in 'ipP'):
shops = [] shops = []
upgrade_shops = [] upgrade_shops = []
total_inventory = [] total_inventory = []
@ -461,6 +487,13 @@ def shuffle_shops(world, items, player: int):
for item in shop.inventory: for item in shop.inventory:
adjust_item(item) adjust_item(item)
if 'P' in option:
for item in total_inventory:
price_to_funny_price(item, world, player)
# Don't apply to upgrade shops
# Upgrade shop is only one place, and will generally be too easy to
# replenish hearts and bombs
if 'i' in option: if 'i' in option:
world.random.shuffle(total_inventory) world.random.shuffle(total_inventory)
@ -469,3 +502,82 @@ def shuffle_shops(world, items, player: int):
slots = shop.slots slots = shop.slots
shop.inventory = total_inventory[i:i + slots] shop.inventory = total_inventory[i:i + slots]
i += slots i += slots
price_blacklist = {
ShopPriceType.Rupees: {'Rupees'},
ShopPriceType.Hearts: {'Small Heart', 'Apple'},
ShopPriceType.Magic: {'Magic Jar'},
ShopPriceType.Bombs: {'Bombs', 'Single Bomb'},
ShopPriceType.Arrows: {'Arrows', 'Single Arrow'},
ShopPriceType.HeartContainer: {},
ShopPriceType.BombUpgrade: {"Bomb Upgrade"},
ShopPriceType.ArrowUpgrade: {"Arrow Upgrade"},
ShopPriceType.Keys: {"Small Key"},
ShopPriceType.Potion: {},
}
price_chart = {
ShopPriceType.Rupees: lambda p: p,
ShopPriceType.Hearts: lambda p: min(5, p // 5) * 8, # Each heart is 0x8 in memory, Max of 5 hearts (20 total??)
ShopPriceType.Magic: lambda p: min(15, p // 5) * 8, # Each pip is 0x8 in memory, Max of 15 pips (16 total...)
ShopPriceType.Bombs: lambda p: max(1, min(10, p // 5)), # 10 Bombs max
ShopPriceType.Arrows: lambda p: max(1, min(30, p // 5)), # 30 Arrows Max
ShopPriceType.HeartContainer: lambda p: 0x8,
ShopPriceType.BombUpgrade: lambda p: 0x1,
ShopPriceType.ArrowUpgrade: lambda p: 0x1,
ShopPriceType.Keys: lambda p: min(3, (p // 100) + 1), # Max of 3 keys for a price
ShopPriceType.Potion: lambda p: (p // 5) % 5,
}
price_type_display_name = {
ShopPriceType.Rupees: "Rupees",
ShopPriceType.Hearts: "Hearts",
ShopPriceType.Bombs: "Bombs",
ShopPriceType.Arrows: "Arrows",
ShopPriceType.Keys: "Keys",
}
# price division
price_rate_display = {
ShopPriceType.Hearts: 8,
ShopPriceType.Magic: 8,
}
# prices with no? logic requirements
simple_price_types = [
ShopPriceType.Rupees,
ShopPriceType.Hearts,
ShopPriceType.Bombs,
ShopPriceType.Arrows,
ShopPriceType.Keys
]
def price_to_funny_price(item: dict, world, player: int):
"""
Converts a raw Rupee price into a special price type
"""
if item:
my_price_types = simple_price_types.copy()
world.random.shuffle(my_price_types)
for p_type in my_price_types:
# Ignore rupee prices, logic-based prices or Keys (if we're not on universal keys)
if p_type in [ShopPriceType.Rupees, ShopPriceType.BombUpgrade, ShopPriceType.ArrowUpgrade]:
return
# If we're using keys...
# Check if we're in universal, check if our replacement isn't a Small Key
# Check if price isn't super small... (this will ideally be handled in a future table)
if p_type in [ShopPriceType.Keys]:
if world.smallkey_shuffle[player] != smallkey_shuffle.option_universal:
continue
elif item['replacement'] and 'Small Key' in item['replacement']:
continue
if item['price'] < 50:
continue
if any(x in item['item'] for x in price_blacklist[p_type]):
continue
else:
item['price'] = min(price_chart[p_type](item['price']) , 255)
item['price_type'] = p_type
break