Merge branch 'main' of https://github.com/Ijwu/Archipelago into main
This commit is contained in:
commit
e960d7b58c
BaseClasses.pyFactorioClient.pyFill.pyGenerate.pyLttPClient.pyMain.pyMultiServer.pyNetUtils.pyOptions.pyREADME.mdUtils.py
WebHostLib
playerSettings.yamltest/inverted
worlds
AutoWorld.py
alttp
Dungeons.pyEntranceRandomizer.pyItemPool.pyOptions.pyRegions.pyRom.pyRules.pyShops.pySubClasses.py__init__.py
factorio
generic
hk
minecraft
oribf
spire
124
BaseClasses.py
124
BaseClasses.py
|
@ -6,7 +6,7 @@ import logging
|
|||
import json
|
||||
import functools
|
||||
from collections import OrderedDict, Counter, deque
|
||||
from typing import List, Dict, Optional, Set, Iterable, Union, Any
|
||||
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple
|
||||
import secrets
|
||||
import random
|
||||
|
||||
|
@ -38,7 +38,7 @@ class MultiWorld():
|
|||
self.players = players
|
||||
self.glitch_triforce = False
|
||||
self.algorithm = 'balanced'
|
||||
self.dungeons = []
|
||||
self.dungeons: Dict[Tuple[str, int], Dungeon] = {}
|
||||
self.regions = []
|
||||
self.shops = []
|
||||
self.itempool = []
|
||||
|
@ -95,10 +95,6 @@ class MultiWorld():
|
|||
set_player_attr('can_access_trock_big_chest', None)
|
||||
set_player_attr('can_access_trock_middle', None)
|
||||
set_player_attr('fix_fake_world', True)
|
||||
set_player_attr('mapshuffle', False)
|
||||
set_player_attr('compassshuffle', False)
|
||||
set_player_attr('keyshuffle', False)
|
||||
set_player_attr('bigkeyshuffle', False)
|
||||
set_player_attr('difficulty_requirements', None)
|
||||
set_player_attr('boss_shuffle', 'none')
|
||||
set_player_attr('enemy_shuffle', False)
|
||||
|
@ -118,7 +114,6 @@ class MultiWorld():
|
|||
set_player_attr('blue_clock_time', 2)
|
||||
set_player_attr('green_clock_time', 4)
|
||||
set_player_attr('can_take_damage', True)
|
||||
set_player_attr('glitch_boots', True)
|
||||
set_player_attr('progression_balancing', True)
|
||||
set_player_attr('local_items', set())
|
||||
set_player_attr('non_local_items', set())
|
||||
|
@ -158,6 +153,10 @@ class MultiWorld():
|
|||
def get_game_players(self, game_name: str):
|
||||
return tuple(player for player in self.player_ids if self.game[player] == game_name)
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_game_worlds(self, game_name: str):
|
||||
return tuple(world for player, world in self.worlds.items() if self.game[player] == game_name)
|
||||
|
||||
def get_name_string_for_object(self, obj) -> str:
|
||||
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
|
||||
|
||||
|
@ -209,10 +208,10 @@ class MultiWorld():
|
|||
return self._location_cache[location, player]
|
||||
|
||||
def get_dungeon(self, dungeonname: str, player: int) -> Dungeon:
|
||||
for dungeon in self.dungeons:
|
||||
if dungeon.name == dungeonname and dungeon.player == player:
|
||||
return dungeon
|
||||
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player))
|
||||
try:
|
||||
return self.dungeons[dungeonname, player]
|
||||
except KeyError as e:
|
||||
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player)) from e
|
||||
|
||||
def get_all_state(self, keys=False) -> CollectionState:
|
||||
key = f"_all_state_{keys}"
|
||||
|
@ -537,9 +536,7 @@ class CollectionState(object):
|
|||
locations = {location for location in locations if location.event}
|
||||
while new_locations:
|
||||
reachable_events = {location for location in locations if
|
||||
(not key_only or
|
||||
(not self.world.keyshuffle[location.item.player] and location.item.smallkey)
|
||||
or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
|
||||
(not key_only or getattr(location.item, "locked_dungeon_item", False))
|
||||
and location.can_reach(self)}
|
||||
new_locations = reachable_events - self.events
|
||||
for event in new_locations:
|
||||
|
@ -569,13 +566,6 @@ class CollectionState(object):
|
|||
found += self.prog_items[item_name, player]
|
||||
return found
|
||||
|
||||
def has_key(self, item, player, count: int = 1):
|
||||
if self.world.logic[player] == 'nologic':
|
||||
return True
|
||||
if self.world.keyshuffle[player] == "universal":
|
||||
return self.can_buy_unlimited('Small Key (Universal)', player)
|
||||
return self.prog_items[item, player] >= count
|
||||
|
||||
def can_buy_unlimited(self, item: str, player: int) -> bool:
|
||||
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for
|
||||
shop in self.world.shops)
|
||||
|
@ -807,13 +797,6 @@ class Region(object):
|
|||
return True
|
||||
return False
|
||||
|
||||
def can_fill(self, item: Item):
|
||||
inside_dungeon_item = item.locked_dungeon_item
|
||||
if inside_dungeon_item:
|
||||
return self.dungeon.is_dungeon_item(item) and item.player == self.player
|
||||
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
@ -855,8 +838,8 @@ class Entrance(object):
|
|||
world = self.parent_region.world if self.parent_region else None
|
||||
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
||||
|
||||
class Dungeon(object):
|
||||
|
||||
class Dungeon(object):
|
||||
def __init__(self, name: str, regions, big_key, small_keys, dungeon_items, player: int):
|
||||
self.name = name
|
||||
self.regions = regions
|
||||
|
@ -911,7 +894,8 @@ class Boss():
|
|||
return f"Boss({self.name})"
|
||||
|
||||
class Location():
|
||||
shop_slot: bool = False
|
||||
# If given as integer, then this is the shop's inventory index
|
||||
shop_slot: Optional[int] = None
|
||||
shop_slot_disabled: bool = False
|
||||
event: bool = False
|
||||
locked: bool = False
|
||||
|
@ -930,7 +914,7 @@ class Location():
|
|||
self.item: Optional[Item] = None
|
||||
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
||||
return self.always_allow(state, item) or (self.parent_region.can_fill(item) and self.item_rule(item) and (not check_access or self.can_reach(state)))
|
||||
return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state)))
|
||||
|
||||
def can_reach(self, state: CollectionState) -> bool:
|
||||
# self.access_rule computes faster on average, so placing it first for faster abort
|
||||
|
@ -966,21 +950,33 @@ class Location():
|
|||
|
||||
@property
|
||||
def hint_text(self):
|
||||
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
|
||||
hint_text = getattr(self, "_hint_text", None)
|
||||
if hint_text:
|
||||
return hint_text
|
||||
return "at " + self.name.replace("_", " ").replace("-", " ")
|
||||
|
||||
|
||||
class Item():
|
||||
location: Optional[Location] = None
|
||||
world: Optional[MultiWorld] = None
|
||||
code: Optional[str] = None # an item with ID None is called an Event, and does not get written to multidata
|
||||
game: str = "Generic"
|
||||
type: str = None
|
||||
never_exclude = False # change manually to ensure that a specific nonprogression item never goes on an excluded location
|
||||
# change manually to ensure that a specific non-progression item never goes on an excluded location
|
||||
never_exclude = False
|
||||
|
||||
# need to find a decent place for these to live and to allow other games to register texts if they want.
|
||||
pedestal_credit_text: str = "and the Unknown Item"
|
||||
sickkid_credit_text: Optional[str] = None
|
||||
magicshop_credit_text: Optional[str] = None
|
||||
zora_credit_text: Optional[str] = None
|
||||
fluteboy_credit_text: Optional[str] = None
|
||||
code: Optional[str] = None # an item with ID None is called an Event, and does not get written to multidata
|
||||
|
||||
# hopefully temporary attributes to satisfy legacy LttP code, proper implementation in subclass ALttPItem
|
||||
smallkey: bool = False
|
||||
bigkey: bool = False
|
||||
map: bool = False
|
||||
compass: bool = False
|
||||
|
||||
def __init__(self, name: str, advancement: bool, code: Optional[int], player: int):
|
||||
self.name = name
|
||||
|
@ -1007,51 +1003,6 @@ class Item():
|
|||
def __hash__(self):
|
||||
return hash((self.name, self.player))
|
||||
|
||||
@property
|
||||
def crystal(self) -> bool:
|
||||
return self.type == 'Crystal'
|
||||
|
||||
@property
|
||||
def smallkey(self) -> bool:
|
||||
return self.type == 'SmallKey'
|
||||
|
||||
@property
|
||||
def bigkey(self) -> bool:
|
||||
return self.type == 'BigKey'
|
||||
|
||||
@property
|
||||
def map(self) -> bool:
|
||||
return self.type == 'Map'
|
||||
|
||||
@property
|
||||
def compass(self) -> bool:
|
||||
return self.type == 'Compass'
|
||||
|
||||
@property
|
||||
def dungeon_item(self) -> Optional[str]:
|
||||
if self.game == "A Link to the Past" and self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
|
||||
return self.type
|
||||
|
||||
@property
|
||||
def shuffled_dungeon_item(self) -> bool:
|
||||
dungeon_item_type = self.dungeon_item
|
||||
if dungeon_item_type:
|
||||
return {"SmallKey" : self.world.keyshuffle,
|
||||
"BigKey": self.world.bigkeyshuffle,
|
||||
"Map": self.world.mapshuffle,
|
||||
"Compass": self.world.compassshuffle}[dungeon_item_type][self.player]
|
||||
return False
|
||||
|
||||
@property
|
||||
def locked_dungeon_item(self) -> bool:
|
||||
dungeon_item_type = self.dungeon_item
|
||||
if dungeon_item_type:
|
||||
return not {"SmallKey" : self.world.keyshuffle,
|
||||
"BigKey": self.world.bigkeyshuffle,
|
||||
"Map": self.world.mapshuffle,
|
||||
"Compass": self.world.compassshuffle}[dungeon_item_type][self.player]
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
@ -1105,7 +1056,7 @@ class Spoiler():
|
|||
self.locations['Caves'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in cave_locations])
|
||||
listed_locations.update(cave_locations)
|
||||
|
||||
for dungeon in self.world.dungeons:
|
||||
for dungeon in self.world.dungeons.values():
|
||||
dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon]
|
||||
self.locations[str(dungeon)] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dungeon_locations])
|
||||
listed_locations.update(dungeon_locations)
|
||||
|
@ -1179,10 +1130,6 @@ class Spoiler():
|
|||
'open_pyramid': self.world.open_pyramid,
|
||||
'accessibility': self.world.accessibility,
|
||||
'hints': self.world.hints,
|
||||
'mapshuffle': self.world.mapshuffle,
|
||||
'compassshuffle': self.world.compassshuffle,
|
||||
'keyshuffle': self.world.keyshuffle,
|
||||
'bigkeyshuffle': self.world.bigkeyshuffle,
|
||||
'boss_shuffle': self.world.boss_shuffle,
|
||||
'enemy_shuffle': self.world.enemy_shuffle,
|
||||
'enemy_health': self.world.enemy_health,
|
||||
|
@ -1277,15 +1224,6 @@ class Spoiler():
|
|||
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
|
||||
outfile.write('Pyramid hole pre-opened: %s\n' % (
|
||||
'Yes' if self.metadata['open_pyramid'][player] else 'No'))
|
||||
|
||||
outfile.write('Map shuffle: %s\n' %
|
||||
('Yes' if self.metadata['mapshuffle'][player] else 'No'))
|
||||
outfile.write('Compass shuffle: %s\n' %
|
||||
('Yes' if self.metadata['compassshuffle'][player] else 'No'))
|
||||
outfile.write(
|
||||
'Small Key shuffle: %s\n' % (bool_to_text(self.metadata['keyshuffle'][player])))
|
||||
outfile.write('Big Key shuffle: %s\n' % (
|
||||
'Yes' if self.metadata['bigkeyshuffle'][player] else 'No'))
|
||||
outfile.write('Shop inventory shuffle: %s\n' %
|
||||
bool_to_text("i" in self.metadata["shop_shuffle"][player]))
|
||||
outfile.write('Shop price shuffle: %s\n' %
|
||||
|
|
|
@ -19,15 +19,17 @@ from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePar
|
|||
|
||||
from worlds.factorio import Factorio
|
||||
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
log_folder = Utils.local_path("logs")
|
||||
|
||||
os.makedirs(log_folder, exist_ok=True)
|
||||
|
||||
|
||||
if gui_enabled:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
|
||||
filename=os.path.join("logs", "FactorioClient.txt"), filemode="w", force=True)
|
||||
filename=os.path.join(log_folder, "FactorioClient.txt"), filemode="w", force=True)
|
||||
else:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w"))
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join(log_folder, "FactorioClient.txt"), "w"))
|
||||
|
||||
|
||||
class FactorioCommandProcessor(ClientCommandProcessor):
|
||||
|
@ -110,7 +112,7 @@ class FactorioContext(CommonContext):
|
|||
if cmd == "Connected":
|
||||
# catch up sync anything that is already cleared.
|
||||
if args["checked_locations"]:
|
||||
self.rcon_client.send_commands({item_name: f'/ap-get-technology {item_name}\t-1' for
|
||||
self.rcon_client.send_commands({item_name: f'/ap-get-technology ap-{item_name}-\t-1' for
|
||||
item_name in args["checked_locations"]})
|
||||
|
||||
|
||||
|
|
13
Fill.py
13
Fill.py
|
@ -81,6 +81,7 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
|
|||
progitempool = []
|
||||
nonexcludeditempool = []
|
||||
localrestitempool = {player: [] for player in range(1, world.players + 1)}
|
||||
nonlocalrestitempool = []
|
||||
restitempool = []
|
||||
|
||||
for item in world.itempool:
|
||||
|
@ -90,11 +91,13 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
|
|||
nonexcludeditempool.append(item)
|
||||
elif item.name in world.local_items[item.player]:
|
||||
localrestitempool[item.player].append(item)
|
||||
elif item.name in world.non_local_items[item.player]:
|
||||
nonlocalrestitempool.append(item)
|
||||
else:
|
||||
restitempool.append(item)
|
||||
|
||||
world.random.shuffle(fill_locations)
|
||||
call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, restitempool, fill_locations)
|
||||
call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations)
|
||||
|
||||
fill_restrictive(world, world.state, fill_locations, progitempool)
|
||||
|
||||
|
@ -120,6 +123,14 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
|
|||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
fill_locations.remove(spot_to_fill)
|
||||
|
||||
for item_to_place in nonlocalrestitempool:
|
||||
for i, location in enumerate(fill_locations):
|
||||
if location.player != item_to_place.player:
|
||||
world.push_item(fill_locations.pop(i), item_to_place, False)
|
||||
break
|
||||
else:
|
||||
logging.warning(f"Could not place non_local_item {item_to_place} among {fill_locations}, tossing.")
|
||||
|
||||
world.random.shuffle(fill_locations)
|
||||
|
||||
restitempool, fill_locations = fast_fill(world, restitempool, fill_locations)
|
||||
|
|
163
Generate.py
163
Generate.py
|
@ -120,7 +120,6 @@ def main(args=None, callback=ERmain):
|
|||
f"A mix is also permitted.")
|
||||
erargs = parse_arguments(['--multi', str(args.multi)])
|
||||
erargs.seed = seed
|
||||
erargs.name = {x: "" for x in range(1, args.multi + 1)} # only so it can be overwrittin in mystery
|
||||
erargs.create_spoiler = args.spoiler > 0
|
||||
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
|
||||
erargs.race = args.race
|
||||
|
@ -189,6 +188,9 @@ def main(args=None, callback=ERmain):
|
|||
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||
|
||||
if len(set(erargs.name.values())) != len(erargs.name):
|
||||
raise Exception(f"Names have to be unique. Names: {erargs.name}")
|
||||
|
||||
if args.yaml_output:
|
||||
import yaml
|
||||
important = {}
|
||||
|
@ -237,7 +239,7 @@ def convert_to_on_off(value):
|
|||
return {True: "on", False: "off"}.get(value, value)
|
||||
|
||||
|
||||
def get_choice(option, root, value=None) -> typing.Any:
|
||||
def get_choice_legacy(option, root, value=None) -> typing.Any:
|
||||
if option not in root:
|
||||
return value
|
||||
if type(root[option]) is list:
|
||||
|
@ -252,6 +254,20 @@ def get_choice(option, root, value=None) -> typing.Any:
|
|||
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
|
||||
|
||||
|
||||
def get_choice(option, root, value=None) -> typing.Any:
|
||||
if option not in root:
|
||||
return value
|
||||
if type(root[option]) is list:
|
||||
return random.choices(root[option])[0]
|
||||
if type(root[option]) is not dict:
|
||||
return root[option]
|
||||
if not root[option]:
|
||||
return value
|
||||
if any(root[option].values()):
|
||||
return random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0]
|
||||
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
|
||||
|
||||
|
||||
class SafeDict(dict):
|
||||
def __missing__(self, key):
|
||||
return '{' + key + '}'
|
||||
|
@ -465,7 +481,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
|||
inventoryweights = game_weights.get('start_inventory', {})
|
||||
startitems = []
|
||||
for item in inventoryweights.keys():
|
||||
itemvalue = get_choice(item, inventoryweights)
|
||||
itemvalue = get_choice_legacy(item, inventoryweights)
|
||||
if isinstance(itemvalue, int):
|
||||
for i in range(int(itemvalue)):
|
||||
startitems.append(item)
|
||||
|
@ -513,7 +529,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
|||
|
||||
|
||||
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
glitches_required = get_choice('glitches_required', weights)
|
||||
glitches_required = get_choice_legacy('glitches_required', weights)
|
||||
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
|
||||
logging.warning("Only NMG, OWG, HMG and No Logic supported")
|
||||
glitches_required = 'none'
|
||||
|
@ -521,7 +537,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||
'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[
|
||||
glitches_required]
|
||||
|
||||
ret.dark_room_logic = get_choice("dark_room_logic", weights, "lamp")
|
||||
ret.dark_room_logic = get_choice_legacy("dark_room_logic", weights, "lamp")
|
||||
if not ret.dark_room_logic: # None/False
|
||||
ret.dark_room_logic = "none"
|
||||
if ret.dark_room_logic == "sconces":
|
||||
|
@ -529,94 +545,78 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||
if ret.dark_room_logic not in {"lamp", "torches", "none"}:
|
||||
raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"")
|
||||
|
||||
ret.restrict_dungeon_item_on_boss = get_choice('restrict_dungeon_item_on_boss', weights, False)
|
||||
ret.restrict_dungeon_item_on_boss = get_choice_legacy('restrict_dungeon_item_on_boss', weights, False)
|
||||
|
||||
dungeon_items = get_choice('dungeon_items', weights)
|
||||
if dungeon_items == 'full' or dungeon_items == True:
|
||||
dungeon_items = 'mcsb'
|
||||
elif dungeon_items == 'standard':
|
||||
dungeon_items = ""
|
||||
elif not dungeon_items:
|
||||
dungeon_items = ""
|
||||
if "u" in dungeon_items:
|
||||
dungeon_items.replace("s", "")
|
||||
|
||||
ret.mapshuffle = get_choice('map_shuffle', weights, 'm' in dungeon_items)
|
||||
ret.compassshuffle = get_choice('compass_shuffle', weights, 'c' in dungeon_items)
|
||||
ret.keyshuffle = get_choice('smallkey_shuffle', weights,
|
||||
'universal' if 'u' in dungeon_items else 's' in dungeon_items)
|
||||
ret.bigkeyshuffle = get_choice('bigkey_shuffle', weights, 'b' in dungeon_items)
|
||||
|
||||
entrance_shuffle = get_choice('entrance_shuffle', weights, 'vanilla')
|
||||
entrance_shuffle = get_choice_legacy('entrance_shuffle', weights, 'vanilla')
|
||||
if entrance_shuffle.startswith('none-'):
|
||||
ret.shuffle = 'vanilla'
|
||||
else:
|
||||
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
|
||||
|
||||
goal = get_choice('goals', weights, 'ganon')
|
||||
goal = get_choice_legacy('goals', weights, 'ganon')
|
||||
|
||||
ret.goal = goals[goal]
|
||||
|
||||
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
|
||||
# fast ganon + ganon at hole
|
||||
ret.open_pyramid = get_choice('open_pyramid', weights, 'goal')
|
||||
ret.open_pyramid = get_choice_legacy('open_pyramid', weights, 'goal')
|
||||
|
||||
extra_pieces = get_choice('triforce_pieces_mode', weights, 'available')
|
||||
extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available')
|
||||
|
||||
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice('triforce_pieces_required', weights, 20))
|
||||
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice_legacy('triforce_pieces_required', weights, 20))
|
||||
|
||||
# sum a percentage to required
|
||||
if extra_pieces == 'percentage':
|
||||
percentage = max(100, float(get_choice('triforce_pieces_percentage', weights, 150))) / 100
|
||||
percentage = max(100, float(get_choice_legacy('triforce_pieces_percentage', weights, 150))) / 100
|
||||
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
|
||||
# vanilla mode (specify how many pieces are)
|
||||
elif extra_pieces == 'available':
|
||||
ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any(
|
||||
get_choice('triforce_pieces_available', weights, 30))
|
||||
get_choice_legacy('triforce_pieces_available', weights, 30))
|
||||
# required pieces + fixed extra
|
||||
elif extra_pieces == 'extra':
|
||||
extra_pieces = max(0, int(get_choice('triforce_pieces_extra', weights, 10)))
|
||||
extra_pieces = max(0, int(get_choice_legacy('triforce_pieces_extra', weights, 10)))
|
||||
ret.triforce_pieces_available = ret.triforce_pieces_required + extra_pieces
|
||||
|
||||
# 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 = get_choice('shop_shuffle', weights, '')
|
||||
ret.shop_shuffle = get_choice_legacy('shop_shuffle', weights, '')
|
||||
if not ret.shop_shuffle:
|
||||
ret.shop_shuffle = ''
|
||||
|
||||
ret.mode = get_choice("mode", weights)
|
||||
ret.retro = get_choice("retro", weights)
|
||||
ret.mode = get_choice_legacy("mode", weights)
|
||||
ret.retro = get_choice_legacy("retro", weights)
|
||||
|
||||
ret.hints = get_choice('hints', weights)
|
||||
ret.hints = get_choice_legacy('hints', weights)
|
||||
|
||||
ret.swordless = get_choice('swordless', weights, False)
|
||||
ret.swordless = get_choice_legacy('swordless', weights, False)
|
||||
|
||||
ret.difficulty = get_choice('item_pool', weights)
|
||||
ret.difficulty = get_choice_legacy('item_pool', weights)
|
||||
|
||||
ret.item_functionality = get_choice('item_functionality', weights)
|
||||
ret.item_functionality = get_choice_legacy('item_functionality', weights)
|
||||
|
||||
boss_shuffle = get_choice('boss_shuffle', weights)
|
||||
boss_shuffle = get_choice_legacy('boss_shuffle', weights)
|
||||
ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options)
|
||||
|
||||
ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False))
|
||||
ret.enemy_shuffle = bool(get_choice_legacy('enemy_shuffle', weights, False))
|
||||
|
||||
ret.killable_thieves = get_choice('killable_thieves', weights, False)
|
||||
ret.tile_shuffle = get_choice('tile_shuffle', weights, False)
|
||||
ret.bush_shuffle = get_choice('bush_shuffle', weights, False)
|
||||
ret.killable_thieves = get_choice_legacy('killable_thieves', weights, False)
|
||||
ret.tile_shuffle = get_choice_legacy('tile_shuffle', weights, False)
|
||||
ret.bush_shuffle = get_choice_legacy('bush_shuffle', weights, False)
|
||||
|
||||
ret.enemy_damage = {None: 'default',
|
||||
'default': 'default',
|
||||
'shuffled': 'shuffled',
|
||||
'random': 'chaos', # to be removed
|
||||
'chaos': 'chaos',
|
||||
}[get_choice('enemy_damage', weights)]
|
||||
}[get_choice_legacy('enemy_damage', weights)]
|
||||
|
||||
ret.enemy_health = get_choice('enemy_health', weights)
|
||||
ret.enemy_health = get_choice_legacy('enemy_health', weights)
|
||||
|
||||
ret.shufflepots = get_choice('pot_shuffle', weights)
|
||||
ret.shufflepots = get_choice_legacy('pot_shuffle', weights)
|
||||
|
||||
ret.beemizer = int(get_choice('beemizer', weights, 0))
|
||||
ret.beemizer = int(get_choice_legacy('beemizer', weights, 0))
|
||||
|
||||
ret.timer = {'none': False,
|
||||
None: False,
|
||||
|
@ -625,19 +625,19 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||
'timed_ohko': 'timed-ohko',
|
||||
'ohko': 'ohko',
|
||||
'timed_countdown': 'timed-countdown',
|
||||
'display': 'display'}[get_choice('timer', weights, False)]
|
||||
'display': 'display'}[get_choice_legacy('timer', weights, False)]
|
||||
|
||||
ret.countdown_start_time = int(get_choice('countdown_start_time', weights, 10))
|
||||
ret.red_clock_time = int(get_choice('red_clock_time', weights, -2))
|
||||
ret.blue_clock_time = int(get_choice('blue_clock_time', weights, 2))
|
||||
ret.green_clock_time = int(get_choice('green_clock_time', weights, 4))
|
||||
ret.countdown_start_time = int(get_choice_legacy('countdown_start_time', weights, 10))
|
||||
ret.red_clock_time = int(get_choice_legacy('red_clock_time', weights, -2))
|
||||
ret.blue_clock_time = int(get_choice_legacy('blue_clock_time', weights, 2))
|
||||
ret.green_clock_time = int(get_choice_legacy('green_clock_time', weights, 4))
|
||||
|
||||
ret.dungeon_counters = get_choice('dungeon_counters', weights, 'default')
|
||||
ret.dungeon_counters = get_choice_legacy('dungeon_counters', weights, 'default')
|
||||
|
||||
ret.shuffle_prizes = get_choice('shuffle_prizes', weights, "g")
|
||||
ret.shuffle_prizes = get_choice_legacy('shuffle_prizes', weights, "g")
|
||||
|
||||
ret.required_medallions = [get_choice("misery_mire_medallion", weights, "random"),
|
||||
get_choice("turtle_rock_medallion", weights, "random")]
|
||||
ret.required_medallions = [get_choice_legacy("misery_mire_medallion", weights, "random"),
|
||||
get_choice_legacy("turtle_rock_medallion", weights, "random")]
|
||||
|
||||
for index, medallion in enumerate(ret.required_medallions):
|
||||
ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \
|
||||
|
@ -645,13 +645,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||
if not ret.required_medallions[index]:
|
||||
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
|
||||
|
||||
ret.glitch_boots = get_choice('glitch_boots', weights, True)
|
||||
|
||||
if get_choice("local_keys", weights, "l" in dungeon_items):
|
||||
# () important for ordering of commands, without them the Big Keys section is part of the Small Key else
|
||||
ret.local_items |= item_name_groups["Small Keys"] if ret.keyshuffle else set()
|
||||
ret.local_items |= item_name_groups["Big Keys"] if ret.bigkeyshuffle else set()
|
||||
|
||||
ret.plando_items = []
|
||||
if "items" in plando_options:
|
||||
|
||||
|
@ -665,10 +658,10 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||
|
||||
options = weights.get("plando_items", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
from_pool = get_choice("from_pool", placement, PlandoItem._field_defaults["from_pool"])
|
||||
location_world = get_choice("world", placement, PlandoItem._field_defaults["world"])
|
||||
force = str(get_choice("force", placement, PlandoItem._field_defaults["force"])).lower()
|
||||
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
|
||||
from_pool = get_choice_legacy("from_pool", placement, PlandoItem._field_defaults["from_pool"])
|
||||
location_world = get_choice_legacy("world", placement, PlandoItem._field_defaults["world"])
|
||||
force = str(get_choice_legacy("force", placement, PlandoItem._field_defaults["force"])).lower()
|
||||
if "items" in placement and "locations" in placement:
|
||||
items = placement["items"]
|
||||
locations = placement["locations"]
|
||||
|
@ -684,8 +677,8 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||
for item, location in zip(items, locations):
|
||||
add_plando_item(item, location)
|
||||
else:
|
||||
item = get_choice("item", placement, get_choice("items", placement))
|
||||
location = get_choice("location", placement)
|
||||
item = get_choice_legacy("item", placement, get_choice_legacy("items", placement))
|
||||
location = get_choice_legacy("location", placement)
|
||||
add_plando_item(item, location)
|
||||
|
||||
ret.plando_texts = {}
|
||||
|
@ -694,39 +687,39 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
|||
tt.removeUnwantedText()
|
||||
options = weights.get("plando_texts", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
at = str(get_choice("at", placement))
|
||||
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
|
||||
at = str(get_choice_legacy("at", placement))
|
||||
if at not in tt:
|
||||
raise Exception(f"No text target \"{at}\" found.")
|
||||
ret.plando_texts[at] = str(get_choice("text", placement))
|
||||
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
|
||||
|
||||
ret.plando_connections = []
|
||||
if "connections" in plando_options:
|
||||
options = weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
|
||||
ret.plando_connections.append(PlandoConnection(
|
||||
get_choice("entrance", placement),
|
||||
get_choice("exit", placement),
|
||||
get_choice("direction", placement, "both")
|
||||
get_choice_legacy("entrance", placement),
|
||||
get_choice_legacy("exit", placement),
|
||||
get_choice_legacy("direction", placement, "both")
|
||||
))
|
||||
|
||||
ret.sprite_pool = weights.get('sprite_pool', [])
|
||||
ret.sprite = get_choice('sprite', weights, "Link")
|
||||
ret.sprite = get_choice_legacy('sprite', weights, "Link")
|
||||
if 'random_sprite_on_event' in weights:
|
||||
randomoneventweights = weights['random_sprite_on_event']
|
||||
if get_choice('enabled', randomoneventweights, False):
|
||||
if get_choice_legacy('enabled', randomoneventweights, False):
|
||||
ret.sprite = 'randomon'
|
||||
ret.sprite += '-hit' if get_choice('on_hit', randomoneventweights, True) else ''
|
||||
ret.sprite += '-enter' if get_choice('on_enter', randomoneventweights, False) else ''
|
||||
ret.sprite += '-exit' if get_choice('on_exit', randomoneventweights, False) else ''
|
||||
ret.sprite += '-slash' if get_choice('on_slash', randomoneventweights, False) else ''
|
||||
ret.sprite += '-item' if get_choice('on_item', randomoneventweights, False) else ''
|
||||
ret.sprite += '-bonk' if get_choice('on_bonk', randomoneventweights, False) else ''
|
||||
ret.sprite = 'randomonall' if get_choice('on_everything', randomoneventweights, False) else ret.sprite
|
||||
ret.sprite += '-hit' if get_choice_legacy('on_hit', randomoneventweights, True) else ''
|
||||
ret.sprite += '-enter' if get_choice_legacy('on_enter', randomoneventweights, False) else ''
|
||||
ret.sprite += '-exit' if get_choice_legacy('on_exit', randomoneventweights, False) else ''
|
||||
ret.sprite += '-slash' if get_choice_legacy('on_slash', randomoneventweights, False) else ''
|
||||
ret.sprite += '-item' if get_choice_legacy('on_item', randomoneventweights, False) else ''
|
||||
ret.sprite += '-bonk' if get_choice_legacy('on_bonk', randomoneventweights, False) else ''
|
||||
ret.sprite = 'randomonall' if get_choice_legacy('on_everything', randomoneventweights, False) else ret.sprite
|
||||
ret.sprite = 'randomonnone' if ret.sprite == 'randomon' else ret.sprite
|
||||
|
||||
if (not ret.sprite_pool or get_choice('use_weighted_sprite_pool', randomoneventweights, False)) \
|
||||
if (not ret.sprite_pool or get_choice_legacy('use_weighted_sprite_pool', randomoneventweights, False)) \
|
||||
and 'sprite' in weights: # Use sprite as a weighted sprite pool, if a sprite pool is not already defined.
|
||||
for key, value in weights['sprite'].items():
|
||||
if key.startswith('random'):
|
||||
|
|
|
@ -8,6 +8,8 @@ import os
|
|||
import subprocess
|
||||
import base64
|
||||
import shutil
|
||||
import logging
|
||||
import asyncio
|
||||
from json import loads, dumps
|
||||
|
||||
from Utils import get_item_name_from_id
|
||||
|
@ -30,15 +32,16 @@ snes_logger = logging.getLogger("SNES")
|
|||
|
||||
from MultiServer import mark_raw
|
||||
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
log_folder = Utils.local_path("logs")
|
||||
os.makedirs(log_folder, exist_ok=True)
|
||||
|
||||
# Log to file in gui case
|
||||
if gui_enabled:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
|
||||
filename=os.path.join("logs", "LttPClient.txt"), filemode="w", force=True)
|
||||
filename=os.path.join(log_folder, "LttPClient.txt"), filemode="w", force=True)
|
||||
else:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "LttPClient.txt"), "w"))
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join(log_folder, "LttPClient.txt"), "w"))
|
||||
|
||||
|
||||
class LttPCommandProcessor(ClientCommandProcessor):
|
||||
|
@ -52,11 +55,26 @@ class LttPCommandProcessor(ClientCommandProcessor):
|
|||
self.output(f"Setting slow mode to {self.ctx.slow_mode}")
|
||||
|
||||
@mark_raw
|
||||
def _cmd_snes(self, snes_address: str = "") -> bool:
|
||||
"""Connect to a snes.
|
||||
Optionally include network address of a snes to connect to, otherwise show available devices"""
|
||||
def _cmd_snes(self, snes_options: str = "") -> bool:
|
||||
"""Connect to a snes. Optionally include network address of a snes to connect to, otherwise show available devices; and a SNES device number if more than one SNES is detected"""
|
||||
|
||||
snes_address = self.ctx.snes_address
|
||||
snes_device_number = -1
|
||||
|
||||
options = snes_options.split()
|
||||
num_options = len(options)
|
||||
|
||||
if num_options > 0:
|
||||
snes_address = options[0]
|
||||
|
||||
if num_options > 1:
|
||||
try:
|
||||
snes_device_number = int(options[1])
|
||||
except:
|
||||
pass
|
||||
|
||||
self.ctx.snes_reconnect_address = None
|
||||
asyncio.create_task(snes_connect(self.ctx, snes_address if snes_address else self.ctx.snes_address))
|
||||
asyncio.create_task(snes_connect(self.ctx, snes_address, snes_device_number))
|
||||
return True
|
||||
|
||||
def _cmd_snes_close(self) -> bool:
|
||||
|
@ -495,7 +513,7 @@ async def get_snes_devices(ctx: Context):
|
|||
return devices
|
||||
|
||||
|
||||
async def snes_connect(ctx: Context, address):
|
||||
async def snes_connect(ctx: Context, address, deviceIndex = -1):
|
||||
global SNES_RECONNECT_DELAY
|
||||
if ctx.snes_socket is not None and ctx.snes_state == SNESState.SNES_CONNECTED:
|
||||
if ctx.rom:
|
||||
|
@ -504,6 +522,7 @@ async def snes_connect(ctx: Context, address):
|
|||
snes_logger.error('Already connected to SNI, likely awaiting a device.')
|
||||
return
|
||||
|
||||
device = None
|
||||
recv_task = None
|
||||
ctx.snes_state = SNESState.SNES_CONNECTING
|
||||
socket = await _snes_connect(ctx, address)
|
||||
|
@ -512,15 +531,29 @@ async def snes_connect(ctx: Context, address):
|
|||
|
||||
try:
|
||||
devices = await get_snes_devices(ctx)
|
||||
numDevices = len(devices)
|
||||
|
||||
if len(devices) == 1:
|
||||
if numDevices == 1:
|
||||
device = devices[0]
|
||||
elif ctx.snes_reconnect_address:
|
||||
if ctx.snes_attached_device[1] in devices:
|
||||
device = ctx.snes_attached_device[1]
|
||||
else:
|
||||
device = devices[ctx.snes_attached_device[0]]
|
||||
else:
|
||||
elif numDevices > 1:
|
||||
if deviceIndex == -1:
|
||||
snes_logger.info("Found " + str(numDevices) + " SNES devices; connect to one with /snes <address> <device number>:")
|
||||
|
||||
for idx, availableDevice in enumerate(devices):
|
||||
snes_logger.info(str(idx + 1) + ": " + availableDevice)
|
||||
|
||||
elif (deviceIndex < 0) or (deviceIndex - 1) > numDevices:
|
||||
snes_logger.warning("SNES device number out of range")
|
||||
|
||||
else:
|
||||
device = devices[deviceIndex - 1]
|
||||
|
||||
if device is None:
|
||||
await snes_disconnect(ctx)
|
||||
return
|
||||
|
||||
|
|
136
Main.py
136
Main.py
|
@ -14,8 +14,7 @@ from BaseClasses import MultiWorld, CollectionState, Region, RegionType
|
|||
from worlds.alttp.Items import item_name_groups
|
||||
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
|
||||
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
|
||||
from worlds.alttp.Shops import ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
||||
from worlds.alttp.ItemPool import difficulties
|
||||
from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
||||
from Utils import output_path, get_options, __version__, version_tuple
|
||||
from worlds.generic.Rules import locality_rules, exclusion_rules
|
||||
from worlds import AutoWorld
|
||||
|
@ -30,15 +29,6 @@ def get_seed(seed=None):
|
|||
return seed
|
||||
|
||||
|
||||
def get_same_seed(world: MultiWorld, seed_def: tuple) -> str:
|
||||
seeds: Dict[tuple, str] = getattr(world, "__named_seeds", {})
|
||||
if seed_def in seeds:
|
||||
return seeds[seed_def]
|
||||
seeds[seed_def] = str(world.random.randint(0, 2 ** 64))
|
||||
world.__named_seeds = seeds
|
||||
return seeds[seed_def]
|
||||
|
||||
|
||||
def main(args, seed=None):
|
||||
if args.outputpath:
|
||||
os.makedirs(args.outputpath, exist_ok=True)
|
||||
|
@ -76,11 +66,6 @@ def main(args, seed=None):
|
|||
world.retro = args.retro.copy()
|
||||
|
||||
world.hints = args.hints.copy()
|
||||
|
||||
world.mapshuffle = args.mapshuffle.copy()
|
||||
world.compassshuffle = args.compassshuffle.copy()
|
||||
world.keyshuffle = args.keyshuffle.copy()
|
||||
world.bigkeyshuffle = args.bigkeyshuffle.copy()
|
||||
world.open_pyramid = args.open_pyramid.copy()
|
||||
world.boss_shuffle = args.shufflebosses.copy()
|
||||
world.enemy_shuffle = args.enemy_shuffle.copy()
|
||||
|
@ -97,7 +82,6 @@ def main(args, seed=None):
|
|||
world.green_clock_time = args.green_clock_time.copy()
|
||||
world.shufflepots = args.shufflepots.copy()
|
||||
world.dungeon_counters = args.dungeon_counters.copy()
|
||||
world.glitch_boots = args.glitch_boots.copy()
|
||||
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()
|
||||
|
@ -122,40 +106,23 @@ def main(args, seed=None):
|
|||
world.slot_seeds = {player: random.Random(world.random.getrandbits(64)) for player in
|
||||
range(1, world.players + 1)}
|
||||
|
||||
AutoWorld.call_all(world, "generate_early")
|
||||
|
||||
# system for sharing ER layouts
|
||||
for player in world.get_game_players("A Link to the Past"):
|
||||
world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
|
||||
|
||||
if "-" in world.shuffle[player]:
|
||||
shuffle, seed = world.shuffle[player].split("-", 1)
|
||||
world.shuffle[player] = shuffle
|
||||
if shuffle == "vanilla":
|
||||
world.er_seeds[player] = "vanilla"
|
||||
elif seed.startswith("group-") or args.race:
|
||||
world.er_seeds[player] = get_same_seed(world, (
|
||||
shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
|
||||
else: # not a race or group seed, use set seed as is.
|
||||
world.er_seeds[player] = seed
|
||||
elif world.shuffle[player] == "vanilla":
|
||||
world.er_seeds[player] = "vanilla"
|
||||
|
||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
|
||||
|
||||
logger.info("Found World Types:")
|
||||
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
|
||||
numlength = 8
|
||||
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
|
||||
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | {len(cls.location_names):3} Locations")
|
||||
logger.info(f" Item IDs: {min(cls.item_id_to_name):{numlength}} - "
|
||||
f"{max(cls.item_id_to_name):{numlength}} | "
|
||||
f"Location IDs: {min(cls.location_id_to_name):{numlength}} - "
|
||||
f"{max(cls.location_id_to_name):{numlength}}")
|
||||
if not cls.hidden:
|
||||
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | "
|
||||
f"{len(cls.location_names):3} Locations")
|
||||
logger.info(f" Item IDs: {min(cls.item_id_to_name):{numlength}} - "
|
||||
f"{max(cls.item_id_to_name):{numlength}} | "
|
||||
f"Location IDs: {min(cls.location_id_to_name):{numlength}} - "
|
||||
f"{max(cls.location_id_to_name):{numlength}}")
|
||||
|
||||
AutoWorld.call_all(world, "generate_early")
|
||||
|
||||
logger.info('')
|
||||
for player in world.get_game_players("A Link to the Past"):
|
||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||
|
||||
for player in world.player_ids:
|
||||
for item_name in args.startinventory[player]:
|
||||
|
@ -167,21 +134,6 @@ def main(args, seed=None):
|
|||
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
|
||||
world.local_items[player].add('Triforce Piece')
|
||||
|
||||
# dungeon items can't be in non-local if the appropriate dungeon item shuffle setting is not set.
|
||||
if not world.mapshuffle[player]:
|
||||
world.non_local_items[player] -= item_name_groups['Maps']
|
||||
|
||||
if not world.compassshuffle[player]:
|
||||
world.non_local_items[player] -= item_name_groups['Compasses']
|
||||
|
||||
if not world.keyshuffle[player]:
|
||||
world.non_local_items[player] -= item_name_groups['Small Keys']
|
||||
# This could probably use a more elegant solution.
|
||||
elif world.keyshuffle[player] == True and world.mode[player] == "Standard":
|
||||
world.local_items[player].add("Small Key (Hyrule Castle)")
|
||||
if not world.bigkeyshuffle[player]:
|
||||
world.non_local_items[player] -= item_name_groups['Big Keys']
|
||||
|
||||
# Not possible to place pendants/crystals out side of boss prizes yet.
|
||||
world.non_local_items[player] -= item_name_groups['Pendants']
|
||||
world.non_local_items[player] -= item_name_groups['Crystals']
|
||||
|
@ -225,9 +177,7 @@ def main(args, seed=None):
|
|||
elif world.algorithm == 'balanced':
|
||||
distribute_items_restrictive(world)
|
||||
|
||||
logger.info("Filling Shop Slots")
|
||||
|
||||
ShopSlotFill(world)
|
||||
AutoWorld.call_all(world, 'post_fill')
|
||||
|
||||
if world.players > 1:
|
||||
balance_multiworld_progression(world)
|
||||
|
@ -276,20 +226,21 @@ def main(args, seed=None):
|
|||
for player in range(1, world.players + 1):
|
||||
checks_in_area[player]["Total"] = 0
|
||||
|
||||
for location in [loc for loc in world.get_filled_locations() if type(loc.address) is int]:
|
||||
main_entrance = get_entrance_to_region(location.parent_region)
|
||||
if location.game != "A Link to the Past":
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif location.parent_region.dungeon:
|
||||
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
|
||||
'Inverted Ganons Tower': 'Ganons Tower'} \
|
||||
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
||||
checks_in_area[location.player][dungeonname].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.DarkWorld:
|
||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
checks_in_area[location.player]["Total"] += 1
|
||||
for location in world.get_filled_locations():
|
||||
if type(location.address) is int:
|
||||
main_entrance = get_entrance_to_region(location.parent_region)
|
||||
if location.game != "A Link to the Past":
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif location.parent_region.dungeon:
|
||||
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
|
||||
'Inverted Ganons Tower': 'Ganons Tower'} \
|
||||
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
||||
checks_in_area[location.player][dungeonname].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.DarkWorld:
|
||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
checks_in_area[location.player]["Total"] += 1
|
||||
|
||||
oldmancaves = []
|
||||
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
|
||||
|
@ -338,6 +289,8 @@ def main(args, seed=None):
|
|||
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
|
||||
for location in world.get_filled_locations():
|
||||
if type(location.address) == int:
|
||||
# item code None should be event, location.address should then also be None
|
||||
assert location.item.code is not None
|
||||
locations_data[location.player][location.address] = location.item.code, location.item.player
|
||||
if location.player in sending_visible_players and location.item.player != location.player:
|
||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||
|
@ -377,29 +330,34 @@ def main(args, seed=None):
|
|||
f.write(bytes([1])) # version of format
|
||||
f.write(multidata)
|
||||
|
||||
|
||||
multidata_task = pool.submit(write_multidata)
|
||||
if not check_accessibility_task.result():
|
||||
if not world.can_beat_game():
|
||||
raise Exception("Game appears as unbeatable. Aborting.")
|
||||
else:
|
||||
logger.warning("Location Accessibility requirements not fulfilled.")
|
||||
|
||||
# retrieve exceptions via .result() if they occured.
|
||||
if multidata_task:
|
||||
multidata_task.result() # retrieve exception if one exists
|
||||
multidata_task.result()
|
||||
for future in output_file_futures:
|
||||
future.result()
|
||||
|
||||
pool.shutdown() # wait for all queued tasks to complete
|
||||
|
||||
if not args.skip_playthrough:
|
||||
logger.info('Calculating playthrough.')
|
||||
create_playthrough(world)
|
||||
|
||||
if args.create_spoiler:
|
||||
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
|
||||
for future in output_file_futures:
|
||||
future.result()
|
||||
|
||||
zipfilename = output_path(f"AP_{world.seed_name}.zip")
|
||||
logger.info(f'Creating final archive at {zipfilename}.')
|
||||
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
|
||||
compresslevel=9) as zf:
|
||||
for file in os.scandir(temp_dir):
|
||||
zf.write(os.path.join(temp_dir, file), arcname=file.name)
|
||||
zf.write(file.path, arcname=file.name)
|
||||
|
||||
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
|
||||
return world
|
||||
|
@ -415,7 +373,6 @@ def create_playthrough(world):
|
|||
sphere_candidates = set(prog_locations)
|
||||
logging.debug('Building up collection spheres.')
|
||||
while sphere_candidates:
|
||||
state.sweep_for_events(key_only=True)
|
||||
|
||||
# build up spheres of collection radius.
|
||||
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
||||
|
@ -520,14 +477,15 @@ def create_playthrough(world):
|
|||
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
|
||||
sphere if location.player == player})
|
||||
if player in world.get_game_players("A Link to the Past"):
|
||||
for path in dict(world.spoiler.paths).values():
|
||||
if any(exit_path == 'Pyramid Fairy' for (_, exit_path) in path):
|
||||
if world.mode[player] != 'inverted':
|
||||
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
|
||||
get_path(state,world.get_region('Big Bomb Shop', player))
|
||||
else:
|
||||
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
|
||||
get_path(state,world.get_region('Inverted Big Bomb Shop', player))
|
||||
# If Pyramid Fairy Entrance needs to be reached, also path to Big Bomb Shop
|
||||
# Maybe move the big bomb over to the Event system instead?
|
||||
if any(exit_path == 'Pyramid Fairy' for path in world.spoiler.paths.values() for (_, exit_path) in path):
|
||||
if world.mode[player] != 'inverted':
|
||||
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
|
||||
get_path(state, world.get_region('Big Bomb Shop', player))
|
||||
else:
|
||||
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
|
||||
get_path(state, world.get_region('Inverted Big Bomb Shop', player))
|
||||
|
||||
# we can finally output our playthrough
|
||||
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}
|
||||
|
|
183
MultiServer.py
183
MultiServer.py
|
@ -31,10 +31,11 @@ from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_
|
|||
import Utils
|
||||
from Utils import get_item_name_from_id, get_location_name_from_id, \
|
||||
version_tuple, restricted_loads, Version
|
||||
from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode, NetworkPlayer
|
||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer
|
||||
|
||||
colorama.init()
|
||||
|
||||
|
||||
class Client(Endpoint):
|
||||
version = Version(0, 0, 0)
|
||||
tags: typing.List[str] = []
|
||||
|
@ -50,9 +51,14 @@ class Client(Endpoint):
|
|||
self.messageprocessor = client_message_processor(ctx, self)
|
||||
self.ctx = weakref.ref(ctx)
|
||||
|
||||
|
||||
team_slot = typing.Tuple[int, int]
|
||||
|
||||
class Context(Node):
|
||||
|
||||
class Context:
|
||||
dumper = staticmethod(encode)
|
||||
loader = staticmethod(decode)
|
||||
|
||||
simple_options = {"hint_cost": int,
|
||||
"location_check_points": int,
|
||||
"server_password": str,
|
||||
|
@ -64,8 +70,10 @@ class Context(Node):
|
|||
|
||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", remaining_mode: str = "disabled",
|
||||
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2):
|
||||
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, log_network: bool = False):
|
||||
super(Context, self).__init__()
|
||||
self.log_network = log_network
|
||||
self.endpoints = []
|
||||
self.compatibility: int = compatibility
|
||||
self.shutdown_task = None
|
||||
self.data_filename = None
|
||||
|
@ -113,10 +121,70 @@ class Context(Node):
|
|||
self.seed_name = ""
|
||||
self.random = random.Random()
|
||||
|
||||
def get_hint_cost(self, slot):
|
||||
if self.hint_cost:
|
||||
return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
||||
return 0
|
||||
# General networking
|
||||
|
||||
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
|
||||
if not endpoint.socket or not endpoint.socket.open:
|
||||
return False
|
||||
msg = self.dumper(msgs)
|
||||
try:
|
||||
await endpoint.socket.send(msg)
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception(f"Exception during send_msgs, could not send {msg}")
|
||||
await self.disconnect(endpoint)
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
return True
|
||||
|
||||
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
|
||||
if not endpoint.socket or not endpoint.socket.open:
|
||||
return False
|
||||
try:
|
||||
await endpoint.socket.send(msg)
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception("Exception during send_encoded_msgs")
|
||||
await self.disconnect(endpoint)
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
return True
|
||||
|
||||
def broadcast_all(self, msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
for endpoint in self.endpoints:
|
||||
if endpoint.auth:
|
||||
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
|
||||
|
||||
def broadcast_team(self, team, msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
for client in self.endpoints:
|
||||
if client.auth and client.team == team:
|
||||
asyncio.create_task(self.send_encoded_msgs(client, msgs))
|
||||
|
||||
async def disconnect(self, endpoint):
|
||||
if endpoint in self.endpoints:
|
||||
self.endpoints.remove(endpoint)
|
||||
await on_client_disconnected(self, endpoint)
|
||||
|
||||
# text
|
||||
|
||||
def notify_all(self, text):
|
||||
logging.info("Notice (all): %s" % text)
|
||||
self.broadcast_all([{"cmd": "Print", "text": text}])
|
||||
|
||||
def notify_client(self, client: Client, text: str):
|
||||
if not client.auth:
|
||||
return
|
||||
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}]))
|
||||
|
||||
def notify_client_multiple(self, client: Client, texts: typing.List[str]):
|
||||
if not client.auth:
|
||||
return
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
|
||||
|
||||
# loading
|
||||
|
||||
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
|
||||
if multidatapath.lower().endswith(".zip"):
|
||||
|
@ -177,27 +245,7 @@ class Context(Node):
|
|||
server_options = decoded_obj.get("server_options", {})
|
||||
self._set_options(server_options)
|
||||
|
||||
def get_players_package(self):
|
||||
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
|
||||
|
||||
def _set_options(self, server_options: dict):
|
||||
for key, value in server_options.items():
|
||||
data_type = self.simple_options.get(key, None)
|
||||
if data_type is not None:
|
||||
if value not in {False, True, None}: # some can be boolean OR text, such as password
|
||||
try:
|
||||
value = data_type(value)
|
||||
except Exception as e:
|
||||
try:
|
||||
raise Exception(f"Could not set server option {key}, skipping.") from e
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
|
||||
setattr(self, key, value)
|
||||
elif key == "disable_item_cheat":
|
||||
self.item_cheat = not bool(value)
|
||||
else:
|
||||
logging.debug(f"Unrecognized server option {key}")
|
||||
# saving
|
||||
|
||||
def save(self, now=False) -> bool:
|
||||
if self.saving:
|
||||
|
@ -228,7 +276,7 @@ class Context(Node):
|
|||
import os
|
||||
name, ext = os.path.splitext(self.data_filename)
|
||||
self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago','.zip') \
|
||||
else self.data_filename + '_' + 'apsave'
|
||||
else self.data_filename + '_' + 'apsave'
|
||||
try:
|
||||
with open(self.save_filename, 'rb') as f:
|
||||
save_data = restricted_loads(zlib.decompress(f.read()))
|
||||
|
@ -256,13 +304,6 @@ class Context(Node):
|
|||
import atexit
|
||||
atexit.register(self._save, True) # make sure we save on exit too
|
||||
|
||||
def recheck_hints(self):
|
||||
for team, slot in self.hints:
|
||||
self.hints[team, slot] = {
|
||||
hint.re_check(self, team) for hint in
|
||||
self.hints[team, slot]
|
||||
}
|
||||
|
||||
def get_save(self) -> dict:
|
||||
self.recheck_hints()
|
||||
d = {
|
||||
|
@ -303,43 +344,48 @@ class Context(Node):
|
|||
logging.info(f'Loaded save file with {sum([len(p) for p in self.received_items.values()])} received items '
|
||||
f'for {len(self.received_items)} players')
|
||||
|
||||
# rest
|
||||
|
||||
def get_hint_cost(self, slot):
|
||||
if self.hint_cost:
|
||||
return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
||||
return 0
|
||||
|
||||
def recheck_hints(self):
|
||||
for team, slot in self.hints:
|
||||
self.hints[team, slot] = {
|
||||
hint.re_check(self, team) for hint in
|
||||
self.hints[team, slot]
|
||||
}
|
||||
|
||||
def get_players_package(self):
|
||||
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
|
||||
|
||||
def _set_options(self, server_options: dict):
|
||||
for key, value in server_options.items():
|
||||
data_type = self.simple_options.get(key, None)
|
||||
if data_type is not None:
|
||||
if value not in {False, True, None}: # some can be boolean OR text, such as password
|
||||
try:
|
||||
value = data_type(value)
|
||||
except Exception as e:
|
||||
try:
|
||||
raise Exception(f"Could not set server option {key}, skipping.") from e
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
|
||||
setattr(self, key, value)
|
||||
elif key == "disable_item_cheat":
|
||||
self.item_cheat = not bool(value)
|
||||
else:
|
||||
logging.debug(f"Unrecognized server option {key}")
|
||||
|
||||
def get_aliased_name(self, team: int, slot: int):
|
||||
if (team, slot) in self.name_aliases:
|
||||
return f"{self.name_aliases[team, slot]} ({self.player_names[team, slot]})"
|
||||
else:
|
||||
return self.player_names[team, slot]
|
||||
|
||||
def notify_all(self, text):
|
||||
logging.info("Notice (all): %s" % text)
|
||||
self.broadcast_all([{"cmd": "Print", "text": text}])
|
||||
|
||||
def notify_client(self, client: Client, text: str):
|
||||
if not client.auth:
|
||||
return
|
||||
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}]))
|
||||
|
||||
def notify_client_multiple(self, client: Client, texts: typing.List[str]):
|
||||
if not client.auth:
|
||||
return
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
|
||||
|
||||
def broadcast_team(self, team, msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
for client in self.endpoints:
|
||||
if client.auth and client.team == team:
|
||||
asyncio.create_task(self.send_encoded_msgs(client, msgs))
|
||||
|
||||
def broadcast_all(self, msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
for endpoint in self.endpoints:
|
||||
if endpoint.auth:
|
||||
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
|
||||
|
||||
async def disconnect(self, endpoint):
|
||||
await super(Context, self).disconnect(endpoint)
|
||||
await on_client_disconnected(self, endpoint)
|
||||
|
||||
|
||||
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint]):
|
||||
concerns = collections.defaultdict(list)
|
||||
|
@ -1431,8 +1477,7 @@ async def main(args: argparse.Namespace):
|
|||
|
||||
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
|
||||
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode,
|
||||
args.auto_shutdown, args.compatibility)
|
||||
ctx.log_network = args.log_network
|
||||
args.auto_shutdown, args.compatibility, args.log_network)
|
||||
data_filename = args.multidata
|
||||
|
||||
try:
|
||||
|
|
48
NetUtils.py
48
NetUtils.py
|
@ -1,6 +1,4 @@
|
|||
from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
import typing
|
||||
import enum
|
||||
from json import JSONEncoder, JSONDecoder
|
||||
|
@ -94,52 +92,6 @@ def _object_hook(o: typing.Any) -> typing.Any:
|
|||
decode = JSONDecoder(object_hook=_object_hook).decode
|
||||
|
||||
|
||||
class Node:
|
||||
endpoints: typing.List
|
||||
dumper = staticmethod(encode)
|
||||
loader = staticmethod(decode)
|
||||
|
||||
def __init__(self):
|
||||
self.endpoints = []
|
||||
super(Node, self).__init__()
|
||||
self.log_network = 0
|
||||
|
||||
def broadcast_all(self, msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
for endpoint in self.endpoints:
|
||||
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
|
||||
|
||||
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
|
||||
if not endpoint.socket or not endpoint.socket.open:
|
||||
return False
|
||||
msg = self.dumper(msgs)
|
||||
try:
|
||||
await endpoint.socket.send(msg)
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception(f"Exception during send_msgs, could not send {msg}")
|
||||
await self.disconnect(endpoint)
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
return True
|
||||
|
||||
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
|
||||
if not endpoint.socket or not endpoint.socket.open:
|
||||
return False
|
||||
try:
|
||||
await endpoint.socket.send(msg)
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception("Exception during send_encoded_msgs")
|
||||
await self.disconnect(endpoint)
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
return True
|
||||
|
||||
async def disconnect(self, endpoint):
|
||||
if endpoint in self.endpoints:
|
||||
self.endpoints.remove(endpoint)
|
||||
|
||||
|
||||
class Endpoint:
|
||||
socket: websockets.WebSocketServerProtocol
|
||||
|
|
37
Options.py
37
Options.py
|
@ -9,7 +9,7 @@ class AssembleOptions(type):
|
|||
name_lookup = attrs["name_lookup"] = {}
|
||||
# merge parent class options
|
||||
for base in bases:
|
||||
if hasattr(base, "options"):
|
||||
if getattr(base, "options", None):
|
||||
options.update(base.options)
|
||||
name_lookup.update(base.name_lookup)
|
||||
new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
|
||||
|
@ -147,6 +147,29 @@ class Choice(Option):
|
|||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, str):
|
||||
assert other in self.options
|
||||
return other == self.current_key
|
||||
elif isinstance(other, int):
|
||||
assert other in self.name_lookup
|
||||
return other == self.value
|
||||
elif isinstance(other, bool):
|
||||
return other == bool(self.value)
|
||||
else:
|
||||
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
def __ne__(self, other):
|
||||
if isinstance(other, str):
|
||||
assert other in self.options
|
||||
return other != self.current_key
|
||||
elif isinstance(other, int):
|
||||
assert other in self.name_lookup
|
||||
return other != self.value
|
||||
elif isinstance(other, bool):
|
||||
return other != bool(self.value)
|
||||
else:
|
||||
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
class Range(Option, int):
|
||||
range_start = 0
|
||||
|
@ -233,14 +256,14 @@ if __name__ == "__main__":
|
|||
|
||||
from worlds.alttp.Options import Logic
|
||||
import argparse
|
||||
mapshuffle = Toggle
|
||||
compassshuffle = Toggle
|
||||
map_shuffle = Toggle
|
||||
compass_shuffle = Toggle
|
||||
keyshuffle = Toggle
|
||||
bigkeyshuffle = Toggle
|
||||
bigkey_shuffle = Toggle
|
||||
hints = Toggle
|
||||
test = argparse.Namespace()
|
||||
test.logic = Logic.from_text("no_logic")
|
||||
test.mapshuffle = mapshuffle.from_text("ON")
|
||||
test.map_shuffle = map_shuffle.from_text("ON")
|
||||
test.hints = hints.from_text('OFF')
|
||||
try:
|
||||
test.logic = Logic.from_text("overworld_glitches_typo")
|
||||
|
@ -250,7 +273,7 @@ if __name__ == "__main__":
|
|||
test.logic_owg = Logic.from_text("owg")
|
||||
except KeyError as e:
|
||||
print(e)
|
||||
if test.mapshuffle:
|
||||
print("Mapshuffle is on")
|
||||
if test.map_shuffle:
|
||||
print("map_shuffle is on")
|
||||
print(f"Hints are {bool(test.hints)}")
|
||||
print(test)
|
||||
|
|
|
@ -7,8 +7,9 @@ Currently, the following games are supported:
|
|||
* Factorio
|
||||
* Minecraft
|
||||
* Subnautica
|
||||
* Slay the Spire
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial).
|
||||
For setup and instructions check out our [tutorials page](http://archipelago.gg:48484/tutorial).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
windows binaries.
|
||||
|
||||
|
|
2
Utils.py
2
Utils.py
|
@ -13,7 +13,7 @@ class Version(typing.NamedTuple):
|
|||
build: int
|
||||
|
||||
|
||||
__version__ = "0.1.6"
|
||||
__version__ = "0.1.7"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
import builtins
|
||||
|
|
|
@ -93,10 +93,10 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
|||
"sid": generation.id,
|
||||
"owner": generation.owner},
|
||||
handle_generation_success, handle_generation_failure)
|
||||
except:
|
||||
except Exception as e:
|
||||
generation.state = STATE_ERROR
|
||||
commit()
|
||||
raise
|
||||
logging.exception(e)
|
||||
else:
|
||||
generation.state = STATE_STARTED
|
||||
|
||||
|
|
|
@ -52,8 +52,8 @@ progression_balancing:
|
|||
# exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk.
|
||||
# - "Master Sword Pedestal"
|
||||
{{ game }}:
|
||||
{%- for option_name, option in options.items() %}
|
||||
{{ option_name }}:{% if option.__doc__ %} # {{ option.__doc__ }}{% endif %}
|
||||
{%- for option_key, option in options.items() %}
|
||||
{{ option_key }}:{% if option.__doc__ %} # {{ option.__doc__ | replace('\n', '\n#') | indent(4, first=False) }}{% endif %}
|
||||
{%- if option.range_start is defined %}
|
||||
# you can add additional values between minimum and maximum
|
||||
{{ option.range_start }}: 0 # minimum value
|
||||
|
@ -62,7 +62,7 @@ progression_balancing:
|
|||
random-low: 0
|
||||
random-high: 0
|
||||
{%- elif option.options -%}
|
||||
{%- for sub_option_name, suboption_option_id in option.options.items() %}
|
||||
{%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
|
||||
{{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
|
||||
{%- endfor -%}
|
||||
{%- else %}
|
||||
|
|
|
@ -28,6 +28,7 @@ game: # Pick a game to play
|
|||
Factorio: 0
|
||||
Minecraft: 0
|
||||
Subnautica: 0
|
||||
Slay the Spire: 0
|
||||
requires:
|
||||
version: 0.1.6 # Version of Archipelago required for this yaml to work as expected.
|
||||
# Shared Options supported by all games:
|
||||
|
@ -56,7 +57,23 @@ progression_balancing:
|
|||
# - "Master Sword Pedestal"
|
||||
|
||||
Subnautica: {}
|
||||
|
||||
Slay the Spire:
|
||||
character: # Pick What Character you wish to play with.
|
||||
ironclad: 50
|
||||
silent: 50
|
||||
defect: 50
|
||||
watcher: 50
|
||||
ascension: # What Ascension do you wish to play with.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 50 # minimum value
|
||||
20: 0 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
heart_run: # Whether or not you will need to collect they 3 keys to unlock the final act
|
||||
# and beat the heart to finish the game.
|
||||
false: 50
|
||||
true: 0
|
||||
Factorio:
|
||||
tech_tree_layout:
|
||||
single: 1
|
||||
|
@ -279,29 +296,31 @@ A Link to the Past:
|
|||
on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items
|
||||
off: 50
|
||||
### End of Logic Section ###
|
||||
map_shuffle: # Shuffle dungeon maps into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
off: 50
|
||||
compass_shuffle: # Shuffle compasses into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
off: 50
|
||||
smallkey_shuffle: # Shuffle small keys into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
universal: 0 # allows small keys to be used in any dungeon and adds shops to buy more
|
||||
off: 50
|
||||
bigkey_shuffle: # Shuffle big keys into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
off: 50
|
||||
local_keys: # Keep small keys and big keys local to your world
|
||||
on: 0
|
||||
off: 50
|
||||
dungeon_items: # Alternative to the 4 shuffles and local_keys above this, does nothing until the respective 4 shuffles and local_keys above are deleted
|
||||
mc: 0 # Shuffle maps and compasses
|
||||
none: 50 # Shuffle none of the 4
|
||||
mcsb: 0 # Shuffle all of the 4, any combination of m, c, s and b will shuffle the respective item, or not if it's missing, so you can add more options here
|
||||
lmcsb: 0 # Like mcsb above, but with keys kept local to your world. l is what makes your keys local, or not if it's missing
|
||||
ub: 0 # universal small keys and shuffled big keys
|
||||
# you can add more combos of these letters here
|
||||
bigkey_shuffle: # Big Key Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
smallkey_shuffle: # Small Key Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
universal: 0
|
||||
compass_shuffle: # Compass Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
map_shuffle: # Map Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
dungeon_counters:
|
||||
on: 0 # Always display amount of items checked in a dungeon
|
||||
pickup: 50 # Show when compass is picked up
|
||||
|
|
|
@ -4,11 +4,10 @@ from BaseClasses import MultiWorld
|
|||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||
from worlds.alttp.EntranceShuffle import link_inverted_entrances
|
||||
from worlds.alttp.InvertedRegions import create_inverted_regions
|
||||
from worlds.alttp.ItemPool import generate_itempool, difficulties
|
||||
from worlds.alttp.ItemPool import difficulties
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
from worlds.alttp.Regions import mark_light_world_regions
|
||||
from worlds.alttp.Shops import create_shops
|
||||
from worlds.alttp.Rules import set_rules
|
||||
from test.TestBase import TestBase
|
||||
|
||||
from worlds import AutoWorld
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import unittest
|
||||
from argparse import Namespace
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.alttp.Dungeons import create_dungeons
|
||||
|
@ -7,6 +8,7 @@ from worlds.alttp.EntranceShuffle import connect_entrance, Inverted_LW_Entrances
|
|||
from worlds.alttp.InvertedRegions import create_inverted_regions
|
||||
from worlds.alttp.ItemPool import difficulties
|
||||
from worlds.alttp.Rules import set_inverted_big_bomb_rules
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
class TestInvertedBombRules(unittest.TestCase):
|
||||
|
@ -14,6 +16,10 @@ class TestInvertedBombRules(unittest.TestCase):
|
|||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
self.world.mode[1] = "inverted"
|
||||
args = Namespace
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
self.world.set_options(args)
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
create_inverted_regions(self.world, 1)
|
||||
create_dungeons(self.world, 1)
|
||||
|
|
|
@ -91,6 +91,9 @@ class World(metaclass=AutoWorldRegister):
|
|||
# the client finds its own items in its own world.
|
||||
remote_items: bool = True
|
||||
|
||||
# Hide World Type from various views. Does not remove functionality.
|
||||
hidden = False
|
||||
|
||||
# autoset on creation:
|
||||
world: MultiWorld
|
||||
player: int
|
||||
|
@ -130,11 +133,15 @@ class World(metaclass=AutoWorldRegister):
|
|||
pass
|
||||
|
||||
def fill_hook(cls, progitempool: List[Item], nonexcludeditempool: List[Item],
|
||||
localrestitempool: Dict[int, List[Item]], restitempool: List[Item], fill_locations: List[Location]):
|
||||
localrestitempool: Dict[int, List[Item]], nonlocalrestitempool: Dict[int, List[Item]],
|
||||
restitempool: List[Item], fill_locations: List[Location]):
|
||||
"""Special method that gets called as part of distribute_items_restrictive (main fill).
|
||||
This gets called once per present world type."""
|
||||
pass
|
||||
|
||||
def post_fill(self):
|
||||
"""Optional Method that is called after regular fill. Can be used to do adjustments before output generation."""
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
"""This method gets called from a threadpool, do not use world.random here.
|
||||
If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead."""
|
||||
|
|
|
@ -3,12 +3,16 @@ from worlds.alttp.Bosses import BossFactory
|
|||
from Fill import fill_restrictive
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
from worlds.alttp.Regions import lookup_boss_drops
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
|
||||
|
||||
def create_dungeons(world, player):
|
||||
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
|
||||
dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.keyshuffle[player] == "universal" else small_keys,
|
||||
dungeon = Dungeon(name, dungeon_regions, big_key,
|
||||
[] if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal else small_keys,
|
||||
dungeon_items, player)
|
||||
for item in dungeon.all_items:
|
||||
item.dungeon = dungeon
|
||||
dungeon.boss = BossFactory(default_boss, player) if default_boss else None
|
||||
for region in dungeon.regions:
|
||||
world.get_region(region, player).dungeon = dungeon
|
||||
|
@ -21,56 +25,139 @@ def create_dungeons(world, player):
|
|||
EP = make_dungeon('Eastern Palace', 'Armos Knights', ['Eastern Palace'],
|
||||
ItemFactory('Big Key (Eastern Palace)', player), [],
|
||||
ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], player))
|
||||
DP = make_dungeon('Desert Palace', 'Lanmolas', ['Desert Palace North', 'Desert Palace Main (Inner)', 'Desert Palace Main (Outer)', 'Desert Palace East'], ItemFactory('Big Key (Desert Palace)', player), [ItemFactory('Small Key (Desert Palace)', player)], ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player))
|
||||
ToH = make_dungeon('Tower of Hera', 'Moldorm', ['Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)'], ItemFactory('Big Key (Tower of Hera)', player), [ItemFactory('Small Key (Tower of Hera)', player)], ItemFactory(['Map (Tower of Hera)', 'Compass (Tower of Hera)'], player))
|
||||
PoD = make_dungeon('Palace of Darkness', 'Helmasaur King', ['Palace of Darkness (Entrance)', 'Palace of Darkness (Center)', 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness (Bonk Section)', 'Palace of Darkness (North)', 'Palace of Darkness (Maze)', 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness (Final Section)'], ItemFactory('Big Key (Palace of Darkness)', player), ItemFactory(['Small Key (Palace of Darkness)'] * 6, player), ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)'], player))
|
||||
TT = make_dungeon('Thieves Town', 'Blind', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'], ItemFactory('Big Key (Thieves Town)', player), [ItemFactory('Small Key (Thieves Town)', player)], ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)'], player))
|
||||
SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section', 'Skull Woods Second Section', 'Skull Woods Second Section (Drop)', 'Skull Woods Final Section (Mothula)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'], ItemFactory('Big Key (Skull Woods)', player), ItemFactory(['Small Key (Skull Woods)'] * 3, player), ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)'], player))
|
||||
SP = make_dungeon('Swamp Palace', 'Arrghus', ['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)', 'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)', player), [ItemFactory('Small Key (Swamp Palace)', player)], ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)'], player))
|
||||
IP = make_dungeon('Ice Palace', 'Kholdstare', ['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)', 'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player), ItemFactory(['Small Key (Ice Palace)'] * 2, player), ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player))
|
||||
MM = make_dungeon('Misery Mire', 'Vitreous', ['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)', 'Misery Mire (Vitreous)'], ItemFactory('Big Key (Misery Mire)', player), ItemFactory(['Small Key (Misery Mire)'] * 3, player), ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player))
|
||||
TR = make_dungeon('Turtle Rock', 'Trinexx', ['Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Crystaroller Room)', 'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)'], ItemFactory('Big Key (Turtle Rock)', player), ItemFactory(['Small Key (Turtle Rock)'] * 4, player), ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))
|
||||
DP = make_dungeon('Desert Palace', 'Lanmolas',
|
||||
['Desert Palace North', 'Desert Palace Main (Inner)', 'Desert Palace Main (Outer)',
|
||||
'Desert Palace East'], ItemFactory('Big Key (Desert Palace)', player),
|
||||
[ItemFactory('Small Key (Desert Palace)', player)],
|
||||
ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player))
|
||||
ToH = make_dungeon('Tower of Hera', 'Moldorm',
|
||||
['Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)'],
|
||||
ItemFactory('Big Key (Tower of Hera)', player),
|
||||
[ItemFactory('Small Key (Tower of Hera)', player)],
|
||||
ItemFactory(['Map (Tower of Hera)', 'Compass (Tower of Hera)'], player))
|
||||
PoD = make_dungeon('Palace of Darkness', 'Helmasaur King',
|
||||
['Palace of Darkness (Entrance)', 'Palace of Darkness (Center)',
|
||||
'Palace of Darkness (Big Key Chest)', 'Palace of Darkness (Bonk Section)',
|
||||
'Palace of Darkness (North)', 'Palace of Darkness (Maze)',
|
||||
'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness (Final Section)'],
|
||||
ItemFactory('Big Key (Palace of Darkness)', player),
|
||||
ItemFactory(['Small Key (Palace of Darkness)'] * 6, player),
|
||||
ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)'], player))
|
||||
TT = make_dungeon('Thieves Town', 'Blind', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'],
|
||||
ItemFactory('Big Key (Thieves Town)', player), [ItemFactory('Small Key (Thieves Town)', player)],
|
||||
ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)'], player))
|
||||
SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section',
|
||||
'Skull Woods Second Section', 'Skull Woods Second Section (Drop)',
|
||||
'Skull Woods Final Section (Mothula)',
|
||||
'Skull Woods First Section (Right)',
|
||||
'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'],
|
||||
ItemFactory('Big Key (Skull Woods)', player),
|
||||
ItemFactory(['Small Key (Skull Woods)'] * 3, player),
|
||||
ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)'], player))
|
||||
SP = make_dungeon('Swamp Palace', 'Arrghus',
|
||||
['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)',
|
||||
'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)', player),
|
||||
[ItemFactory('Small Key (Swamp Palace)', player)],
|
||||
ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)'], player))
|
||||
IP = make_dungeon('Ice Palace', 'Kholdstare',
|
||||
['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)',
|
||||
'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player),
|
||||
ItemFactory(['Small Key (Ice Palace)'] * 2, player),
|
||||
ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player))
|
||||
MM = make_dungeon('Misery Mire', 'Vitreous',
|
||||
['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)',
|
||||
'Misery Mire (Vitreous)'], ItemFactory('Big Key (Misery Mire)', player),
|
||||
ItemFactory(['Small Key (Misery Mire)'] * 3, player),
|
||||
ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player))
|
||||
TR = make_dungeon('Turtle Rock', 'Trinexx',
|
||||
['Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)',
|
||||
'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Crystaroller Room)',
|
||||
'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)'],
|
||||
ItemFactory('Big Key (Turtle Rock)', player),
|
||||
ItemFactory(['Small Key (Turtle Rock)'] * 4, player),
|
||||
ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
|
||||
GT = make_dungeon('Ganons Tower', 'Agahnim2', ['Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
|
||||
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None,
|
||||
ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
|
||||
GT = make_dungeon('Ganons Tower', 'Agahnim2',
|
||||
['Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)',
|
||||
'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)',
|
||||
'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)',
|
||||
'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'],
|
||||
ItemFactory('Big Key (Ganons Tower)', player),
|
||||
ItemFactory(['Small Key (Ganons Tower)'] * 4, player),
|
||||
ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
|
||||
else:
|
||||
AT = make_dungeon('Inverted Agahnims Tower', 'Agahnim', ['Inverted Agahnims Tower', 'Agahnim 1'], None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
|
||||
GT = make_dungeon('Inverted Ganons Tower', 'Agahnim2', ['Inverted Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
|
||||
AT = make_dungeon('Inverted Agahnims Tower', 'Agahnim', ['Inverted Agahnims Tower', 'Agahnim 1'], None,
|
||||
ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
|
||||
GT = make_dungeon('Inverted Ganons Tower', 'Agahnim2',
|
||||
['Inverted Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)',
|
||||
'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)',
|
||||
'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)',
|
||||
'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)',
|
||||
'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player),
|
||||
ItemFactory(['Small Key (Ganons Tower)'] * 4, player),
|
||||
ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
|
||||
|
||||
GT.bosses['bottom'] = BossFactory('Armos Knights', player)
|
||||
GT.bosses['middle'] = BossFactory('Lanmolas', player)
|
||||
GT.bosses['top'] = BossFactory('Moldorm', player)
|
||||
|
||||
world.dungeons += [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT]
|
||||
for dungeon in [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT]:
|
||||
world.dungeons[dungeon.name, dungeon.player] = dungeon
|
||||
|
||||
|
||||
def get_dungeon_item_pool(world):
|
||||
items = [item for dungeon in world.dungeons for item in dungeon.all_items]
|
||||
items = [item for dungeon in world.dungeons.values() for item in dungeon.all_items]
|
||||
for item in items:
|
||||
item.world = world
|
||||
return items
|
||||
|
||||
|
||||
def fill_dungeons_restrictive(world):
|
||||
"""Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside."""
|
||||
dungeon_items = [item for item in get_dungeon_item_pool(world) if
|
||||
(((item.smallkey and not world.keyshuffle[item.player])
|
||||
or (item.bigkey and not world.bigkeyshuffle[item.player])
|
||||
or (item.map and not world.mapshuffle[item.player])
|
||||
or (item.compass and not world.compassshuffle[item.player])
|
||||
) and world.goal[item.player] != 'icerodhunt')]
|
||||
if dungeon_items:
|
||||
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if restricted}
|
||||
locations = [location for location in world.get_unfilled_dungeon_locations()
|
||||
if not (location.player in restricted_players and location.name in lookup_boss_drops)] # filter boss
|
||||
def get_dungeon_item_pool_player(world, player):
|
||||
items = [item for dungeon in world.dungeons.values() if dungeon.player == player for item in dungeon.all_items]
|
||||
for item in items:
|
||||
item.world = world
|
||||
return items
|
||||
|
||||
world.random.shuffle(locations)
|
||||
all_state_base = world.get_all_state()
|
||||
# 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, True)
|
||||
|
||||
def fill_dungeons_restrictive(autoworld, world):
|
||||
"""Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside."""
|
||||
localized: set = set()
|
||||
dungeon_specific: set = set()
|
||||
for subworld in world.get_game_worlds("A Link to the Past"):
|
||||
player = subworld.player
|
||||
localized |= {(player, item_name) for item_name in
|
||||
subworld.dungeon_local_item_names}
|
||||
dungeon_specific |= {(player, item_name) for item_name in
|
||||
subworld.dungeon_specific_item_names}
|
||||
|
||||
if localized:
|
||||
in_dungeon_items = [item for item in get_dungeon_item_pool(world) if (item.player, item.name) in localized]
|
||||
if in_dungeon_items:
|
||||
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if
|
||||
restricted}
|
||||
locations = [location for location in world.get_unfilled_dungeon_locations()
|
||||
# filter boss
|
||||
if not (location.player in restricted_players and location.name in lookup_boss_drops)]
|
||||
if dungeon_specific:
|
||||
for location in locations:
|
||||
dungeon = location.parent_region.dungeon
|
||||
orig_rule = location.item_rule
|
||||
location.item_rule = lambda item, dungeon=dungeon, orig_rule=orig_rule: \
|
||||
(not (item.player, item.name) in dungeon_specific or item.dungeon is dungeon) and orig_rule(item)
|
||||
|
||||
world.random.shuffle(locations)
|
||||
all_state_base = world.get_all_state()
|
||||
# Dungeon-locked items have to be placed first, to not run out of spaces for dungeon-locked items
|
||||
# subsort in the order Big Key, Small Key, Other before placing dungeon items
|
||||
|
||||
sort_order = {"BigKey": 3, "SmallKey": 2}
|
||||
in_dungeon_items.sort(
|
||||
key=lambda item: sort_order.get(item.type, 1) +
|
||||
(5 if (item.player, item.name) in dungeon_specific else 0))
|
||||
fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True)
|
||||
|
||||
|
||||
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
|
||||
|
@ -79,7 +166,8 @@ dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
|
|||
'Palace of Darkness - Prize': [0x155B8],
|
||||
'Swamp Palace - Prize': [0x155B7],
|
||||
'Thieves\' Town - Prize': [0x155C6],
|
||||
'Skull Woods - Prize': [0x155BA, 0x155BB, 0x155BC, 0x155BD, 0x15608, 0x15609, 0x1560A, 0x1560B],
|
||||
'Skull Woods - Prize': [0x155BA, 0x155BB, 0x155BC, 0x155BD, 0x15608, 0x15609, 0x1560A,
|
||||
0x1560B],
|
||||
'Ice Palace - Prize': [0x155BF],
|
||||
'Misery Mire - Prize': [0x155B9],
|
||||
'Turtle Rock - Prize': [0x155C7, 0x155A7, 0x155AA, 0x155AB]}
|
||||
|
|
|
@ -206,26 +206,10 @@ def parse_arguments(argv, no_defaults=False):
|
|||
time).
|
||||
''', type=int)
|
||||
|
||||
parser.add_argument('--mapshuffle', default=defval(False),
|
||||
help='Maps are no longer restricted to their dungeons, but can be anywhere',
|
||||
action='store_true')
|
||||
parser.add_argument('--compassshuffle', default=defval(False),
|
||||
help='Compasses are no longer restricted to their dungeons, but can be anywhere',
|
||||
action='store_true')
|
||||
parser.add_argument('--keyshuffle', default=defval("off"), help='\
|
||||
on: Small Keys are no longer restricted to their dungeons, but can be anywhere.\
|
||||
universal: Makes all Small Keys usable in any dungeon and places shops to buy more keys.',
|
||||
choices=["on", "universal", "off"])
|
||||
parser.add_argument('--bigkeyshuffle', default=defval(False),
|
||||
help='Big Keys are no longer restricted to their dungeons, but can be anywhere',
|
||||
action='store_true')
|
||||
parser.add_argument('--keysanity', default=defval(False), help=argparse.SUPPRESS, action='store_true')
|
||||
parser.add_argument('--retro', default=defval(False), help='''\
|
||||
Keys are universal, shooting arrows costs rupees,
|
||||
and a few other little things make this more like Zelda-1.
|
||||
''', action='store_true')
|
||||
parser.add_argument('--startinventory', default=defval(''),
|
||||
help='Specifies a list of items that will be in your starting inventory (separated by commas)')
|
||||
parser.add_argument('--local_items', default=defval(''),
|
||||
help='Specifies a list of items that will not spread across the multiworld (separated by commas)')
|
||||
parser.add_argument('--non_local_items', default=defval(''),
|
||||
|
@ -291,13 +275,10 @@ def parse_arguments(argv, no_defaults=False):
|
|||
parser.add_argument('--restrict_dungeon_item_on_boss', default=defval(False), action="store_true")
|
||||
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
|
||||
parser.add_argument('--names', default=defval(''))
|
||||
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
|
||||
parser.add_argument('--outputpath')
|
||||
parser.add_argument('--game', default="A Link to the Past")
|
||||
parser.add_argument('--race', default=defval(False), action='store_true')
|
||||
parser.add_argument('--outputname')
|
||||
parser.add_argument('--disable_glitch_boots', default=defval(False), action='store_true', help='''\
|
||||
turns off starting with Pegasus Boots in glitched modes.''')
|
||||
parser.add_argument('--start_hints')
|
||||
if multiargs.multi:
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
|
@ -314,7 +295,6 @@ def parse_arguments(argv, no_defaults=False):
|
|||
ret.plando_connections = []
|
||||
ret.er_seeds = {}
|
||||
|
||||
ret.glitch_boots = not ret.disable_glitch_boots
|
||||
if ret.timer == "none":
|
||||
ret.timer = False
|
||||
if ret.dungeon_counters == 'on':
|
||||
|
@ -322,12 +302,6 @@ def parse_arguments(argv, no_defaults=False):
|
|||
elif ret.dungeon_counters == 'off':
|
||||
ret.dungeon_counters = False
|
||||
|
||||
if ret.keysanity:
|
||||
ret.mapshuffle = ret.compassshuffle = ret.keyshuffle = ret.bigkeyshuffle = True
|
||||
elif ret.keyshuffle == "on":
|
||||
ret.keyshuffle = True
|
||||
elif ret.keyshuffle == "off":
|
||||
ret.keyshuffle = False
|
||||
if multiargs.multi:
|
||||
defaults = copy.deepcopy(ret)
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
|
@ -336,7 +310,6 @@ def parse_arguments(argv, no_defaults=False):
|
|||
for name in ['logic', 'mode', 'swordless', 'goal', 'difficulty', 'item_functionality',
|
||||
'shuffle', 'open_pyramid', 'timer',
|
||||
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
|
||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
||||
'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer',
|
||||
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
|
||||
'sprite',
|
||||
|
@ -344,7 +317,7 @@ def parse_arguments(argv, no_defaults=False):
|
|||
"triforce_pieces_required", "shop_shuffle",
|
||||
"required_medallions", "start_hints",
|
||||
"plando_items", "plando_texts", "plando_connections", "er_seeds",
|
||||
'dungeon_counters', 'glitch_boots', 'killable_thieves',
|
||||
'dungeon_counters', 'killable_thieves',
|
||||
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
|
||||
'restrict_dungeon_item_on_boss', 'game']:
|
||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||
|
|
|
@ -5,10 +5,11 @@ from BaseClasses import Region, RegionType
|
|||
from worlds.alttp.SubClasses import ALttPLocation
|
||||
from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops
|
||||
from worlds.alttp.Bosses import place_bosses
|
||||
from worlds.alttp.Dungeons import get_dungeon_item_pool
|
||||
from worlds.alttp.Dungeons import get_dungeon_item_pool_player
|
||||
from worlds.alttp.EntranceShuffle import connect_entrance
|
||||
from Fill import FillError, fill_restrictive
|
||||
from Fill import FillError
|
||||
from worlds.alttp.Items import ItemFactory, GetBeemizerItem
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
|
||||
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
|
||||
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
|
||||
|
@ -274,7 +275,7 @@ def generate_itempool(world):
|
|||
itempool.extend(['Rupees (300)'] * 34)
|
||||
itempool.extend(['Bombs (10)'] * 5)
|
||||
itempool.extend(['Arrows (10)'] * 7)
|
||||
if world.keyshuffle[player] == 'universal':
|
||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
itempool.extend(itemdiff.universal_keys)
|
||||
itempool.append('Small Key (Universal)')
|
||||
|
||||
|
@ -362,12 +363,8 @@ def generate_itempool(world):
|
|||
if treasure_hunt_icon is not None:
|
||||
world.treasure_hunt_icon[player] = treasure_hunt_icon
|
||||
|
||||
dungeon_items = [item for item in get_dungeon_item_pool(world) if item.player == player
|
||||
and ((item.smallkey and world.keyshuffle[player])
|
||||
or (item.bigkey and world.bigkeyshuffle[player])
|
||||
or (item.map and world.mapshuffle[player])
|
||||
or (item.compass and world.compassshuffle[player])
|
||||
or world.goal[player] == 'icerodhunt')]
|
||||
dungeon_items = [item for item in get_dungeon_item_pool_player(world, player)
|
||||
if item.name not in world.worlds[player].dungeon_local_item_names]
|
||||
|
||||
if world.goal[player] == 'icerodhunt':
|
||||
for item in dungeon_items:
|
||||
|
@ -500,14 +497,14 @@ def create_dynamic_shop_locations(world, player):
|
|||
if item is None:
|
||||
continue
|
||||
if item['create_location']:
|
||||
loc = ALttPLocation(player, "{} Slot {}".format(shop.region.name, i + 1), parent=shop.region)
|
||||
loc = ALttPLocation(player, f"{shop.region.name} {shop.slot_names[i]}", 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.shop_slot = i
|
||||
loc.event = True
|
||||
loc.locked = True
|
||||
|
||||
|
@ -637,7 +634,7 @@ def get_pool_core(world, player: int):
|
|||
if retro:
|
||||
replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)'}
|
||||
pool = ['Rupees (5)' if item in replace else item for item in pool]
|
||||
if world.keyshuffle[player] == "universal":
|
||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
pool.extend(diff.universal_keys)
|
||||
item_to_place = 'Small Key (Universal)' if goal != 'icerodhunt' else 'Nothing'
|
||||
if mode == 'standard':
|
||||
|
@ -774,7 +771,7 @@ def make_custom_item_pool(world, player):
|
|||
itemtotal = itemtotal + 1
|
||||
|
||||
if mode == 'standard':
|
||||
if world.keyshuffle[player] == "universal":
|
||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
key_location = world.random.choice(
|
||||
['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
|
||||
'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
|
||||
|
@ -797,7 +794,7 @@ def make_custom_item_pool(world, player):
|
|||
pool.extend(['Magic Mirror'] * customitemarray[22])
|
||||
pool.extend(['Moon Pearl'] * customitemarray[28])
|
||||
|
||||
if world.keyshuffle == "universal":
|
||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode
|
||||
if itemtotal < total_items_to_place:
|
||||
pool.extend(['Nothing'] * (total_items_to_place - itemtotal))
|
||||
|
|
|
@ -28,6 +28,46 @@ class Goal(Choice):
|
|||
option_hand_in = 2
|
||||
|
||||
|
||||
class DungeonItem(Choice):
|
||||
value: int
|
||||
option_original_dungeon = 0
|
||||
option_own_dungeons = 1
|
||||
option_own_world = 2
|
||||
option_any_world = 3
|
||||
option_different_world = 4
|
||||
alias_true = 3
|
||||
alias_false = 0
|
||||
|
||||
@property
|
||||
def in_dungeon(self):
|
||||
return self.value in {0, 1}
|
||||
|
||||
|
||||
class bigkey_shuffle(DungeonItem):
|
||||
"""Big Key Placement"""
|
||||
item_name_group = "Big Keys"
|
||||
displayname = "Big Key Shuffle"
|
||||
|
||||
|
||||
class smallkey_shuffle(DungeonItem):
|
||||
"""Small Key Placement"""
|
||||
option_universal = 5
|
||||
item_name_group = "Small Keys"
|
||||
displayname = "Small Key Shuffle"
|
||||
|
||||
|
||||
class compass_shuffle(DungeonItem):
|
||||
"""Compass Placement"""
|
||||
item_name_group = "Compasses"
|
||||
displayname = "Compass Shuffle"
|
||||
|
||||
|
||||
class map_shuffle(DungeonItem):
|
||||
"""Map Placement"""
|
||||
item_name_group = "Maps"
|
||||
displayname = "Map Shuffle"
|
||||
|
||||
|
||||
class Crystals(Range):
|
||||
range_start = 0
|
||||
range_end = 7
|
||||
|
@ -85,6 +125,7 @@ class Progressive(Choice):
|
|||
def want_progressives(self, random):
|
||||
return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)
|
||||
|
||||
|
||||
class Palette(Choice):
|
||||
option_default = 0
|
||||
option_good = 1
|
||||
|
@ -126,9 +167,10 @@ class HeartBeep(Choice):
|
|||
displayname = "Heart Beep Rate"
|
||||
option_normal = 0
|
||||
option_double = 1
|
||||
option_half = 2,
|
||||
option_half = 2
|
||||
option_quarter = 3
|
||||
option_off = 4
|
||||
alias_false = 4
|
||||
|
||||
|
||||
class HeartColor(Choice):
|
||||
|
@ -179,6 +221,10 @@ class TriforceHud(Choice):
|
|||
alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"crystals_needed_for_gt": CrystalsTower,
|
||||
"crystals_needed_for_ganon": CrystalsGanon,
|
||||
"bigkey_shuffle": bigkey_shuffle,
|
||||
"smallkey_shuffle": smallkey_shuffle,
|
||||
"compass_shuffle": compass_shuffle,
|
||||
"map_shuffle": map_shuffle,
|
||||
"progressive": Progressive,
|
||||
"shop_item_slots": ShopItemSlots,
|
||||
"ow_palettes": OWPalette,
|
||||
|
@ -193,6 +239,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
|
|||
"menuspeed": MenuSpeed,
|
||||
"music": Music,
|
||||
"reduceflashing": ReduceFlashing,
|
||||
"triforcehud": TriforceHud
|
||||
"triforcehud": TriforceHud,
|
||||
"glitch_boots": DefaultOnToggle
|
||||
|
||||
}
|
||||
|
|
|
@ -676,12 +676,10 @@ location_table: typing.Dict[str,
|
|||
|
||||
from worlds.alttp.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", -2: "Server"}
|
||||
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}}
|
||||
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, "Server": -2}
|
||||
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()}}
|
||||
lookup_name_to_id.update(shop_table_by_location)
|
||||
|
||||
lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 191256: 'Kings Grave Inner Rocks',
|
||||
|
|
|
@ -37,6 +37,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts
|
|||
from Utils import local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen
|
||||
from worlds.alttp.Items import ItemFactory, item_table
|
||||
from worlds.alttp.EntranceShuffle import door_addresses
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
import Patch
|
||||
|
||||
try:
|
||||
|
@ -763,7 +764,7 @@ def patch_rom(world, rom, player, enemized):
|
|||
# patch items
|
||||
|
||||
for location in world.get_locations():
|
||||
if location.player != player or location.address is None or location.shop_slot:
|
||||
if location.player != player or location.address is None or location.shop_slot is not None:
|
||||
continue
|
||||
|
||||
itemid = location.item.code if location.item is not None else 0x5A
|
||||
|
@ -802,14 +803,14 @@ def patch_rom(world, rom, player, enemized):
|
|||
|
||||
# patch music
|
||||
music_addresses = dungeon_music_addresses[location.name]
|
||||
if world.mapshuffle[player]:
|
||||
if world.map_shuffle[player]:
|
||||
music = local_random.choice([0x11, 0x16])
|
||||
else:
|
||||
music = 0x11 if 'Pendant' in location.item.name else 0x16
|
||||
for music_address in music_addresses:
|
||||
rom.write_byte(music_address, music)
|
||||
|
||||
if world.mapshuffle[player]:
|
||||
if world.map_shuffle[player]:
|
||||
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
|
||||
|
||||
# patch entrance/exits/holes
|
||||
|
@ -1491,18 +1492,18 @@ def patch_rom(world, rom, player, enemized):
|
|||
# block HC upstairs doors in rain state in standard mode
|
||||
rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" and world.shuffle[player] != 'vanilla' else 0x00)
|
||||
|
||||
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] is True else 0x00)
|
||||
| (0x02 if world.compassshuffle[player] else 0x00)
|
||||
| (0x04 if world.mapshuffle[player] else 0x00)
|
||||
| (0x08 if world.bigkeyshuffle[player] else 0x00))) # free roaming item text boxes
|
||||
rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] else 0x00) # maps showing crystals on overworld
|
||||
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.smallkey_shuffle[player] else 0x00)
|
||||
| (0x02 if world.compass_shuffle[player] else 0x00)
|
||||
| (0x04 if world.map_shuffle[player] else 0x00)
|
||||
| (0x08 if world.bigkey_shuffle[player] else 0x00))) # free roaming item text boxes
|
||||
rom.write_byte(0x18003B, 0x01 if world.map_shuffle[player] else 0x00) # maps showing crystals on overworld
|
||||
|
||||
# compasses showing dungeon count
|
||||
if world.clock_mode[player] or not world.dungeon_counters[player]:
|
||||
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
|
||||
elif world.dungeon_counters[player] is True:
|
||||
rom.write_byte(0x18003C, 0x02) # always on
|
||||
elif world.compassshuffle[player] or world.dungeon_counters[player] == 'pickup':
|
||||
elif world.compass_shuffle[player] or world.dungeon_counters[player] == 'pickup':
|
||||
rom.write_byte(0x18003C, 0x01) # show on pickup
|
||||
else:
|
||||
rom.write_byte(0x18003C, 0x00)
|
||||
|
@ -1515,10 +1516,11 @@ def patch_rom(world, rom, player, enemized):
|
|||
# b - Big Key
|
||||
# a - Small Key
|
||||
#
|
||||
rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] is True else 0x00)
|
||||
| (0x02 if world.bigkeyshuffle[player] else 0x00)
|
||||
| (0x04 if world.mapshuffle[player] else 0x00)
|
||||
| (0x08 if world.compassshuffle[player] else 0x00))) # free roaming items in menu
|
||||
rom.write_byte(0x180045, ((0x00 if (world.smallkey_shuffle[player] == smallkey_shuffle.option_original_dungeon or
|
||||
world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) else 0x01)
|
||||
| (0x02 if world.bigkey_shuffle[player] else 0x00)
|
||||
| (0x04 if world.map_shuffle[player] else 0x00)
|
||||
| (0x08 if world.compass_shuffle[player] else 0x00))) # free roaming items in menu
|
||||
|
||||
# Map reveals
|
||||
reveal_bytes = {
|
||||
|
@ -1544,11 +1546,11 @@ def patch_rom(world, rom, player, enemized):
|
|||
return 0x0000
|
||||
|
||||
rom.write_int16(0x18017A,
|
||||
get_reveal_bytes('Green Pendant') if world.mapshuffle[player] else 0x0000) # Sahasrahla reveal
|
||||
rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if world.mapshuffle[
|
||||
get_reveal_bytes('Green Pendant') if world.map_shuffle[player] else 0x0000) # Sahasrahla reveal
|
||||
rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if world.map_shuffle[
|
||||
player] else 0x0000) # Bomb Shop Reveal
|
||||
|
||||
rom.write_byte(0x180172, 0x01 if world.keyshuffle[player] == "universal" else 0x00) # universal keys
|
||||
rom.write_byte(0x180172, 0x01 if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal else 0x00) # universal keys
|
||||
rom.write_byte(0x18637E, 0x01 if world.retro[player] else 0x00) # Skip quiver in item shops once bought
|
||||
rom.write_byte(0x180175, 0x01 if world.retro[player] else 0x00) # rupee bow
|
||||
rom.write_byte(0x180176, 0x0A if world.retro[player] else 0x00) # wood arrow cost
|
||||
|
@ -2087,9 +2089,9 @@ def write_strings(rom, world, player):
|
|||
if not dest:
|
||||
return "nothing"
|
||||
if ped_hint:
|
||||
hint = dest.pedestal_hint_text if dest.pedestal_hint_text else "unknown item"
|
||||
hint = dest.pedestal_hint_text
|
||||
else:
|
||||
hint = dest.hint_text if dest.hint_text else "something"
|
||||
hint = dest.hint_text
|
||||
if dest.player != player:
|
||||
if ped_hint:
|
||||
hint += f" for {world.player_name[dest.player]}!"
|
||||
|
@ -2247,9 +2249,9 @@ def write_strings(rom, world, player):
|
|||
|
||||
# Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well.
|
||||
items_to_hint = RelevantItems.copy()
|
||||
if world.keyshuffle[player]:
|
||||
if world.smallkey_shuffle[player]:
|
||||
items_to_hint.extend(SmallKeys)
|
||||
if world.bigkeyshuffle[player]:
|
||||
if world.bigkey_shuffle[player]:
|
||||
items_to_hint.extend(BigKeys)
|
||||
local_random.shuffle(items_to_hint)
|
||||
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'] else 8
|
||||
|
|
|
@ -8,6 +8,7 @@ from worlds.alttp.UnderworldGlitchRules import underworld_glitches_rules
|
|||
from worlds.alttp.Bosses import GanonDefeatRule
|
||||
from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \
|
||||
item_name
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
|
||||
|
||||
def set_rules(world):
|
||||
|
@ -99,7 +100,7 @@ def set_rules(world):
|
|||
add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.world.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or')
|
||||
|
||||
set_bunny_rules(world, player, world.mode[player] == 'inverted')
|
||||
|
||||
|
||||
|
||||
def mirrorless_path_to_castle_courtyard(world, player):
|
||||
# If Agahnim is defeated then the courtyard needs to be accessible without using the mirror for the mirror offset glitch.
|
||||
|
@ -211,17 +212,17 @@ def global_rules(world, player):
|
|||
set_rule(world.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player))
|
||||
|
||||
set_rule(world.get_entrance('Sewers Door', player),
|
||||
lambda state: state.has_key('Small Key (Hyrule Castle)', player) or (
|
||||
world.keyshuffle[player] == "universal" and world.mode[
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player) or (
|
||||
world.smallkey_shuffle[player] == smallkey_shuffle.option_universal and world.mode[
|
||||
player] == 'standard')) # standard universal small keys cannot access the shop
|
||||
set_rule(world.get_entrance('Sewers Back Door', player),
|
||||
lambda state: state.has_key('Small Key (Hyrule Castle)', player))
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
|
||||
set_rule(world.get_entrance('Agahnim 1', player),
|
||||
lambda state: state.has_sword(player) and state.has_key('Small Key (Agahnims Tower)', player, 2))
|
||||
lambda state: state.has_sword(player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
|
||||
|
||||
set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: state.can_kill_most_things(player, 8))
|
||||
set_rule(world.get_location('Castle Tower - Dark Maze', player),
|
||||
lambda state: state.can_kill_most_things(player, 8) and state.has_key('Small Key (Agahnims Tower)',
|
||||
lambda state: state.can_kill_most_things(player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)',
|
||||
player))
|
||||
|
||||
set_rule(world.get_location('Eastern Palace - Big Chest', player),
|
||||
|
@ -238,15 +239,15 @@ def global_rules(world, player):
|
|||
|
||||
set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player))
|
||||
set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player))
|
||||
set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state.has_key('Small Key (Desert Palace)', player))
|
||||
set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state.has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player))
|
||||
set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
|
||||
# logic patch to prevent placing a crystal in Desert that's required to reach the required keys
|
||||
if not (world.keyshuffle[player] and world.bigkeyshuffle[player]):
|
||||
if not (world.smallkey_shuffle[player] and world.bigkey_shuffle[player]):
|
||||
add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.world.get_region('Desert Palace Main (Outer)', player).can_reach(state))
|
||||
|
||||
set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state.has_key('Small Key (Tower of Hera)', player) or item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player))
|
||||
set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player))
|
||||
set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
||||
set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
||||
set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: state.has_fire_source(player))
|
||||
|
@ -254,36 +255,36 @@ def global_rules(world, player):
|
|||
set_always_allow(world.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
|
||||
|
||||
set_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
|
||||
set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state.has_key('Small Key (Swamp Palace)', player))
|
||||
set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player))
|
||||
set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player) or item_name(state, 'Swamp Palace - Big Chest', player) == ('Big Key (Swamp Palace)', player))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Swamp Palace - Big Chest', player), lambda state, item: item.name == 'Big Key (Swamp Palace)' and item.player == player)
|
||||
set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player))
|
||||
if not world.keyshuffle[player] and world.logic[player] != 'nologic':
|
||||
if not world.smallkey_shuffle[player] and world.logic[player] != 'nologic':
|
||||
forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
|
||||
|
||||
set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
|
||||
set_rule(world.get_entrance('Blind Fight', player), lambda state: state.has_key('Small Key (Thieves Town)', player))
|
||||
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state.has_key('Small Key (Thieves Town)', player) or item_name(state, 'Thieves\' Town - Big Chest', player) == ('Small Key (Thieves Town)', player)) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
||||
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player) or item_name(state, 'Thieves\' Town - Big Chest', player) == ('Small Key (Thieves Town)', player)) and state.has('Hammer', player))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player and state.has('Hammer', player))
|
||||
set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state.has_key('Small Key (Thieves Town)', player))
|
||||
set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
||||
|
||||
set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state.has_key('Small Key (Skull Woods)', player))
|
||||
set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state.has_key('Small Key (Skull Woods)', player))
|
||||
set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section
|
||||
set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 2))
|
||||
set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
|
||||
set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
|
||||
set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section
|
||||
set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2))
|
||||
set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) or item_name(state, 'Skull Woods - Big Chest', player) == ('Big Key (Skull Woods)', player))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Skull Woods - Big Chest', player), lambda state, item: item.name == 'Big Key (Skull Woods)' and item.player == player)
|
||||
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and state.has_sword(player)) # sword required for curtain
|
||||
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and state.has_sword(player)) # sword required for curtain
|
||||
|
||||
set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.can_melt_things(player))
|
||||
set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player))
|
||||
set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state.has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state.has_key('Small Key (Ice Palace)', player, 1))))
|
||||
set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1))))
|
||||
set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or (
|
||||
item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state.has_key('Small Key (Ice Palace)', player))) and (state.world.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
|
||||
item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.world.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
|
||||
set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player))
|
||||
|
||||
set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player))) # need to defeat wizzrobes, bombs don't work ...
|
||||
|
@ -292,13 +293,13 @@ def global_rules(world, player):
|
|||
set_rule(world.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player))
|
||||
# you can squander the free small key from the pot by opening the south door to the north west switch room, locking you out of accessing a color switch ...
|
||||
# big key gives backdoor access to that from the teleporter in the north west
|
||||
set_rule(world.get_location('Misery Mire - Map Chest', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 1) or state.has('Big Key (Misery Mire)', player))
|
||||
set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 1) or state.has_key('Big Key (Misery Mire)', player))
|
||||
set_rule(world.get_location('Misery Mire - Map Chest', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state.has('Big Key (Misery Mire)', player))
|
||||
set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state._lttp_has_key('Big Key (Misery Mire)', player))
|
||||
# we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet
|
||||
set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 2) if ((
|
||||
set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2) if ((
|
||||
item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or
|
||||
(
|
||||
item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state.has_key('Small Key (Misery Mire)', player, 3))
|
||||
item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3))
|
||||
set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: state.has_fire_source(player))
|
||||
set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: state.has_fire_source(player))
|
||||
set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player))
|
||||
|
@ -317,27 +318,27 @@ def global_rules(world, player):
|
|||
set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
|
||||
|
||||
if not world.enemy_shuffle[player]:
|
||||
set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: state.can_shoot_arrows(player))
|
||||
set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and state.can_shoot_arrows(player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 4))
|
||||
set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and state.can_shoot_arrows(player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))
|
||||
set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player))
|
||||
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||
item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state.has_key('Small Key (Palace of Darkness)', player, 3)))
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||
item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3)))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state.has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
|
||||
set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||
item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state.has_key('Small Key (Palace of Darkness)', player, 4)))
|
||||
set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||
item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state.has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
|
||||
set_rule(world.get_entrance('Palace of Darkness Maze Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6))
|
||||
set_rule(world.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
|
||||
|
||||
# these key rules are conservative, you might be able to get away with more lenient rules
|
||||
randomizer_room_chests = ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right']
|
||||
|
@ -346,30 +347,30 @@ def global_rules(world, player):
|
|||
set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player))
|
||||
set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
|
||||
set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 4) or (
|
||||
item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state.has_key('Small Key (Ganons Tower)', player, 3)))
|
||||
set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
|
||||
item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state.has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
|
||||
set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
|
||||
|
||||
# It is possible to need more than 2 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
|
||||
# However we need to leave these at the lower values to derive that with 3 keys it is always possible to reach Bob and Ice Armos.
|
||||
set_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 2))
|
||||
set_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 2))
|
||||
# It is possible to need more than 3 keys ....
|
||||
set_rule(world.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3))
|
||||
set_rule(world.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3))
|
||||
|
||||
#The actual requirements for these rooms to avoid key-lock
|
||||
set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3) or ((
|
||||
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_in_locations(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state.has_key('Small Key (Ganons Tower)', player, 2)))
|
||||
set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) or ((
|
||||
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_in_locations(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 2)))
|
||||
for location in randomizer_room_chests:
|
||||
set_rule(world.get_location(location, player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 4) or (
|
||||
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state.has_key('Small Key (Ganons Tower)', player, 3)))
|
||||
set_rule(world.get_location(location, player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
|
||||
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
|
||||
|
||||
# Once again it is possible to need more than 3 keys...
|
||||
set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player))
|
||||
set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player))
|
||||
# Actual requirements
|
||||
for location in compass_room_chests:
|
||||
set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state.has_key('Small Key (Ganons Tower)', player, 4) or (
|
||||
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state.has_key('Small Key (Ganons Tower)', player, 3))))
|
||||
set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
|
||||
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))))
|
||||
|
||||
set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player))
|
||||
|
||||
|
@ -388,9 +389,9 @@ def global_rules(world, player):
|
|||
set_rule(world.get_entrance('Ganons Tower Torch Rooms', player),
|
||||
lambda state: state.has_fire_source(player) and state.world.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
|
||||
set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player),
|
||||
lambda state: state.has_key('Small Key (Ganons Tower)', player, 3))
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3))
|
||||
set_rule(world.get_entrance('Ganons Tower Moldorm Door', player),
|
||||
lambda state: state.has_key('Small Key (Ganons Tower)', player, 4))
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4))
|
||||
set_rule(world.get_entrance('Ganons Tower Moldorm Gap', player),
|
||||
lambda state: state.has('Hookshot', player) and state.world.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state))
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player))
|
||||
|
@ -799,14 +800,14 @@ def add_conditional_lamps(world, player):
|
|||
def open_rules(world, player):
|
||||
# softlock protection as you can reach the sewers small key door with a guard drop key
|
||||
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player),
|
||||
lambda state: state.has_key('Small Key (Hyrule Castle)', player))
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
|
||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
||||
lambda state: state.has_key('Small Key (Hyrule Castle)', player))
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
|
||||
|
||||
|
||||
def swordless_rules(world, player):
|
||||
set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or state.can_shoot_arrows(player) or state.has('Cane of Somaria', player)) and state.has_key('Small Key (Agahnims Tower)', player, 2))
|
||||
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain
|
||||
set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or state.can_shoot_arrows(player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
|
||||
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain
|
||||
set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace
|
||||
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop
|
||||
|
||||
|
@ -849,7 +850,7 @@ def toss_junk_item(world, player):
|
|||
|
||||
def set_trock_key_rules(world, player):
|
||||
# First set all relevant locked doors to impassible.
|
||||
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Pokey Room']:
|
||||
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Pokey Room', 'Turtle Rock Big Key Door']:
|
||||
set_rule(world.get_entrance(entrance, player), lambda state: False)
|
||||
|
||||
all_state = world.get_all_state(True)
|
||||
|
@ -877,28 +878,32 @@ def set_trock_key_rules(world, player):
|
|||
|
||||
# The following represent the common key rules.
|
||||
|
||||
# Big key door requires the big key, obviously. We removed this rule in the previous section to flag front_locked_locations correctly,
|
||||
# otherwise crystaroller room might not be properly marked as reachable through the back.
|
||||
set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player))
|
||||
|
||||
# No matter what, the key requirement for going from the middle to the bottom should be three keys.
|
||||
set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 3))
|
||||
set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
|
||||
|
||||
# Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we
|
||||
# might open all the locked doors in any order so we need maximally restrictive rules.
|
||||
if can_reach_back:
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state.has_key('Small Key (Turtle Rock)', player, 4) or item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 4))
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 4) or item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
|
||||
# Only consider wasting the key on the Trinexx door for going from the front entrance to middle section. If other key doors are accessible, then these doors can be avoided
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 3))
|
||||
set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2))
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
|
||||
set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
|
||||
else:
|
||||
# Middle to front requires 2 keys if the back is locked, otherwise 4
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2)
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2)
|
||||
if item_in_locations(state, 'Big Key (Turtle Rock)', player, front_locked_locations)
|
||||
else state.has_key('Small Key (Turtle Rock)', player, 4))
|
||||
else state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
|
||||
|
||||
# Front to middle requires 2 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted)
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2))
|
||||
set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 1))
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
|
||||
set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1))
|
||||
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
|
||||
|
||||
def tr_big_key_chest_keys_needed(state):
|
||||
# This function handles the key requirements for the TR Big Chest in the situations it having the Big Key should logically require 2 keys, small key
|
||||
|
@ -911,14 +916,14 @@ def set_trock_key_rules(world, player):
|
|||
return 4
|
||||
|
||||
# If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential
|
||||
if not can_reach_front and not world.keyshuffle[player]:
|
||||
if not can_reach_front and not world.smallkey_shuffle[player]:
|
||||
# Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests
|
||||
forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
|
||||
if not can_reach_big_chest:
|
||||
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
|
||||
forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
|
||||
if world.accessibility[player] == 'locations' and world.goal[player] != 'icerodhunt':
|
||||
if world.bigkeyshuffle[player] and can_reach_big_chest:
|
||||
if world.bigkey_shuffle[player] and can_reach_big_chest:
|
||||
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
|
||||
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
|
||||
'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right']:
|
||||
|
|
|
@ -6,6 +6,7 @@ import logging
|
|||
from worlds.alttp.SubClasses import ALttPLocation
|
||||
from worlds.alttp.EntranceShuffle import door_addresses
|
||||
from worlds.alttp.Items import item_name_groups, item_table, ItemFactory, trap_replaceable, GetBeemizerItem
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
from Utils import int16_as_bytes
|
||||
|
||||
logger = logging.getLogger("Shops")
|
||||
|
@ -22,6 +23,11 @@ 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
|
||||
slot_names: Dict[int, str] = {
|
||||
0: "Left",
|
||||
1: "Center",
|
||||
2: "Right"
|
||||
}
|
||||
|
||||
def __init__(self, region, room_id: int, shopkeeper_config: int, custom: bool, locked: bool, sram_offset: int):
|
||||
self.region = region
|
||||
|
@ -131,23 +137,22 @@ shop_class_mapping = {ShopType.UpgradeShop: UpgradeShop,
|
|||
|
||||
def FillDisabledShopSlots(world):
|
||||
shop_slots: Set[ALttPLocation] = {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_locations
|
||||
if location.shop_slot is not None 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 = ItemFactory(shop.inventory[location.shop_slot]['item'], location.player)
|
||||
location.item_rule = lambda item: item.name == location.item.name and item.player == location.player
|
||||
|
||||
|
||||
def ShopSlotFill(world):
|
||||
shop_slots: Set[ALttPLocation] = {location for shop_locations in (shop.region.locations for shop in world.shops)
|
||||
for location in shop_locations if location.shop_slot}
|
||||
for location in shop_locations if location.shop_slot is not None}
|
||||
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:
|
||||
if not shop.can_push_inventory(location.shop_slot) or location.shop_slot_disabled:
|
||||
location.shop_slot_disabled = True
|
||||
removed.add(location)
|
||||
|
||||
|
@ -155,6 +160,7 @@ def ShopSlotFill(world):
|
|||
shop_slots -= removed
|
||||
|
||||
if shop_slots:
|
||||
logger.info("Filling LttP Shop Slots")
|
||||
del shop_slots
|
||||
|
||||
from Fill import swap_location_item
|
||||
|
@ -179,7 +185,7 @@ def ShopSlotFill(world):
|
|||
shops_per_sphere.append(current_shops_slots)
|
||||
candidates_per_sphere.append(current_candidates)
|
||||
for location in sphere:
|
||||
if location.shop_slot:
|
||||
if location.shop_slot is not None:
|
||||
if not location.shop_slot_disabled:
|
||||
current_shops_slots.append(location)
|
||||
elif not location.locked and not location.item.name in blacklist_words:
|
||||
|
@ -229,7 +235,7 @@ def ShopSlotFill(world):
|
|||
else:
|
||||
price = world.random.randrange(8, 56)
|
||||
|
||||
shop.push_inventory(int(location.name[-1]) - 1, item_name, price * 5, 1,
|
||||
shop.push_inventory(location.shop_slot, item_name, price * 5, 1,
|
||||
location.item.player if location.item.player != location.player else 0)
|
||||
|
||||
|
||||
|
@ -266,7 +272,7 @@ def create_shops(world, player: int):
|
|||
# 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"]._replace(items=_inverted_hylia_shop_defaults, locked=None)
|
||||
chance_100 = int(world.retro[player])*0.25+int(world.keyshuffle[player] == "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():
|
||||
region = world.get_region(region_name, player)
|
||||
shop: Shop = shop_class_mapping[type](region, room_id, shopkeeper, custom, locked, sram_offset)
|
||||
|
@ -278,10 +284,10 @@ def create_shops(world, player: int):
|
|||
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)
|
||||
slot_name = f"{region.name} {shop.slot_names[index]}"
|
||||
loc = ALttPLocation(player, slot_name, address=shop_table_by_location[slot_name],
|
||||
parent=region, hint_text="for sale")
|
||||
loc.shop_slot = True
|
||||
loc.shop_slot = index
|
||||
loc.locked = True
|
||||
if single_purchase_slots.pop():
|
||||
if world.goal[player] != 'icerodhunt':
|
||||
|
@ -337,9 +343,10 @@ 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 = dict(enumerate(
|
||||
(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))
|
||||
|
||||
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"
|
||||
|
@ -365,13 +372,13 @@ def set_up_shops(world, player: int):
|
|||
rss = world.get_region('Red Shield Shop', player).shop
|
||||
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.
|
||||
if world.keyshuffle[player] == "universal":
|
||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
replacement_items.append(['Small Key (Universal)', 100])
|
||||
replacement_item = world.random.choice(replacement_items)
|
||||
rss.add_inventory(2, 'Single Arrow', 80, 1, replacement_item[0], replacement_item[1])
|
||||
rss.locked = True
|
||||
|
||||
if world.keyshuffle[player] == "universal" or world.retro[player]:
|
||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal or world.retro[player]:
|
||||
for shop in world.random.sample([s for s in world.shops if
|
||||
s.custom and not s.locked and s.type == ShopType.Shop and s.region.player == player],
|
||||
5):
|
||||
|
@ -379,7 +386,7 @@ def set_up_shops(world, player: int):
|
|||
slots = [0, 1, 2]
|
||||
world.random.shuffle(slots)
|
||||
slots = iter(slots)
|
||||
if world.keyshuffle[player] == "universal":
|
||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
shop.add_inventory(next(slots), 'Small Key (Universal)', 100)
|
||||
if world.retro[player]:
|
||||
shop.push_inventory(next(slots), 'Single Arrow', 80)
|
||||
|
|
|
@ -18,6 +18,7 @@ class ALttPLocation(Location):
|
|||
|
||||
class ALttPItem(Item):
|
||||
game: str = "A Link to the Past"
|
||||
dungeon = None
|
||||
|
||||
def __init__(self, name, player, advancement=False, type=None, item_code=None, pedestal_hint=None, pedestal_credit=None,
|
||||
sick_kid_credit=None, zora_credit=None, witch_credit=None, flute_boy_credit=None, hint_text=None):
|
||||
|
@ -29,4 +30,33 @@ class ALttPItem(Item):
|
|||
self.zora_credit_text = zora_credit
|
||||
self.magicshop_credit_text = witch_credit
|
||||
self.fluteboy_credit_text = flute_boy_credit
|
||||
self._hint_text = hint_text
|
||||
self._hint_text = hint_text
|
||||
|
||||
@property
|
||||
def crystal(self) -> bool:
|
||||
return self.type == 'Crystal'
|
||||
|
||||
@property
|
||||
def smallkey(self) -> bool:
|
||||
return self.type == 'SmallKey'
|
||||
|
||||
@property
|
||||
def bigkey(self) -> bool:
|
||||
return self.type == 'BigKey'
|
||||
|
||||
@property
|
||||
def map(self) -> bool:
|
||||
return self.type == 'Map'
|
||||
|
||||
@property
|
||||
def compass(self) -> bool:
|
||||
return self.type == 'Compass'
|
||||
|
||||
@property
|
||||
def dungeon_item(self) -> Optional[str]:
|
||||
if self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
|
||||
return self.type
|
||||
|
||||
@property
|
||||
def locked_dungeon_item(self):
|
||||
return self.location.locked and self.dungeon_item
|
|
@ -2,16 +2,17 @@ import random
|
|||
import logging
|
||||
import os
|
||||
import threading
|
||||
import typing
|
||||
|
||||
from BaseClasses import Item, CollectionState
|
||||
from .SubClasses import ALttPItem
|
||||
from ..AutoWorld import World
|
||||
from .Options import alttp_options
|
||||
from ..AutoWorld import World, LogicMixin
|
||||
from .Options import alttp_options, smallkey_shuffle
|
||||
from .Items import as_dict_item_table, item_name_groups, item_table
|
||||
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions
|
||||
from .Rules import set_rules
|
||||
from .ItemPool import generate_itempool
|
||||
from .Shops import create_shops
|
||||
from .ItemPool import generate_itempool, difficulties
|
||||
from .Shops import create_shops, ShopSlotFill
|
||||
from .Dungeons import create_dungeons
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, get_hash_string
|
||||
import Patch
|
||||
|
@ -21,28 +22,63 @@ from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_con
|
|||
|
||||
lttp_logger = logging.getLogger("A Link to the Past")
|
||||
|
||||
|
||||
class ALTTPWorld(World):
|
||||
game: str = "A Link to the Past"
|
||||
options = alttp_options
|
||||
topology_present = True
|
||||
item_name_groups = item_name_groups
|
||||
item_names = frozenset(item_table)
|
||||
location_names = frozenset(lookup_name_to_id)
|
||||
hint_blacklist = {"Triforce"}
|
||||
|
||||
item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int}
|
||||
location_name_to_id = lookup_name_to_id
|
||||
|
||||
data_version = 7
|
||||
data_version = 8
|
||||
remote_items: bool = False
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
create_items = generate_itempool
|
||||
|
||||
def create_regions(self):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.dungeon_local_item_names = set()
|
||||
self.dungeon_specific_item_names = set()
|
||||
self.rom_name_available_event = threading.Event()
|
||||
super(ALTTPWorld, self).__init__(*args, **kwargs)
|
||||
|
||||
def generate_early(self):
|
||||
player = self.player
|
||||
world = self.world
|
||||
|
||||
# system for sharing ER layouts
|
||||
world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
|
||||
|
||||
if "-" in world.shuffle[player]:
|
||||
shuffle, seed = world.shuffle[player].split("-", 1)
|
||||
world.shuffle[player] = shuffle
|
||||
if shuffle == "vanilla":
|
||||
world.er_seeds[player] = "vanilla"
|
||||
elif seed.startswith("group-") or world.is_race:
|
||||
world.er_seeds[player] = get_same_seed(world, (
|
||||
shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
|
||||
else: # not a race or group seed, use set seed as is.
|
||||
world.er_seeds[player] = seed
|
||||
elif world.shuffle[player] == "vanilla":
|
||||
world.er_seeds[player] = "vanilla"
|
||||
for dungeon_item in ["smallkey_shuffle", "bigkey_shuffle", "compass_shuffle", "map_shuffle"]:
|
||||
option = getattr(world, dungeon_item)[player]
|
||||
if option == "own_world":
|
||||
world.local_items[player] |= self.item_name_groups[option.item_name_group]
|
||||
elif option == "different_world":
|
||||
world.non_local_items[player] |= self.item_name_groups[option.item_name_group]
|
||||
elif option.in_dungeon:
|
||||
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
|
||||
if option == "original_dungeon":
|
||||
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group]
|
||||
|
||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||
|
||||
def create_regions(self):
|
||||
player = self.player
|
||||
world = self.world
|
||||
if world.open_pyramid[player] == 'goal':
|
||||
|
@ -67,7 +103,6 @@ class ALTTPWorld(World):
|
|||
create_shops(world, player)
|
||||
create_dungeons(world, player)
|
||||
|
||||
|
||||
if world.logic[player] not in ["noglitches", "minorglitches"] and world.shuffle[player] in \
|
||||
{"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}:
|
||||
world.fix_fake_world[player] = False
|
||||
|
@ -155,9 +190,11 @@ class ALTTPWorld(World):
|
|||
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
||||
return 'Blue Shield'
|
||||
elif 'Bow' in item_name:
|
||||
if state.has('Silver', item.player):
|
||||
if state.has('Silver Bow', item.player):
|
||||
return
|
||||
elif state.has('Bow', item.player) and self.world.difficulty_requirements[item.player].progressive_bow_limit >= 2:
|
||||
elif state.has('Bow', item.player) and (self.world.difficulty_requirements[item.player].progressive_bow_limit >= 2
|
||||
or self.world.logic[item.player] == 'noglitches'
|
||||
or self.world.swordless[item.player]): # modes where silver bow is always required for ganon
|
||||
return 'Silver Bow'
|
||||
elif self.world.difficulty_requirements[item.player].progressive_bow_limit >= 1:
|
||||
return 'Bow'
|
||||
|
@ -203,7 +240,11 @@ class ALTTPWorld(World):
|
|||
@classmethod
|
||||
def stage_pre_fill(cls, world):
|
||||
from .Dungeons import fill_dungeons_restrictive
|
||||
fill_dungeons_restrictive(world)
|
||||
fill_dungeons_restrictive(cls, world)
|
||||
|
||||
@classmethod
|
||||
def stage_post_fill(cls, world):
|
||||
ShopSlotFill(world)
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
world = self.world
|
||||
|
@ -279,11 +320,13 @@ class ALTTPWorld(World):
|
|||
return ALttPItem(name, self.player, **as_dict_item_table[name])
|
||||
|
||||
@classmethod
|
||||
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, restitempool, fill_locations):
|
||||
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
|
||||
restitempool, fill_locations):
|
||||
trash_counts = {}
|
||||
standard_keyshuffle_players = set()
|
||||
for player in world.get_game_players("A Link to the Past"):
|
||||
if world.mode[player] == 'standard' and world.keyshuffle[player] is True:
|
||||
if world.mode[player] == 'standard' and world.smallkey_shuffle[player] \
|
||||
and world.smallkey_shuffle[player] != smallkey_shuffle.option_universal:
|
||||
standard_keyshuffle_players.add(player)
|
||||
if not world.ganonstower_vanilla[player] or \
|
||||
world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
|
||||
|
@ -341,3 +384,21 @@ class ALTTPWorld(World):
|
|||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
fill_locations.remove(spot_to_fill) # very slow, unfortunately
|
||||
trash_count -= 1
|
||||
|
||||
|
||||
def get_same_seed(world, seed_def: tuple) -> str:
|
||||
seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {})
|
||||
if seed_def in seeds:
|
||||
return seeds[seed_def]
|
||||
seeds[seed_def] = str(world.random.randint(0, 2 ** 64))
|
||||
world.__named_seeds = seeds
|
||||
return seeds[seed_def]
|
||||
|
||||
|
||||
class ALttPLogic(LogicMixin):
|
||||
def _lttp_has_key(self, item, player, count: int = 1):
|
||||
if self.world.logic[player] == 'nologic':
|
||||
return True
|
||||
if self.world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
return self.can_buy_unlimited('Small Key (Universal)', player)
|
||||
return self.prog_items[item, player] >= count
|
|
@ -184,10 +184,7 @@ class Factorio(World):
|
|||
max_energy = remaining_energy * 0.75
|
||||
min_energy = (remaining_energy - max_energy) / remaining_num_ingredients
|
||||
ingredient = pool.pop()
|
||||
if ingredient not in recipes:
|
||||
logging.warning(f"missing recipe for {ingredient}")
|
||||
continue
|
||||
ingredient_recipe = recipes[ingredient]
|
||||
ingredient_recipe = min(all_product_sources[ingredient], key=lambda recipe: recipe.rel_cost)
|
||||
ingredient_raw = sum((count for ingredient, count in ingredient_recipe.base_cost.items()))
|
||||
ingredient_energy = ingredient_recipe.total_energy
|
||||
min_num_raw = min_raw/ingredient_raw
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
from typing import NamedTuple, Union
|
||||
import logging
|
||||
|
||||
from ..AutoWorld import World
|
||||
|
||||
|
||||
class GenericWorld(World):
|
||||
game = "Archipelago"
|
||||
topology_present = False
|
||||
item_name_to_id = {
|
||||
"Nothing": -1
|
||||
}
|
||||
location_name_to_id = {
|
||||
"Cheat Console" : -1,
|
||||
"Server": -2
|
||||
}
|
||||
hidden = True
|
||||
|
||||
class PlandoItem(NamedTuple):
|
||||
item: str
|
||||
|
|
|
@ -19,6 +19,8 @@ class HKWorld(World):
|
|||
item_name_to_id = {name: data.id for name, data in item_table.items() if data.type != "Event"}
|
||||
location_name_to_id = lookup_name_to_id
|
||||
|
||||
hidden = True
|
||||
|
||||
def generate_basic(self):
|
||||
# Link regions
|
||||
self.world.get_entrance('Hollow Nest S&Q', self.player).connect(self.world.get_region('Hollow Nest', self.player))
|
||||
|
|
|
@ -58,6 +58,7 @@ item_table = {
|
|||
"Dragon Egg Shard": ItemData(45043, True),
|
||||
"Bee Trap (Minecraft)": ItemData(45100, False),
|
||||
|
||||
"Blaze Rods": ItemData(None, True),
|
||||
"Victory": ItemData(None, True)
|
||||
}
|
||||
|
||||
|
|
|
@ -109,6 +109,7 @@ advancement_table = {
|
|||
"Librarian": AdvData(42090, 'Overworld'),
|
||||
"Overpowered": AdvData(42091, 'Overworld'),
|
||||
|
||||
"Blaze Spawner": AdvData(None, 'Nether Fortress'),
|
||||
"Ender Dragon": AdvData(None, 'The End')
|
||||
}
|
||||
|
||||
|
|
|
@ -78,13 +78,13 @@ mandatory_connections = [
|
|||
('End Portal', 'The End')
|
||||
]
|
||||
|
||||
default_connections = {
|
||||
default_connections = [
|
||||
('Overworld Structure 1', 'Village'),
|
||||
('Overworld Structure 2', 'Pillager Outpost'),
|
||||
('Nether Structure 1', 'Nether Fortress'),
|
||||
('Nether Structure 2', 'Bastion Remnant'),
|
||||
('The End Structure', 'End City')
|
||||
}
|
||||
]
|
||||
|
||||
# Structure: illegal locations
|
||||
illegal_connections = {
|
||||
|
|
|
@ -31,7 +31,7 @@ class MinecraftLogic(LogicMixin):
|
|||
return self.can_reach('Nether Fortress', 'Region', player) and self._mc_basic_combat(player)
|
||||
|
||||
def _mc_can_brew_potions(self, player: int):
|
||||
return self._mc_fortress_loot(player) and self.has('Brewing', player) and self._mc_has_bottle(player)
|
||||
return self.has('Blaze Rods', player) and self.has('Brewing', player) and self._mc_has_bottle(player)
|
||||
|
||||
def _mc_can_piglin_trade(self, player: int):
|
||||
return self._mc_has_gold_ingots(player) and (
|
||||
|
@ -39,7 +39,7 @@ class MinecraftLogic(LogicMixin):
|
|||
player))
|
||||
|
||||
def _mc_enter_stronghold(self, player: int):
|
||||
return self._mc_fortress_loot(player) and self.has('Brewing', player) and self.has('3 Ender Pearls', player)
|
||||
return self.has('Blaze Rods', player) and self.has('Brewing', player) and self.has('3 Ender Pearls', player)
|
||||
|
||||
# Difficulty-dependent functions
|
||||
def _mc_combat_difficulty(self, player: int):
|
||||
|
@ -135,6 +135,7 @@ def set_rules(world: MultiWorld, player: int):
|
|||
set_rule(world.get_entrance("The End Structure", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("The End Structure", player))
|
||||
|
||||
set_rule(world.get_location("Ender Dragon", player), lambda state: can_complete(state))
|
||||
set_rule(world.get_location("Blaze Spawner", player), lambda state: state._mc_fortress_loot(player))
|
||||
|
||||
set_rule(world.get_location("Who is Cutting Onions?", player), lambda state: state._mc_can_piglin_trade(player))
|
||||
set_rule(world.get_location("Oh Shiny", player), lambda state: state._mc_can_piglin_trade(player))
|
||||
|
|
|
@ -72,9 +72,9 @@ class MinecraftWorld(World):
|
|||
exclusion_pool.update(exclusion_table[key])
|
||||
exclusion_rules(self.world, self.player, exclusion_pool)
|
||||
|
||||
# Prefill the Ender Dragon with the completion condition
|
||||
completion = self.create_item("Victory")
|
||||
self.world.get_location("Ender Dragon", self.player).place_locked_item(completion)
|
||||
# Prefill event locations with their events
|
||||
self.world.get_location("Blaze Spawner", self.player).place_locked_item(self.create_item("Blaze Rods"))
|
||||
self.world.get_location("Ender Dragon", self.player).place_locked_item(self.create_item("Victory"))
|
||||
|
||||
self.world.itempool += itempool
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@ class OriBlindForest(World):
|
|||
|
||||
options = options
|
||||
|
||||
hidden = True
|
||||
|
||||
def generate_early(self):
|
||||
logic_sets = {"casual-core"}
|
||||
for logic_set in location_rules:
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import typing
|
||||
|
||||
from BaseClasses import Item
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
code: typing.Optional[int]
|
||||
progression: bool
|
||||
event: bool = False
|
||||
|
||||
|
||||
item_table: Dict[str, ItemData] = {
|
||||
'Card Draw': ItemData(8000, True),
|
||||
'Rare Card Draw': ItemData(8001, True),
|
||||
'Relic': ItemData(8002, True),
|
||||
'Boss Relic': ItemData(8003, True),
|
||||
|
||||
# Event Items
|
||||
'Victory': ItemData(None, True, True),
|
||||
'Beat Act 1 Boss': ItemData(None, True, True),
|
||||
'Beat Act 2 Boss': ItemData(None, True, True),
|
||||
'Beat Act 3 Boss': ItemData(None, True, True),
|
||||
|
||||
}
|
||||
|
||||
item_pool: Dict[str, int] = {
|
||||
'Card Draw': 15,
|
||||
'Rare Card Draw': 3,
|
||||
'Relic': 10,
|
||||
'Boss Relic': 3
|
||||
}
|
||||
|
||||
event_item_pairs: Dict[str, str] = {
|
||||
"Heart Room": "Victory",
|
||||
"Act 1 Boss": "Beat Act 1 Boss",
|
||||
"Act 2 Boss": "Beat Act 2 Boss",
|
||||
"Act 3 Boss": "Beat Act 3 Boss"
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
location_table = {
|
||||
'Card Draw 1': 19001,
|
||||
'Card Draw 2': 19002,
|
||||
'Card Draw 3': 19003,
|
||||
'Card Draw 4': 19004,
|
||||
'Card Draw 5': 19005,
|
||||
'Card Draw 6': 19006,
|
||||
'Card Draw 7': 19007,
|
||||
'Card Draw 8': 19008,
|
||||
'Card Draw 9': 19009,
|
||||
'Card Draw 10': 19010,
|
||||
'Card Draw 11': 19011,
|
||||
'Card Draw 12': 19012,
|
||||
'Card Draw 13': 19013,
|
||||
'Card Draw 14': 19014,
|
||||
'Card Draw 15': 19015,
|
||||
'Rare Card Draw 1': 21001,
|
||||
'Rare Card Draw 2': 21002,
|
||||
'Rare Card Draw 3': 21003,
|
||||
'Relic 1': 20001,
|
||||
'Relic 2': 20002,
|
||||
'Relic 3': 20003,
|
||||
'Relic 4': 20004,
|
||||
'Relic 5': 20005,
|
||||
'Relic 6': 20006,
|
||||
'Relic 7': 20007,
|
||||
'Relic 8': 20008,
|
||||
'Relic 9': 20009,
|
||||
'Relic 10': 20010,
|
||||
'Boss Relic 1': 22001,
|
||||
'Boss Relic 2': 22002,
|
||||
'Boss Relic 3': 22003,
|
||||
'Heart Room': None,
|
||||
'Act 1 Boss': None,
|
||||
'Act 2 Boss': None,
|
||||
'Act 3 Boss': None
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import typing
|
||||
from Options import Choice, Option, Range, Toggle
|
||||
|
||||
|
||||
class Character(Choice):
|
||||
"""Pick What Character you wish to play with."""
|
||||
display_name = "Character"
|
||||
option_ironclad = 0
|
||||
option_silent = 1
|
||||
option_defect = 2
|
||||
option_watcher = 3
|
||||
default = 0
|
||||
|
||||
|
||||
class Ascension(Range):
|
||||
"""What Ascension do you wish to play with."""
|
||||
display_name = "Ascension"
|
||||
range_start = 0
|
||||
range_end = 20
|
||||
default = 0
|
||||
|
||||
|
||||
class HeartRun(Toggle):
|
||||
"""Whether or not you will need to collect they 3 keys to unlock the final act
|
||||
and beat the heart to finish the game."""
|
||||
display_name = "Heart Run"
|
||||
option_true = 1
|
||||
option_false = 0
|
||||
default = 0
|
||||
|
||||
|
||||
spire_options: typing.Dict[str, type(Option)] = {
|
||||
"character": Character,
|
||||
"ascension": Ascension,
|
||||
"heart_run": HeartRun
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
def create_regions(world, player: int):
|
||||
from . import create_region
|
||||
from .Locations import location_table
|
||||
|
||||
world.regions += [
|
||||
create_region(world, player, 'Menu', None, ['Neow\'s Room']),
|
||||
create_region(world, player, 'The Spire', [location for location in location_table])
|
||||
]
|
||||
|
||||
# link up our region with the entrance we just made
|
||||
world.get_entrance('Neow\'s Room', player).connect(world.get_region('The Spire', player))
|
|
@ -0,0 +1,76 @@
|
|||
from BaseClasses import MultiWorld
|
||||
from ..AutoWorld import LogicMixin
|
||||
from ..generic.Rules import set_rule
|
||||
|
||||
|
||||
class SpireLogic(LogicMixin):
|
||||
def _spire_has_relics(self, player: int, amount: int) -> bool:
|
||||
count: int = self.item_count("Relic", player) + self.item_count("Boss Relic", player)
|
||||
return count >= amount
|
||||
|
||||
def _spire_has_cards(self, player: int, amount: int) -> bool:
|
||||
count = self.item_count("Card Draw", player) + self.item_count("Rare Card Draw", player)
|
||||
return count >= amount
|
||||
|
||||
|
||||
def set_rules(world: MultiWorld, player: int):
|
||||
|
||||
# Act 1 Card Draws
|
||||
set_rule(world.get_location("Card Draw 1", player), lambda state: True)
|
||||
set_rule(world.get_location("Card Draw 2", player), lambda state: True)
|
||||
set_rule(world.get_location("Card Draw 3", player), lambda state: True)
|
||||
set_rule(world.get_location("Card Draw 4", player), lambda state: state._spire_has_relics(player, 1))
|
||||
set_rule(world.get_location("Card Draw 5", player), lambda state: state._spire_has_relics(player, 1))
|
||||
|
||||
# Act 1 Relics
|
||||
set_rule(world.get_location("Relic 1", player), lambda state: state._spire_has_cards(player, 1))
|
||||
set_rule(world.get_location("Relic 2", player), lambda state: state._spire_has_cards(player, 2))
|
||||
set_rule(world.get_location("Relic 3", player), lambda state: state._spire_has_cards(player, 2))
|
||||
|
||||
# Act 1 Boss Event
|
||||
set_rule(world.get_location("Act 1 Boss", player), lambda state: state._spire_has_cards(player, 3) and state._spire_has_relics(player, 2))
|
||||
|
||||
# Act 1 Boss Rewards
|
||||
set_rule(world.get_location("Rare Card Draw 1", player), lambda state: state.has("Beat Act 1 Boss", player))
|
||||
set_rule(world.get_location("Boss Relic 1", player), lambda state: state.has("Beat Act 1 Boss", player))
|
||||
|
||||
# Act 2 Card Draws
|
||||
set_rule(world.get_location("Card Draw 6", player), lambda state: state.has("Beat Act 1 Boss", player))
|
||||
set_rule(world.get_location("Card Draw 7", player), lambda state: state.has("Beat Act 1 Boss", player))
|
||||
set_rule(world.get_location("Card Draw 8", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 6) and state._spire_has_relics(player, 3))
|
||||
set_rule(world.get_location("Card Draw 9", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 6) and state._spire_has_relics(player, 4))
|
||||
set_rule(world.get_location("Card Draw 10", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 7) and state._spire_has_relics(player, 4))
|
||||
|
||||
# Act 2 Relics
|
||||
set_rule(world.get_location("Relic 4", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 7) and state._spire_has_relics(player, 2))
|
||||
set_rule(world.get_location("Relic 5", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 7) and state._spire_has_relics(player, 2))
|
||||
set_rule(world.get_location("Relic 6", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 7) and state._spire_has_relics(player, 3))
|
||||
|
||||
# Act 2 Boss Event
|
||||
set_rule(world.get_location("Act 2 Boss", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 7) and state._spire_has_relics(player, 4) and state.has("Boss Relic", player))
|
||||
|
||||
# Act 2 Boss Rewards
|
||||
set_rule(world.get_location("Rare Card Draw 2", player), lambda state: state.has("Beat Act 2 Boss", player))
|
||||
set_rule(world.get_location("Boss Relic 2", player), lambda state: state.has("Beat Act 2 Boss", player))
|
||||
|
||||
# Act 3 Card Draws
|
||||
set_rule(world.get_location("Card Draw 11", player), lambda state: state.has("Beat Act 2 Boss", player))
|
||||
set_rule(world.get_location("Card Draw 12", player), lambda state: state.has("Beat Act 2 Boss", player))
|
||||
set_rule(world.get_location("Card Draw 13", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 4))
|
||||
set_rule(world.get_location("Card Draw 14", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 4))
|
||||
set_rule(world.get_location("Card Draw 15", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 4))
|
||||
|
||||
# Act 3 Relics
|
||||
set_rule(world.get_location("Relic 7", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 4))
|
||||
set_rule(world.get_location("Relic 8", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 5))
|
||||
set_rule(world.get_location("Relic 9", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 5))
|
||||
set_rule(world.get_location("Relic 10", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 5))
|
||||
|
||||
# Act 3 Boss Event
|
||||
set_rule(world.get_location("Act 3 Boss", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 7) and state.has("Boss Relic", player, 2))
|
||||
|
||||
# Act 3 Boss Rewards
|
||||
set_rule(world.get_location("Rare Card Draw 3", player), lambda state: state.has("Beat Act 3 Boss", player))
|
||||
set_rule(world.get_location("Boss Relic 3", player), lambda state: state.has("Beat Act 3 Boss", player))
|
||||
|
||||
set_rule(world.get_location("Heart Room", player), lambda state: state.has("Beat Act 3 Boss", player))
|
|
@ -0,0 +1,110 @@
|
|||
import string
|
||||
|
||||
from BaseClasses import Item, MultiWorld, Region, Location, Entrance
|
||||
from .Items import item_table, item_pool, event_item_pairs
|
||||
from .Locations import location_table
|
||||
from .Regions import create_regions
|
||||
from .Rules import set_rules
|
||||
from ..AutoWorld import World
|
||||
from .Options import spire_options
|
||||
|
||||
|
||||
class SpireWorld(World):
|
||||
options = spire_options
|
||||
game = "Slay the Spire"
|
||||
topology_present = False
|
||||
data_version = 1
|
||||
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = location_table
|
||||
|
||||
def _get_slot_data(self):
|
||||
return {
|
||||
'seed': "".join(self.world.slot_seeds[self.player].choice(string.ascii_letters) for i in range(16)),
|
||||
'character': self.world.character[self.player],
|
||||
'ascension': self.world.ascension[self.player],
|
||||
'heart_run': self.world.heart_run[self.player]
|
||||
}
|
||||
|
||||
def generate_basic(self):
|
||||
# Fill out our pool with our items from item_pool, assuming 1 item if not present in item_pool
|
||||
pool = []
|
||||
for name, data in item_table.items():
|
||||
if not data.event:
|
||||
if name in item_pool:
|
||||
card_draw = 0
|
||||
for amount in range(item_pool[name]):
|
||||
item = SpireItem(name, self.player)
|
||||
|
||||
# This feels wrong but it makes our failure rate drop dramatically
|
||||
# makes all but 7 basic card draws trash fill
|
||||
if item.name == "Card Draw":
|
||||
card_draw += 1
|
||||
if card_draw > 7:
|
||||
item.advancement = False
|
||||
|
||||
pool.append(item)
|
||||
else:
|
||||
item = SpireItem(name, self.player)
|
||||
pool.append(item)
|
||||
|
||||
self.world.itempool += pool
|
||||
|
||||
# Pair up our event locations with our event items
|
||||
for event, item in event_item_pairs.items():
|
||||
event_item = SpireItem(item, self.player)
|
||||
self.world.get_location(event, self.player).place_locked_item(event_item)
|
||||
|
||||
if self.world.logic[self.player] != 'no logic':
|
||||
self.world.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self.world, self.player)
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
item_data = item_table[name]
|
||||
return Item(name, item_data.progression, item_data.code, self.player)
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self.world, self.player)
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
slot_data = self._get_slot_data()
|
||||
for option_name in spire_options:
|
||||
option = getattr(self.world, option_name)[self.player]
|
||||
slot_data[option_name] = int(option.value)
|
||||
return slot_data
|
||||
|
||||
|
||||
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
ret = Region(name, None, name, player)
|
||||
ret.world = world
|
||||
if locations:
|
||||
for location in locations:
|
||||
loc_id = location_table.get(location, 0)
|
||||
location = SpireLocation(player, location, loc_id, ret)
|
||||
ret.locations.append(location)
|
||||
if exits:
|
||||
for exit in exits:
|
||||
ret.exits.append(Entrance(player, exit, ret))
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class SpireLocation(Location):
|
||||
game: str = "Slay the Spire"
|
||||
|
||||
def __init__(self, player: int, name: str, address=None, parent=None):
|
||||
super(SpireLocation, self).__init__(player, name, address, parent)
|
||||
if address is None:
|
||||
self.event = True
|
||||
self.locked = True
|
||||
|
||||
|
||||
class SpireItem(Item):
|
||||
game = "Slay the Spire"
|
||||
|
||||
def __init__(self, name, player: int = None):
|
||||
item_data = item_table[name]
|
||||
super(SpireItem, self).__init__(name, item_data.progression, item_data.code, player)
|
Loading…
Reference in New Issue