Merge branch 'multishop'
This commit is contained in:
commit
a6dad66b59
BaseClasses.pyDungeons.pyEntranceRandomizer.pyFill.pyItemPool.pyItems.pyMain.pyMultiClient.pyMystery.pyRegions.pyRom.pyShops.pyUtils.py
WebHostLib/static/static
data
playerSettings.yamltest
dungeons
inverted
inverted_minor_glitches
inverted_owg
minor_glitches
owg
shops
vanilla
209
BaseClasses.py
209
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
|
||||
|
||||
|
||||
|
@ -134,6 +133,7 @@ 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('shuffle_prizes', "g")
|
||||
set_player_attr('sprite_pool', [])
|
||||
set_player_attr('dark_room_logic', "lamp")
|
||||
|
@ -415,7 +415,7 @@ class World(object):
|
|||
else:
|
||||
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
|
||||
|
||||
def can_beat_game(self, starting_state=None):
|
||||
def can_beat_game(self, starting_state : Optional[CollectionState]=None):
|
||||
if starting_state:
|
||||
if self.has_beaten_game(starting_state):
|
||||
return True
|
||||
|
@ -447,6 +447,87 @@ class World(object):
|
|||
|
||||
return False
|
||||
|
||||
def get_spheres(self):
|
||||
state = CollectionState(self)
|
||||
|
||||
locations = {location for location in self.get_locations()}
|
||||
|
||||
while locations:
|
||||
sphere = set()
|
||||
|
||||
for location in locations:
|
||||
if location.can_reach(state):
|
||||
sphere.add(location)
|
||||
yield sphere
|
||||
if not sphere:
|
||||
if locations:
|
||||
yield locations # unreachable locations
|
||||
break
|
||||
|
||||
for location in sphere:
|
||||
state.collect(location.item, True, location)
|
||||
locations -= sphere
|
||||
|
||||
|
||||
|
||||
def fulfills_accessibility(self, state: Optional[CollectionState] = None):
|
||||
"""Check if accessibility rules are fulfilled with current or supplied state."""
|
||||
if not state:
|
||||
state = CollectionState(self)
|
||||
players = {"none" : set(),
|
||||
"items": set(),
|
||||
"locations": set()}
|
||||
for player, access in self.accessibility.items():
|
||||
players[access].add(player)
|
||||
|
||||
beatable_fulfilled = False
|
||||
|
||||
def location_conditition(location : Location):
|
||||
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
|
||||
if location.player in players["none"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def location_relevant(location : Location):
|
||||
"""Determine if this location is relevant to sweep."""
|
||||
if location.player in players["locations"] or location.event or \
|
||||
(location.item and location.item.advancement):
|
||||
return True
|
||||
return False
|
||||
|
||||
def all_done():
|
||||
"""Check if all access rules are fulfilled"""
|
||||
if beatable_fulfilled:
|
||||
if any(location_conditition(location) for location in locations):
|
||||
return False # still locations required to be collected
|
||||
return True
|
||||
|
||||
locations = {location for location in self.get_locations() if location_relevant(location)}
|
||||
|
||||
while locations:
|
||||
sphere = set()
|
||||
for location in locations:
|
||||
if location.can_reach(state):
|
||||
sphere.add(location)
|
||||
|
||||
if not sphere:
|
||||
# ran out of places and did not finish yet, quit
|
||||
logging.debug(f"Could not access required locations.")
|
||||
return False
|
||||
|
||||
for location in sphere:
|
||||
locations.remove(location)
|
||||
state.collect(location.item, True, location)
|
||||
|
||||
if self.has_beaten_game(state):
|
||||
beatable_fulfilled = True
|
||||
|
||||
if all_done():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class CollectionState(object):
|
||||
|
||||
def __init__(self, parent: World):
|
||||
|
@ -980,7 +1061,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
|
||||
|
@ -990,7 +1071,13 @@ 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
|
||||
shop_slot_disabled: 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):
|
||||
|
@ -1003,8 +1090,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
|
||||
|
@ -1029,6 +1114,9 @@ class Location(object):
|
|||
def __hash__(self):
|
||||
return hash((self.name, self.player))
|
||||
|
||||
def __lt__(self, other):
|
||||
return (self.player, self.name) < (other.player, other.name)
|
||||
|
||||
|
||||
class Item(object):
|
||||
|
||||
|
@ -1086,105 +1174,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):
|
||||
self.inventory[slot] = {
|
||||
'item': item,
|
||||
'price': price,
|
||||
'max': max,
|
||||
'replacement': replacement,
|
||||
'replacement_price': replacement_price,
|
||||
'create_location': create_location
|
||||
}
|
||||
|
||||
def push_inventory(self, slot: int, item: str, price: int, max: int = 1):
|
||||
if not self.inventory[slot]:
|
||||
raise ValueError("Inventory can't be pushed back if it doesn't exist")
|
||||
|
||||
self.inventory[slot] = {
|
||||
'item': item,
|
||||
'price': price,
|
||||
'max': max,
|
||||
'replacement': self.inventory[slot]["item"],
|
||||
'replacement_price': self.inventory[slot]["price"],
|
||||
'create_location': self.inventory[slot]["create_location"]
|
||||
}
|
||||
|
||||
|
||||
class 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
|
||||
|
@ -1247,6 +1236,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
|
||||
|
@ -1257,6 +1247,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 +1324,7 @@ 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,
|
||||
'shuffle_prizes': self.world.shuffle_prizes,
|
||||
'sprite_pool': self.world.sprite_pool,
|
||||
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss
|
||||
|
|
|
@ -144,7 +144,7 @@ def fill_dungeons_restrictive(world):
|
|||
# sort in the order Big Key, Small Key, Other before placing dungeon items
|
||||
sort_order = {"BigKey": 3, "SmallKey": 2}
|
||||
dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1))
|
||||
fill_restrictive(world, all_state_base, locations, dungeon_items, True)
|
||||
fill_restrictive(world, all_state_base, locations, dungeon_items, True, True)
|
||||
|
||||
|
||||
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
|
||||
|
|
|
@ -326,9 +326,17 @@ 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
|
||||
w: consider witch's hut like any other shop and shuffle/randomize it too
|
||||
''')
|
||||
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('--shuffle_prizes', default=defval('g'), choices=['', 'g', 'b', 'gb'])
|
||||
parser.add_argument('--sprite_pool', help='''\
|
||||
|
@ -390,7 +398,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",
|
||||
"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',
|
||||
|
|
27
Fill.py
27
Fill.py
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
import typing
|
||||
|
||||
from BaseClasses import CollectionState, PlandoItem
|
||||
from BaseClasses import CollectionState, PlandoItem, Location
|
||||
from Items import ItemFactory
|
||||
from Regions import key_drop_data
|
||||
|
||||
|
@ -10,7 +10,8 @@ class FillError(RuntimeError):
|
|||
pass
|
||||
|
||||
|
||||
def fill_restrictive(world, base_state: CollectionState, locations, itempool, single_player_placement=False):
|
||||
def fill_restrictive(world, base_state: CollectionState, locations, itempool, single_player_placement=False,
|
||||
lock=False):
|
||||
def sweep_from_pool():
|
||||
new_state = base_state.copy()
|
||||
for item in itempool:
|
||||
|
@ -59,6 +60,8 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
|
|||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
if lock:
|
||||
spot_to_fill.locked = True
|
||||
locations.remove(spot_to_fill)
|
||||
placements.append(spot_to_fill)
|
||||
spot_to_fill.event = True
|
||||
|
@ -168,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)
|
||||
|
@ -244,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:
|
||||
|
@ -331,7 +336,8 @@ def balance_multiworld_progression(world):
|
|||
replacement_locations.insert(0, new_location)
|
||||
new_location = replacement_locations.pop()
|
||||
|
||||
new_location.item, old_location.item = old_location.item, new_location.item
|
||||
swap_location_item(old_location, new_location)
|
||||
|
||||
new_location.event, old_location.event = True, False
|
||||
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
|
||||
f"displacing {old_location.item} in {old_location}")
|
||||
|
@ -355,6 +361,18 @@ def balance_multiworld_progression(world):
|
|||
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
|
||||
|
||||
|
||||
def swap_location_item(location_1: Location, location_2: Location, check_locked=True):
|
||||
"""Swaps Items of locations. Does NOT swap flags like event, shop_slot or locked"""
|
||||
if check_locked:
|
||||
if location_1.locked:
|
||||
logging.warning(f"Swapping {location_1}, which is marked as locked.")
|
||||
if location_2.locked:
|
||||
logging.warning(f"Swapping {location_2}, which is marked as locked.")
|
||||
location_2.item, location_1.item = location_1.item, location_2.item
|
||||
location_1.item.location = location_1
|
||||
location_2.item.location = location_2
|
||||
|
||||
|
||||
def distribute_planned(world):
|
||||
world_name_lookup = {world.player_names[player_id][0]: player_id for player_id in world.player_ids}
|
||||
|
||||
|
@ -362,7 +380,8 @@ def distribute_planned(world):
|
|||
placement: PlandoItem
|
||||
for placement in world.plando_items[player]:
|
||||
if placement.location in key_drop_data:
|
||||
placement.warn(f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
|
||||
placement.warn(
|
||||
f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
|
||||
continue
|
||||
item = ItemFactory(placement.item, player)
|
||||
target_world: int = placement.world
|
||||
|
|
32
ItemPool.py
32
ItemPool.py
|
@ -1,7 +1,8 @@
|
|||
from collections import namedtuple
|
||||
import logging
|
||||
|
||||
from BaseClasses import Region, RegionType, ShopType, Location, TakeAny
|
||||
from BaseClasses import Region, RegionType, Location
|
||||
from Shops import ShopType, Shop, TakeAny, total_shop_slots
|
||||
from Bosses import place_bosses
|
||||
from Dungeons import get_dungeon_item_pool
|
||||
from EntranceShuffle import connect_entrance
|
||||
|
@ -460,10 +461,12 @@ def shuffle_shops(world, items, player: int):
|
|||
|
||||
world.random.shuffle(new_items) # Decide what gets tossed randomly if it can't insert everything.
|
||||
|
||||
capacityshop: Shop = None
|
||||
for shop in world.shops:
|
||||
if shop.type == ShopType.UpgradeShop and shop.region.player == player and \
|
||||
shop.region.name == "Capacity Upgrade":
|
||||
shop.clear_inventory()
|
||||
capacityshop = shop
|
||||
|
||||
if world.goal[player] != 'icerodhunt':
|
||||
for i, item in enumerate(items):
|
||||
|
@ -472,7 +475,13 @@ def shuffle_shops(world, items, player: int):
|
|||
if not new_items:
|
||||
break
|
||||
else:
|
||||
logging.warning(f"Not all upgrades put into Player{player}' item pool. Still missing: {new_items}")
|
||||
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)
|
||||
arrowupgrades = sum(1 for item in new_items if 'Arrow Upgrade' in item)
|
||||
if bombupgrades:
|
||||
capacityshop.add_inventory(1, 'Bomb Upgrade (+5)', 100, bombupgrades)
|
||||
if arrowupgrades:
|
||||
capacityshop.add_inventory(1, 'Arrow Upgrade (+5)', 100, arrowupgrades)
|
||||
else:
|
||||
for item in new_items:
|
||||
world.push_precollected(ItemFactory(item, player))
|
||||
|
@ -485,14 +494,19 @@ def shuffle_shops(world, items, player: int):
|
|||
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':
|
||||
elif shop.type == ShopType.Shop:
|
||||
if shop.region.name == 'Potion Shop' and not 'w' in option:
|
||||
# don't modify potion shop
|
||||
pass
|
||||
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 +521,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
|
||||
|
@ -548,7 +563,7 @@ def set_up_take_anys(world, player):
|
|||
entrance = world.get_region(reg, player).entrances[0]
|
||||
connect_entrance(world, entrance.name, old_man_take_any.name, player)
|
||||
entrance.target = 0x58
|
||||
old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True)
|
||||
old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots)
|
||||
world.shops.append(old_man_take_any.shop)
|
||||
|
||||
swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player]
|
||||
|
@ -570,7 +585,7 @@ def set_up_take_anys(world, player):
|
|||
entrance = world.get_region(reg, player).entrances[0]
|
||||
connect_entrance(world, entrance.name, take_any.name, player)
|
||||
entrance.target = target
|
||||
take_any.shop = TakeAny(take_any, room_id, 0xE3, True, True)
|
||||
take_any.shop = TakeAny(take_any, room_id, 0xE3, True, True, total_shop_slots + num + 1)
|
||||
world.shops.append(take_any.shop)
|
||||
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
|
||||
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0)
|
||||
|
@ -584,13 +599,14 @@ 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, "{} 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)
|
||||
loc.shop_slot = True
|
||||
loc.event = True
|
||||
loc.locked = True
|
||||
|
||||
|
@ -611,7 +627,7 @@ def fill_prizes(world, attempts=15):
|
|||
prize_locs = list(empty_crystal_locations)
|
||||
world.random.shuffle(prizepool)
|
||||
world.random.shuffle(prize_locs)
|
||||
fill_restrictive(world, all_state, prize_locs, prizepool, True)
|
||||
fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True)
|
||||
except FillError as e:
|
||||
logging.getLogger('').exception("Failed to place dungeon prizes (%s). Will retry %s more times", e,
|
||||
attempts - attempt)
|
||||
|
|
6
Items.py
6
Items.py
|
@ -169,6 +169,12 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
|
|||
'Small Key (Universal)': (False, True, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key'),
|
||||
'Nothing': (False, False, None, 0x5A, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again', 'nothing'),
|
||||
'Bee Trap': (False, False, None, 0xB0, 'We will sting your face a whole lot!', 'and the sting buddies', 'the beekeeper kid', 'insects for sale', 'shroom pollenation', 'bottle boy has mad bees again', 'Friendship'),
|
||||
'Faerie': (False, False, None, 0xB1, 'Save me and I will revive you', 'and the captive', 'the tingle kid','hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a faerie'),
|
||||
'Good Bee': (False, False, None, 0xB2, 'Save me and I will sting you (sometimes)', 'and the captive', 'the tingle kid','hostage for sale', 'good dust and shrooms', 'bottle boy has friend again', 'a bee'),
|
||||
'Magic Jar': (False, False, None, 0xB3, '', '', '','', '', '', ''),
|
||||
'Apple': (False, False, None, 0xB4, '', '', '','', '', '', ''),
|
||||
# 'Hint': (False, False, None, 0xB5, '', '', '','', '', '', ''),
|
||||
# 'Bomb Trap': (False, False, None, 0xB6, '', '', '','', '', '', ''),
|
||||
'Red Potion': (False, False, None, 0x2E, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a red potion'),
|
||||
'Green Potion': (False, False, None, 0x2F, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a green potion'),
|
||||
'Blue Potion': (False, False, None, 0x30, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a blue potion'),
|
||||
|
|
50
Main.py
50
Main.py
|
@ -9,9 +9,10 @@ import time
|
|||
import zlib
|
||||
import concurrent.futures
|
||||
|
||||
from BaseClasses import World, CollectionState, Item, Region, Location, PlandoItem
|
||||
from BaseClasses import World, CollectionState, Item, Region, Location
|
||||
from Shops import ShopSlotFill, create_shops, SHOP_ID_START, FillDisabledShopSlots
|
||||
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
|
||||
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
|
||||
|
@ -83,6 +84,7 @@ 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.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()
|
||||
|
@ -209,8 +211,14 @@ def main(args, seed=None):
|
|||
if world.players > 1:
|
||||
balance_multiworld_progression(world)
|
||||
|
||||
logger.info("Filling Shop Slots")
|
||||
|
||||
ShopSlotFill(world)
|
||||
|
||||
logger.info('Patching ROM.')
|
||||
|
||||
|
||||
|
||||
outfilebase = 'BM_%s' % (args.outputname if args.outputname else world.seed)
|
||||
|
||||
rom_names = []
|
||||
|
@ -245,7 +253,6 @@ def main(args, seed=None):
|
|||
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
|
||||
palettes_options, world, player, True)
|
||||
|
||||
|
||||
mcsb_name = ''
|
||||
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
||||
world.bigkeyshuffle[player]]):
|
||||
|
@ -312,7 +319,7 @@ def main(args, seed=None):
|
|||
|
||||
pool = concurrent.futures.ThreadPoolExecutor()
|
||||
multidata_task = None
|
||||
check_beatability_task = pool.submit(world.can_beat_game)
|
||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||
if not args.suppress_rom:
|
||||
|
||||
rom_futures = []
|
||||
|
@ -329,13 +336,14 @@ def main(args, seed=None):
|
|||
return get_entrance_to_region(entrance.parent_region)
|
||||
|
||||
# collect ER hint info
|
||||
er_hint_data = {player: {} for player in range(1, world.players + 1) if world.shuffle[player] != "vanilla"}
|
||||
er_hint_data = {player: {} for player in range(1, world.players + 1) if world.shuffle[player] != "vanilla" or world.retro[player]}
|
||||
from Regions import RegionType
|
||||
for region in world.regions:
|
||||
if region.player in er_hint_data and region.locations:
|
||||
main_entrance = get_entrance_to_region(region)
|
||||
for location in region.locations:
|
||||
if type(location.address) == int: # skips events and crystals
|
||||
if location.address >= SHOP_ID_START + 33: continue
|
||||
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
||||
er_hint_data[region.player][location.address] = main_entrance.name
|
||||
|
||||
|
@ -362,11 +370,27 @@ def main(args, seed=None):
|
|||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
checks_in_area[location.player]["Total"] += 1
|
||||
|
||||
oldmancaves = []
|
||||
for region in [world.get_region("Old Man Sword Cave", player) for player in range(1, world.players + 1) if world.retro[player]]:
|
||||
item = ItemFactory(region.shop.inventory[0]['item'], region.player)
|
||||
player = region.player
|
||||
location_id = SHOP_ID_START + 33
|
||||
|
||||
if region.type == RegionType.LightWorld:
|
||||
checks_in_area[player]["Light World"].append(location_id)
|
||||
else:
|
||||
checks_in_area[player]["Dark World"].append(location_id)
|
||||
checks_in_area[player]["Total"] += 1
|
||||
|
||||
er_hint_data[player][location_id] = get_entrance_to_region(region).name
|
||||
oldmancaves.append(((location_id, player), (item.code, player)))
|
||||
|
||||
precollected_items = [[] for player in range(world.players)]
|
||||
for item in world.precollected_items:
|
||||
precollected_items[item.player - 1].append(item.code)
|
||||
|
||||
FillDisabledShopSlots(world)
|
||||
|
||||
def write_multidata(roms):
|
||||
for future in roms:
|
||||
rom_name = future.result()
|
||||
|
@ -379,6 +403,10 @@ def main(args, seed=None):
|
|||
if not args.skip_playthrough:
|
||||
multidatatags.append("Play through")
|
||||
minimum_versions = {"server": (1, 0, 0)}
|
||||
minimum_versions["clients"] = client_versions = []
|
||||
for (slot, team, name) in rom_names:
|
||||
if world.shop_shuffle_slots[slot]:
|
||||
client_versions.append([team, slot, [3, 6, 1]])
|
||||
multidata = zlib.compress(json.dumps({"names": parsed_names,
|
||||
# backwards compat for < 2.4.1
|
||||
"roms": [(slot, team, list(name.encode()))
|
||||
|
@ -389,7 +417,7 @@ def main(args, seed=None):
|
|||
"locations": [((location.address, location.player),
|
||||
(location.item.code, location.item.player))
|
||||
for location in world.get_filled_locations() if
|
||||
type(location.address) is int],
|
||||
type(location.address) is int] + oldmancaves,
|
||||
"checks_in_area": checks_in_area,
|
||||
"server_options": get_options()["server_options"],
|
||||
"er_hint_data": er_hint_data,
|
||||
|
@ -403,8 +431,11 @@ def main(args, seed=None):
|
|||
f.write(multidata)
|
||||
|
||||
multidata_task = pool.submit(write_multidata, rom_futures)
|
||||
if not check_beatability_task.result():
|
||||
raise Exception("Game appears unbeatable. Aborting.")
|
||||
if not check_accessibility_task.result():
|
||||
if not world.can_beat_game():
|
||||
raise Exception("Game appears is unbeatable. Aborting.")
|
||||
else:
|
||||
logger.warning("Location Accessibility requirements not fulfilled.")
|
||||
if not args.skip_playthrough:
|
||||
logger.info('Calculating playthrough.')
|
||||
create_playthrough(world)
|
||||
|
@ -458,6 +489,7 @@ def copy_world(world):
|
|||
ret.shufflepots = world.shufflepots.copy()
|
||||
ret.shuffle_prizes = world.shuffle_prizes.copy()
|
||||
ret.shop_shuffle = world.shop_shuffle.copy()
|
||||
ret.shop_shuffle_slots = world.shop_shuffle_slots.copy()
|
||||
ret.dark_room_logic = world.dark_room_logic.copy()
|
||||
ret.restrict_dungeon_item_on_boss = world.restrict_dungeon_item_on_boss.copy()
|
||||
|
||||
|
@ -530,7 +562,7 @@ def copy_dynamic_regions_and_locations(world, ret):
|
|||
|
||||
if region.shop:
|
||||
new_reg.shop = region.shop.__class__(new_reg, region.shop.room_id, region.shop.shopkeeper_config,
|
||||
region.shop.custom, region.shop.locked)
|
||||
region.shop.custom, region.shop.locked, region.shop.sram_offset)
|
||||
ret.shops.append(new_reg.shop)
|
||||
|
||||
for location in world.dynamic_locations:
|
||||
|
|
|
@ -18,6 +18,7 @@ import shutil
|
|||
|
||||
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.")
|
||||
|
@ -93,7 +94,6 @@ class Context():
|
|||
self.player_names: typing.Dict[int: str] = {}
|
||||
self.locations_recognized = set()
|
||||
self.locations_checked = set()
|
||||
self.unsafe_locations_checked = set()
|
||||
self.locations_scouted = set()
|
||||
self.items_received = []
|
||||
self.items_missing = []
|
||||
|
@ -104,7 +104,6 @@ class Context():
|
|||
self.prev_rom = None
|
||||
self.auth = None
|
||||
self.found_items = found_items
|
||||
self.send_unsafe = False
|
||||
self.finished_game = False
|
||||
self.slow_mode = False
|
||||
|
||||
|
@ -168,6 +167,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 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),
|
||||
|
@ -878,9 +882,6 @@ async def process_server_cmd(ctx: Context, cmd, args):
|
|||
raise Exception('Connection refused by the multiworld host, no reason provided')
|
||||
|
||||
elif cmd == 'Connected':
|
||||
if ctx.send_unsafe:
|
||||
ctx.send_unsafe = False
|
||||
logger.info(f'Turning off sending of ALL location checks not declared as missing. If you want it on, please use /send_unsafe true')
|
||||
Utils.persistent_store("servers", "default", ctx.server_address)
|
||||
Utils.persistent_store("servers", ctx.rom, ctx.server_address)
|
||||
ctx.team, ctx.slot = args[0]
|
||||
|
@ -1131,15 +1132,6 @@ class ClientCommandProcessor(CommandProcessor):
|
|||
else:
|
||||
self.output("Web UI was never started.")
|
||||
|
||||
def _cmd_send_unsafe(self, toggle: str = ""):
|
||||
"""Force sending of locations the server did not specify was actually missing. WARNING: This may brick online trackers. Turned off on reconnect."""
|
||||
if toggle:
|
||||
self.ctx.send_unsafe = toggle.lower() in {"1", "true", "on"}
|
||||
logger.info(f'Turning {("on" if self.ctx.send_unsafe else "off")} the option to send ALL location checks to the multiserver.')
|
||||
else:
|
||||
logger.info("You must specify /send_unsafe true explicitly.")
|
||||
self.ctx.send_unsafe = False
|
||||
|
||||
def default(self, raw: str):
|
||||
asyncio.create_task(self.ctx.send_msgs([['Say', raw]]))
|
||||
|
||||
|
@ -1169,11 +1161,11 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
|||
new_locations = []
|
||||
|
||||
def new_check(location):
|
||||
ctx.unsafe_locations_checked.add(location)
|
||||
ctx.locations_checked.add(location)
|
||||
|
||||
check = None
|
||||
if ctx.items_checked is None:
|
||||
check = f'New Check: {location} ({len(ctx.unsafe_locations_checked)}/{len(Regions.lookup_name_to_id)})'
|
||||
check = f'New Check: {location} ({len(ctx.locations_checked)}/{len(Regions.lookup_name_to_id)})'
|
||||
else:
|
||||
items_total = len(ctx.items_missing) + len(ctx.items_checked)
|
||||
if location in ctx.items_missing or location in ctx.items_checked:
|
||||
|
@ -1184,9 +1176,21 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
|||
logger.info(check)
|
||||
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)+5)
|
||||
for cnt, b in enumerate(misc_data):
|
||||
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:
|
||||
print(e)
|
||||
logger.info(f"Exception: {e}")
|
||||
|
||||
|
||||
for location, (loc_roomid, loc_mask) in location_table_uw.items():
|
||||
try:
|
||||
if location not in ctx.unsafe_locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0:
|
||||
if location not in ctx.locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0:
|
||||
new_check(location)
|
||||
except Exception as e:
|
||||
logger.exception(f"Exception: {e}")
|
||||
|
@ -1195,7 +1199,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
|||
uw_end = 0
|
||||
uw_unchecked = {}
|
||||
for location, (roomid, mask) in location_table_uw.items():
|
||||
if location not in ctx.unsafe_locations_checked:
|
||||
if location not in ctx.locations_checked:
|
||||
uw_unchecked[location] = (roomid, mask)
|
||||
uw_begin = min(uw_begin, roomid)
|
||||
uw_end = max(uw_end, roomid + 1)
|
||||
|
@ -1212,7 +1216,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
|||
ow_end = 0
|
||||
ow_unchecked = {}
|
||||
for location, screenid in location_table_ow.items():
|
||||
if location not in ctx.unsafe_locations_checked:
|
||||
if location not in ctx.locations_checked:
|
||||
ow_unchecked[location] = screenid
|
||||
ow_begin = min(ow_begin, screenid)
|
||||
ow_end = max(ow_end, screenid + 1)
|
||||
|
@ -1223,26 +1227,30 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
|||
if ow_data[screenid - ow_begin] & 0x40 != 0:
|
||||
new_check(location)
|
||||
|
||||
if not all([location in ctx.unsafe_locations_checked for location in location_table_npc.keys()]):
|
||||
if not all([location in ctx.locations_checked for location in location_table_npc.keys()]):
|
||||
npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2)
|
||||
if npc_data is not None:
|
||||
npc_value = npc_data[0] | (npc_data[1] << 8)
|
||||
for location, mask in location_table_npc.items():
|
||||
if npc_value & mask != 0 and location not in ctx.unsafe_locations_checked:
|
||||
if npc_value & mask != 0 and location not in ctx.locations_checked:
|
||||
new_check(location)
|
||||
|
||||
if not all([location in ctx.unsafe_locations_checked for location in location_table_misc.keys()]):
|
||||
if not all([location in ctx.locations_checked for location in location_table_misc.keys()]):
|
||||
misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4)
|
||||
if misc_data is not None:
|
||||
for location, (offset, mask) in location_table_misc.items():
|
||||
assert(0x3c6 <= offset <= 0x3c9)
|
||||
if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.unsafe_locations_checked:
|
||||
if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.locations_checked:
|
||||
new_check(location)
|
||||
|
||||
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])
|
||||
for location in ctx.locations_checked:
|
||||
try:
|
||||
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)
|
||||
logger.info(f"Exception: {e}")
|
||||
|
||||
|
||||
await ctx.send_msgs([['LocationChecks', new_locations]])
|
||||
|
||||
|
@ -1274,7 +1282,6 @@ async def game_watcher(ctx : Context):
|
|||
ctx.rom = rom.decode()
|
||||
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
|
||||
ctx.locations_checked = set()
|
||||
ctx.unsafe_locations_checked = set()
|
||||
ctx.locations_scouted = set()
|
||||
ctx.prev_rom = ctx.rom
|
||||
|
||||
|
|
|
@ -405,6 +405,8 @@ 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 = ''
|
||||
|
|
51
Regions.py
51
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,38 +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}
|
||||
for region_name, (room_id, type, shopkeeper, custom, locked, inventory) in 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)
|
||||
|
||||
# (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, 0xFF, False, True, [('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)])
|
||||
}
|
||||
|
||||
old_location_address_to_new_location_address = {
|
||||
0x2eb18: 0x18001b, # Bottle Merchant
|
||||
|
@ -703,10 +672,13 @@ 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)
|
||||
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',
|
||||
|
@ -832,7 +804,18 @@ lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 19125
|
|||
0x140064: 'Misery Mire',
|
||||
0x140058: 'Turtle Rock', 0x140007: 'Dark Death Mountain Ledge (West)',
|
||||
0x140040: 'Ganons Tower', 0x140043: 'Ganons Tower',
|
||||
0x14003a: 'Ganons Tower', 0x14001f: 'Ganons Tower'}
|
||||
0x14003a: 'Ganons Tower', 0x14001f: 'Ganons Tower',
|
||||
0x400000: 'Cave Shop (Dark Death Mountain)', 0x400001: 'Cave Shop (Dark Death Mountain)', 0x400002: 'Cave Shop (Dark Death Mountain)',
|
||||
0x400003: 'Red Shield Shop', 0x400004: 'Red Shield Shop', 0x400005: 'Red Shield Shop',
|
||||
0x400006: 'Dark Lake Hylia Shop', 0x400007: 'Dark Lake Hylia Shop', 0x400008: 'Dark Lake Hylia Shop',
|
||||
0x400009: 'Dark World Lumberjack Shop', 0x40000a: 'Dark World Lumberjack Shop', 0x40000b: 'Dark World Lumberjack Shop',
|
||||
0x40000c: 'Village of Outcasts Shop', 0x40000d: 'Village of Outcasts Shop', 0x40000e: 'Village of Outcasts Shop',
|
||||
0x40000f: 'Dark World Potion Shop', 0x400010: 'Dark World Potion Shop', 0x400011: 'Dark World Potion Shop',
|
||||
0x400012: 'Light World Death Mountain Shop', 0x400013: 'Light World Death Mountain Shop', 0x400014: 'Light World Death Mountain Shop',
|
||||
0x400015: 'Kakariko Shop', 0x400016: 'Kakariko Shop', 0x400017: 'Kakariko Shop',
|
||||
0x400018: 'Cave Shop (Lake Hylia)', 0x400019: 'Cave Shop (Lake Hylia)', 0x40001a: 'Cave Shop (Lake Hylia)',
|
||||
0x40001b: 'Potion Shop', 0x40001c: 'Potion Shop', 0x40001d: 'Potion Shop',
|
||||
0x40001e: 'Capacity Upgrade', 0x40001f: 'Capacity Upgrade', 0x400020: 'Capacity Upgrade'}
|
||||
|
||||
lookup_prizes = {location for location in location_table if location.endswith(" - Prize")}
|
||||
lookup_boss_drops = {location for location in location_table if location.endswith(" - Boss")}
|
35
Rom.py
35
Rom.py
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||
RANDOMIZERBASEHASH = '5a607e36a82bbd14180536c8ec3ae49b'
|
||||
RANDOMIZERBASEHASH = '93538d51eb018955a90181600e3384ba'
|
||||
|
||||
import io
|
||||
import json
|
||||
|
@ -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
|
||||
|
@ -123,6 +124,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 +134,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,15 +682,13 @@ 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:
|
||||
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 not location.crystal:
|
||||
if location.item is not None:
|
||||
# Keys in their native dungeon should use the orignal item code for keys
|
||||
|
@ -724,6 +727,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
|
||||
|
||||
|
@ -1547,31 +1551,26 @@ def patch_race_rom(rom, world, player):
|
|||
|
||||
|
||||
def write_custom_shops(rom, world, player):
|
||||
shops = [shop for shop in world.shops if shop.custom and shop.region.player == player]
|
||||
shops = sorted([shop for shop in world.shops if shop.custom and shop.region.player == player],
|
||||
key=lambda shop: shop.sram_offset)
|
||||
|
||||
shop_data = bytearray()
|
||||
items_data = bytearray()
|
||||
sram_offset = 0
|
||||
|
||||
for shop_id, shop in enumerate(shops):
|
||||
if shop_id == len(shops) - 1:
|
||||
shop_id = 0xFF
|
||||
bytes = shop.get_bytes()
|
||||
bytes[0] = shop_id
|
||||
bytes[-1] = sram_offset
|
||||
if shop.type == ShopType.TakeAny:
|
||||
sram_offset += 1
|
||||
else:
|
||||
sram_offset += shop.item_count
|
||||
bytes[-1] = shop.sram_offset
|
||||
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'],
|
||||
ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + int16_as_bytes(
|
||||
item['replacement_price'])
|
||||
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']) + [0 if item['player'] == player else item['player']]
|
||||
items_data.extend(item_data)
|
||||
|
||||
rom.write_bytes(0x184800, shop_data)
|
||||
|
|
|
@ -0,0 +1,321 @@
|
|||
from __future__ import annotations
|
||||
from enum import unique, Enum
|
||||
from typing import List, Union, Optional, Set, NamedTuple, Dict
|
||||
import logging
|
||||
|
||||
from BaseClasses import Location
|
||||
from EntranceShuffle import door_addresses
|
||||
from Items import item_name_groups, item_table, ItemFactory
|
||||
from Utils import int16_as_bytes
|
||||
|
||||
logger = logging.getLogger("Shops")
|
||||
|
||||
|
||||
@unique
|
||||
class ShopType(Enum):
|
||||
Shop = 0
|
||||
TakeAny = 1
|
||||
UpgradeShop = 2
|
||||
|
||||
|
||||
class Shop():
|
||||
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
|
||||
type = ShopType.Shop
|
||||
|
||||
def __init__(self, region, room_id: int, shopkeeper_config: int, custom: bool, locked: bool, sram_offset: int):
|
||||
self.region = region
|
||||
self.room_id = room_id
|
||||
self.inventory: List[Optional[dict]] = [None] * self.slots
|
||||
self.shopkeeper_config = shopkeeper_config
|
||||
self.custom = custom
|
||||
self.locked = locked
|
||||
self.sram_offset = sram_offset
|
||||
|
||||
@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['max']:
|
||||
if inv['replacement'] == item:
|
||||
return True
|
||||
elif inv['item'] == 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['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"]
|
||||
|
||||
|
||||
shop_class_mapping = {ShopType.UpgradeShop: UpgradeShop,
|
||||
ShopType.Shop: Shop,
|
||||
ShopType.TakeAny: TakeAny}
|
||||
|
||||
|
||||
def FillDisabledShopSlots(world):
|
||||
shop_slots: Set[Location] = {location for shop_locations in (shop.region.locations for shop in world.shops)
|
||||
for location in shop_locations if location.shop_slot and location.shop_slot_disabled}
|
||||
for location in shop_slots:
|
||||
location.shop_slot_disabled = True
|
||||
slot_num = int(location.name[-1]) - 1
|
||||
shop: Shop = location.parent_region.shop
|
||||
location.item = ItemFactory(shop.inventory[slot_num]['item'], location.player)
|
||||
location.item_rule = lambda item: item.name == location.item.name and item.player == location.player
|
||||
|
||||
|
||||
def ShopSlotFill(world):
|
||||
shop_slots: Set[Location] = {location for shop_locations in (shop.region.locations for shop in world.shops)
|
||||
for location in shop_locations if location.shop_slot}
|
||||
removed = set()
|
||||
for location in shop_slots:
|
||||
slot_num = int(location.name[-1]) - 1
|
||||
shop: Shop = location.parent_region.shop
|
||||
if not shop.can_push_inventory(slot_num) or location.shop_slot_disabled:
|
||||
removed.add(location)
|
||||
|
||||
if removed:
|
||||
shop_slots -= removed
|
||||
|
||||
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_per_sphere = list(list(sphere) for sphere in world.get_spheres())
|
||||
|
||||
candidate_condition = lambda location: not location.locked and \
|
||||
not location.shop_slot and \
|
||||
not location.item.name in blacklist_words
|
||||
|
||||
# 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
|
||||
cumu_weights = []
|
||||
|
||||
for sphere in candidates_per_sphere:
|
||||
if cumu_weights:
|
||||
x = cumu_weights[-1]
|
||||
else:
|
||||
x = 0
|
||||
cumu_weights.append(len(sphere) + x)
|
||||
world.random.shuffle(sphere)
|
||||
|
||||
for i, sphere in enumerate(candidates_per_sphere):
|
||||
current_shop_slots = [location for location in sphere if location.shop_slot and not location.shop_slot_disabled]
|
||||
if current_shop_slots:
|
||||
|
||||
for location in current_shop_slots:
|
||||
shop: Shop = location.parent_region.shop
|
||||
swapping_sphere = world.random.choices(candidates_per_sphere[i:], cum_weights=cumu_weights[i:])[0]
|
||||
for c in swapping_sphere: # chosen item locations
|
||||
if candidate_condition(c) and c.item_rule(location.item) and location.item_rule(c.item):
|
||||
swap_location_item(c, location, check_locked=False)
|
||||
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.")
|
||||
location.shop_slot_disabled = True
|
||||
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(int(location.name[-1]) - 1, item_name, price, 1,
|
||||
location.item.player if location.item.player != location.player else 0)
|
||||
|
||||
|
||||
def create_shops(world, player: int):
|
||||
option = world.shop_shuffle[player]
|
||||
|
||||
player_shop_table = shop_table.copy()
|
||||
if "w" in option:
|
||||
player_shop_table["Potion Shop"] = player_shop_table["Potion Shop"]._replace(locked=False)
|
||||
dynamic_shop_slots = total_dynamic_shop_slots + 3
|
||||
else:
|
||||
dynamic_shop_slots = total_dynamic_shop_slots
|
||||
|
||||
num_slots = min(dynamic_shop_slots, max(0, int(world.shop_shuffle_slots[player]))) # 0 to 30
|
||||
single_purchase_slots: List[bool] = [True] * num_slots + [False] * (dynamic_shop_slots - num_slots)
|
||||
world.random.shuffle(single_purchase_slots)
|
||||
|
||||
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]
|
||||
new_basic_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():
|
||||
typ, shop_id, keeper, custom, locked, items, sram_offset = shop
|
||||
if not locked:
|
||||
new_items = world.random.sample(default_shop_table, 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])
|
||||
player_shop_table[name] = ShopData(typ, shop_id, keeper, custom, locked, new_items, sram_offset)
|
||||
if world.mode[player] == "inverted":
|
||||
player_shop_table["Dark Lake Hylia Shop"] = \
|
||||
player_shop_table["Dark Lake Hylia Shop"]._replace(locked=True, items=_inverted_hylia_shop_defaults)
|
||||
for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram_offset) in player_shop_table.items():
|
||||
region = world.get_region(region_name, player)
|
||||
shop: Shop = shop_class_mapping[type](region, room_id, shopkeeper, custom, locked, sram_offset)
|
||||
region.shop = shop
|
||||
world.shops.append(shop)
|
||||
for index, item in enumerate(inventory):
|
||||
shop.add_inventory(index, *item)
|
||||
if not locked and num_slots:
|
||||
slot_name = "{} Slot {}".format(region.name, index + 1)
|
||||
loc = Location(player, slot_name, address=shop_table_by_location[slot_name],
|
||||
parent=region, hint_text="for sale")
|
||||
loc.shop_slot = True
|
||||
loc.locked = True
|
||||
if single_purchase_slots.pop():
|
||||
additional_item = 'Rupees (50)' # world.random.choice(['Rupees (50)', 'Rupees (100)', 'Rupees (300)'])
|
||||
loc.item = ItemFactory(additional_item, player)
|
||||
else:
|
||||
loc.item = ItemFactory('Nothing', player)
|
||||
loc.shop_slot_disabled = True
|
||||
shop.region.locations.append(loc)
|
||||
world.dynamic_locations.append(loc)
|
||||
world.clear_location_cache()
|
||||
|
||||
|
||||
class ShopData(NamedTuple):
|
||||
room: int
|
||||
type: ShopType
|
||||
shopkeeper: int
|
||||
custom: bool
|
||||
locked: bool
|
||||
items: List
|
||||
sram_offset: int
|
||||
|
||||
|
||||
# (type, room_id, shopkeeper, custom, locked, [items], sram_offset)
|
||||
# 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)]
|
||||
_inverted_hylia_shop_defaults = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)]
|
||||
shop_table: Dict[str, ShopData] = {
|
||||
'Cave Shop (Dark Death Mountain)': ShopData(0x0112, ShopType.Shop, 0xC1, True, False, _basic_shop_defaults, 0),
|
||||
'Red Shield Shop': ShopData(0x0110, ShopType.Shop, 0xC1, True, False,
|
||||
[('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)], 3),
|
||||
'Dark Lake Hylia Shop': ShopData(0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults, 6),
|
||||
'Dark World Lumberjack Shop': ShopData(0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults, 9),
|
||||
'Village of Outcasts Shop': ShopData(0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults, 12),
|
||||
'Dark World Potion Shop': ShopData(0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults, 15),
|
||||
'Light World Death Mountain Shop': ShopData(0x00FF, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults, 18),
|
||||
'Kakariko Shop': ShopData(0x011F, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults, 21),
|
||||
'Cave Shop (Lake Hylia)': ShopData(0x0112, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults, 24),
|
||||
'Potion Shop': ShopData(0x0109, ShopType.Shop, 0xA0, True, True,
|
||||
[('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)], 27),
|
||||
'Capacity Upgrade': ShopData(0x0115, ShopType.UpgradeShop, 0x04, True, True,
|
||||
[('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)], 30)
|
||||
}
|
||||
|
||||
total_shop_slots = len(shop_table) * 3
|
||||
total_dynamic_shop_slots = sum(3 for shopname, data in shop_table.items() if not data[4]) # data[4] -> locked
|
||||
|
||||
SHOP_ID_START = 0x400000
|
||||
shop_table_by_location_id = {cnt: s for cnt, s in enumerate(
|
||||
(f"{name} Slot {num}" for name in [key for key, value in sorted(shop_table.items(), key=lambda item: item[1].sram_offset)]
|
||||
for num in range(1, 4)), 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 + 1)] = "Take-Any #1"
|
||||
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 2)] = "Take-Any #2"
|
||||
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 3)] = "Take-Any #3"
|
||||
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 4)] = "Take-Any #4"
|
||||
shop_table_by_location = {y: x for x, y in shop_table_by_location_id.items()}
|
||||
|
||||
shop_generation_types = {
|
||||
'arrows': [('Single Arrow', 5), ('Arrows (10)', 50)],
|
||||
'bombs': [('Single Bomb', 10), ('Bombs (3)', 30), ('Bombs (10)', 50)],
|
||||
'shields': [('Red Shield', 500), ('Blue Shield', 50)],
|
||||
'potions': [('Red Potion', 150), ('Green Potion', 90), ('Blue Potion', 190)],
|
||||
'discount_potions': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
|
||||
'bottle': [('Small Heart', 10), ('Apple', 50), ('Bee', 10), ('Good Bee', 100), ('Faerie', 100), ('Magic Jar', 100)],
|
||||
'time': [('Red Clock', 100), ('Blue Clock', 200), ('Green Clock', 300)],
|
||||
}
|
2
Utils.py
2
Utils.py
|
@ -13,7 +13,7 @@ class Version(typing.NamedTuple):
|
|||
micro: int
|
||||
|
||||
|
||||
__version__ = "3.6.0"
|
||||
__version__ = "3.6.1"
|
||||
_version_tuple = tuplize_version(__version__)
|
||||
|
||||
import os
|
||||
|
|
|
@ -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,14 +211,25 @@ 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
|
||||
### Shop Settings ###
|
||||
shop_shuffle_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
|
||||
0: 50
|
||||
5: 0
|
||||
15: 0
|
||||
30: 0
|
||||
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)
|
||||
w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
|
||||
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
|
||||
### End of Shop Section ###
|
||||
shuffle_prizes: # aka drops
|
||||
none: 0 # do not shuffle prize packs
|
||||
g: 50 # shuffle "general" price packs, as in enemy, tree pull, dig etc.
|
||||
|
@ -253,11 +264,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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
from Shops import shop_table
|
||||
from test.TestBase import TestBase
|
||||
|
||||
|
||||
class TestSram(TestBase):
|
||||
def testUniqueOffset(self):
|
||||
sram_ids = set()
|
||||
for shop_name, shopdata in shop_table.items():
|
||||
for x in range(3):
|
||||
new = shopdata.sram_offset + x
|
||||
with self.subTest(shop_name, slot=x + 1, offset=new):
|
||||
self.assertNotIn(new, sram_ids)
|
||||
sram_ids.add(new)
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue