mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-03 13:53:40 -07:00
Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b57306beac | ||
|
|
af6e159644 | ||
|
|
54e50f69e1 | ||
|
|
3f415b8265 | ||
|
|
8ccdb56bf1 | ||
|
|
17ed957c6b | ||
|
|
e4564abe41 | ||
|
|
f16b29b16b | ||
|
|
ef8af7d618 | ||
|
|
79e33899a8 | ||
|
|
11fc220d4d | ||
|
|
a94a30168c | ||
|
|
19704920a4 | ||
|
|
e4f4c1f1be | ||
|
|
065931cae7 | ||
|
|
78443bffac | ||
|
|
a8b105267c | ||
|
|
f7bd637073 | ||
|
|
3e6f7f0fad | ||
|
|
e301b67e49 | ||
|
|
952d878442 | ||
|
|
8f66f94ffa | ||
|
|
e66a2a7c30 | ||
|
|
96ffe95404 | ||
|
|
438e53d25e | ||
|
|
ca4b0acd92 | ||
|
|
f8deb1bd7f | ||
|
|
d8de84e417 | ||
|
|
eb602aedc3 | ||
|
|
b539892cc0 | ||
|
|
ba13d2179d | ||
|
|
c7a315ac97 | ||
|
|
b1fb793ea4 | ||
|
|
62db9ad982 | ||
|
|
d3780cd9d5 | ||
|
|
6acd08431e | ||
|
|
76d591bab5 | ||
|
|
d10cab824a | ||
|
|
a93d633d25 | ||
|
|
9ebab4a382 | ||
|
|
cd53dcfe43 | ||
|
|
1985423a97 | ||
|
|
f5afc84cd2 | ||
|
|
1217179f8a | ||
|
|
29a207b73e | ||
|
|
f7ecf02beb | ||
|
|
c5193ffdd9 | ||
|
|
916ba2ea41 | ||
|
|
3348dce122 | ||
|
|
53e6ca6e34 | ||
|
|
0fed7f1295 | ||
|
|
6ade832029 | ||
|
|
50ba9a56f7 | ||
|
|
990141df47 | ||
|
|
50f7541ef7 | ||
|
|
6a6962b3b9 | ||
|
|
3314ad0315 | ||
|
|
9a4a96eedd | ||
|
|
ff4a9d1761 | ||
|
|
df2d4a557e | ||
|
|
d831923a54 | ||
|
|
594183d751 | ||
|
|
bddaa954ab | ||
|
|
f4a7777018 | ||
|
|
8fc9a9c55e | ||
|
|
8e457d9b8f | ||
|
|
aa37c9bf81 | ||
|
|
89cbd05600 | ||
|
|
a5e9c4af03 | ||
|
|
ea753cd8bf | ||
|
|
46e9fd7ae3 | ||
|
|
96d7277a22 | ||
|
|
c937167a11 | ||
|
|
0c59ad7e22 | ||
|
|
fa1b93252c | ||
|
|
0d9e186e18 | ||
|
|
b7aa5a17b7 | ||
|
|
d55a057a4d | ||
|
|
72976da3a4 | ||
|
|
81afbb55cf | ||
|
|
d1709764ef | ||
|
|
4f7e3d7a45 | ||
|
|
4ca53a6ee0 | ||
|
|
efe02e2591 | ||
|
|
391f42b4f2 | ||
|
|
cff5db446d | ||
|
|
858d4c74ce | ||
|
|
4801bb1178 | ||
|
|
8b2433584d | ||
|
|
bde02f696b | ||
|
|
0afbe7988e | ||
|
|
345d4c58f3 | ||
|
|
6c44ffaf7a | ||
|
|
16454dbc33 | ||
|
|
89c6fd6ac4 | ||
|
|
ea8b6e6438 | ||
|
|
c0b25e1f6e | ||
|
|
df0335f739 | ||
|
|
1ffe5fc7bb | ||
|
|
cf070e6dd9 | ||
|
|
f9a9189687 | ||
|
|
9daf1abcd9 | ||
|
|
8c525a5e33 | ||
|
|
952a155003 | ||
|
|
7f35f6f8f4 | ||
|
|
8b9e278593 | ||
|
|
655ebcdb07 | ||
|
|
ac534a6881 | ||
|
|
59529eba4e | ||
|
|
c0ff90fc86 | ||
|
|
f9c8816c43 | ||
|
|
73727ab0d1 | ||
|
|
4ed185a155 | ||
|
|
fbb220ce85 | ||
|
|
0c57d35402 | ||
|
|
8cc045f370 | ||
|
|
4d68000692 | ||
|
|
44b5423afc | ||
|
|
fc62b4e0bd |
203
BaseClasses.py
203
BaseClasses.py
@@ -25,9 +25,9 @@ class MultiWorld():
|
|||||||
plando_texts: List[Dict[str, str]]
|
plando_texts: List[Dict[str, str]]
|
||||||
plando_items: List
|
plando_items: List
|
||||||
plando_connections: List
|
plando_connections: List
|
||||||
er_seeds: Dict[int, str]
|
|
||||||
worlds: Dict[int, Any]
|
worlds: Dict[int, Any]
|
||||||
is_race: bool = False
|
is_race: bool = False
|
||||||
|
precollected_items: Dict[int, List[Item]]
|
||||||
|
|
||||||
class AttributeProxy():
|
class AttributeProxy():
|
||||||
def __init__(self, rule):
|
def __init__(self, rule):
|
||||||
@@ -47,7 +47,7 @@ class MultiWorld():
|
|||||||
self.itempool = []
|
self.itempool = []
|
||||||
self.seed = None
|
self.seed = None
|
||||||
self.seed_name: str = "Unavailable"
|
self.seed_name: str = "Unavailable"
|
||||||
self.precollected_items = []
|
self.precollected_items = {player: [] for player in self.player_ids}
|
||||||
self.state = CollectionState(self)
|
self.state = CollectionState(self)
|
||||||
self._cached_entrances = None
|
self._cached_entrances = None
|
||||||
self._cached_locations = None
|
self._cached_locations = None
|
||||||
@@ -66,15 +66,20 @@ class MultiWorld():
|
|||||||
self.dynamic_regions = []
|
self.dynamic_regions = []
|
||||||
self.dynamic_locations = []
|
self.dynamic_locations = []
|
||||||
self.spoiler = Spoiler(self)
|
self.spoiler = Spoiler(self)
|
||||||
self.fix_trock_doors = self.AttributeProxy(lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
|
self.fix_trock_doors = self.AttributeProxy(
|
||||||
self.fix_skullwoods_exit = self.AttributeProxy(lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
|
||||||
self.fix_palaceofdarkness_exit = self.AttributeProxy(lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
self.fix_skullwoods_exit = self.AttributeProxy(
|
||||||
self.fix_trock_exit = self.AttributeProxy(lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||||
|
self.fix_palaceofdarkness_exit = self.AttributeProxy(
|
||||||
|
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||||
|
self.fix_trock_exit = self.AttributeProxy(
|
||||||
|
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||||
self.NOTCURSED = self.AttributeProxy(lambda player: not self.CURSED[player])
|
self.NOTCURSED = self.AttributeProxy(lambda player: not self.CURSED[player])
|
||||||
|
|
||||||
for player in range(1, players + 1):
|
for player in range(1, players + 1):
|
||||||
def set_player_attr(attr, val):
|
def set_player_attr(attr, val):
|
||||||
self.__dict__.setdefault(attr, {})[player] = val
|
self.__dict__.setdefault(attr, {})[player] = val
|
||||||
|
|
||||||
set_player_attr('tech_tree_layout_prerequisites', {})
|
set_player_attr('tech_tree_layout_prerequisites', {})
|
||||||
set_player_attr('_region_cache', {})
|
set_player_attr('_region_cache', {})
|
||||||
set_player_attr('shuffle', "vanilla")
|
set_player_attr('shuffle', "vanilla")
|
||||||
@@ -122,6 +127,17 @@ class MultiWorld():
|
|||||||
set_player_attr('completion_condition', lambda state: True)
|
set_player_attr('completion_condition', lambda state: True)
|
||||||
self.custom_data = {}
|
self.custom_data = {}
|
||||||
self.worlds = {}
|
self.worlds = {}
|
||||||
|
self.slot_seeds = {}
|
||||||
|
|
||||||
|
def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
|
||||||
|
self.seed = get_seed(seed)
|
||||||
|
if secure:
|
||||||
|
self.secure()
|
||||||
|
else:
|
||||||
|
self.random.seed(self.seed)
|
||||||
|
self.seed_name = name if name else str(self.seed)
|
||||||
|
self.slot_seeds = {player: random.Random(self.random.getrandbits(64)) for player in
|
||||||
|
range(1, self.players + 1)}
|
||||||
|
|
||||||
def set_options(self, args):
|
def set_options(self, args):
|
||||||
from worlds import AutoWorld
|
from worlds import AutoWorld
|
||||||
@@ -251,7 +267,7 @@ class MultiWorld():
|
|||||||
|
|
||||||
def push_precollected(self, item: Item):
|
def push_precollected(self, item: Item):
|
||||||
item.world = self
|
item.world = self
|
||||||
self.precollected_items.append(item)
|
self.precollected_items[item.player].append(item)
|
||||||
self.state.collect(item, True)
|
self.state.collect(item, True)
|
||||||
|
|
||||||
def push_item(self, location: Location, item: Item, collect: bool = True):
|
def push_item(self, location: Location, item: Item, collect: bool = True):
|
||||||
@@ -270,7 +286,7 @@ class MultiWorld():
|
|||||||
else:
|
else:
|
||||||
raise RuntimeError('Cannot assign item %s to location %s.' % (item, location))
|
raise RuntimeError('Cannot assign item %s to location %s.' % (item, location))
|
||||||
|
|
||||||
def get_entrances(self) -> list:
|
def get_entrances(self) -> List[Entrance]:
|
||||||
if self._cached_entrances is None:
|
if self._cached_entrances is None:
|
||||||
self._cached_entrances = [entrance for region in self.regions for entrance in region.entrances]
|
self._cached_entrances = [entrance for region in self.regions for entrance in region.entrances]
|
||||||
return self._cached_entrances
|
return self._cached_entrances
|
||||||
@@ -278,7 +294,7 @@ class MultiWorld():
|
|||||||
def clear_entrance_cache(self):
|
def clear_entrance_cache(self):
|
||||||
self._cached_entrances = None
|
self._cached_entrances = None
|
||||||
|
|
||||||
def get_locations(self) -> list:
|
def get_locations(self) -> List[Location]:
|
||||||
if self._cached_locations is None:
|
if self._cached_locations is None:
|
||||||
self._cached_locations = [location for region in self.regions for location in region.locations]
|
self._cached_locations = [location for region in self.regions for location in region.locations]
|
||||||
return self._cached_locations
|
return self._cached_locations
|
||||||
@@ -286,7 +302,7 @@ class MultiWorld():
|
|||||||
def clear_location_cache(self):
|
def clear_location_cache(self):
|
||||||
self._cached_locations = None
|
self._cached_locations = None
|
||||||
|
|
||||||
def get_unfilled_locations(self, player=None) -> list:
|
def get_unfilled_locations(self, player=None) -> List[Location]:
|
||||||
if player is not None:
|
if player is not None:
|
||||||
return [location for location in self.get_locations() if
|
return [location for location in self.get_locations() if
|
||||||
location.player == player and not location.item]
|
location.player == player and not location.item]
|
||||||
@@ -295,19 +311,19 @@ class MultiWorld():
|
|||||||
def get_unfilled_dungeon_locations(self):
|
def get_unfilled_dungeon_locations(self):
|
||||||
return [location for location in self.get_locations() if not location.item and location.parent_region.dungeon]
|
return [location for location in self.get_locations() if not location.item and location.parent_region.dungeon]
|
||||||
|
|
||||||
def get_filled_locations(self, player=None) -> list:
|
def get_filled_locations(self, player=None) -> List[Location]:
|
||||||
if player is not None:
|
if player is not None:
|
||||||
return [location for location in self.get_locations() if
|
return [location for location in self.get_locations() if
|
||||||
location.player == player and location.item is not None]
|
location.player == player and location.item is not None]
|
||||||
return [location for location in self.get_locations() if location.item is not None]
|
return [location for location in self.get_locations() if location.item is not None]
|
||||||
|
|
||||||
def get_reachable_locations(self, state=None, player=None) -> list:
|
def get_reachable_locations(self, state=None, player=None) -> List[Location]:
|
||||||
if state is None:
|
if state is None:
|
||||||
state = self.state
|
state = self.state
|
||||||
return [location for location in self.get_locations() if
|
return [location for location in self.get_locations() if
|
||||||
(player is None or location.player == player) and location.can_reach(state)]
|
(player is None or location.player == player) and location.can_reach(state)]
|
||||||
|
|
||||||
def get_placeable_locations(self, state=None, player=None) -> list:
|
def get_placeable_locations(self, state=None, player=None) -> List[Location]:
|
||||||
if state is None:
|
if state is None:
|
||||||
state = self.state
|
state = self.state
|
||||||
return [location for location in self.get_locations() if
|
return [location for location in self.get_locations() if
|
||||||
@@ -335,7 +351,7 @@ class MultiWorld():
|
|||||||
else:
|
else:
|
||||||
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
|
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
|
||||||
|
|
||||||
def can_beat_game(self, starting_state : Optional[CollectionState]=None):
|
def can_beat_game(self, starting_state: Optional[CollectionState] = None):
|
||||||
if starting_state:
|
if starting_state:
|
||||||
if self.has_beaten_game(starting_state):
|
if self.has_beaten_game(starting_state):
|
||||||
return True
|
return True
|
||||||
@@ -392,7 +408,7 @@ class MultiWorld():
|
|||||||
"""Check if accessibility rules are fulfilled with current or supplied state."""
|
"""Check if accessibility rules are fulfilled with current or supplied state."""
|
||||||
if not state:
|
if not state:
|
||||||
state = CollectionState(self)
|
state = CollectionState(self)
|
||||||
players = {"minimal" : set(),
|
players = {"minimal": set(),
|
||||||
"items": set(),
|
"items": set(),
|
||||||
"locations": set()}
|
"locations": set()}
|
||||||
for player, access in self.accessibility.items():
|
for player, access in self.accessibility.items():
|
||||||
@@ -400,13 +416,13 @@ class MultiWorld():
|
|||||||
|
|
||||||
beatable_fulfilled = False
|
beatable_fulfilled = False
|
||||||
|
|
||||||
def location_conditition(location : Location):
|
def location_conditition(location: Location):
|
||||||
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
|
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
|
||||||
if location.player in players["minimal"]:
|
if location.player in players["minimal"]:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def location_relevant(location : Location):
|
def location_relevant(location: Location):
|
||||||
"""Determine if this location is relevant to sweep."""
|
"""Determine if this location is relevant to sweep."""
|
||||||
if location.player in players["locations"] or location.event or \
|
if location.player in players["locations"] or location.event or \
|
||||||
(location.item and location.item.advancement):
|
(location.item and location.item.advancement):
|
||||||
@@ -458,8 +474,9 @@ class CollectionState(object):
|
|||||||
self.path = {}
|
self.path = {}
|
||||||
self.locations_checked = set()
|
self.locations_checked = set()
|
||||||
self.stale = {player: True for player in range(1, parent.players + 1)}
|
self.stale = {player: True for player in range(1, parent.players + 1)}
|
||||||
for item in parent.precollected_items:
|
for items in parent.precollected_items.values():
|
||||||
self.collect(item, True)
|
for item in items:
|
||||||
|
self.collect(item, True)
|
||||||
|
|
||||||
def update_reachable_regions(self, player: int):
|
def update_reachable_regions(self, player: int):
|
||||||
from worlds.alttp.EntranceShuffle import indirect_connections
|
from worlds.alttp.EntranceShuffle import indirect_connections
|
||||||
@@ -499,7 +516,8 @@ class CollectionState(object):
|
|||||||
ret.prog_items = self.prog_items.copy()
|
ret.prog_items = self.prog_items.copy()
|
||||||
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in
|
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in
|
||||||
range(1, self.world.players + 1)}
|
range(1, self.world.players + 1)}
|
||||||
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)}
|
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in
|
||||||
|
range(1, self.world.players + 1)}
|
||||||
ret.events = copy.copy(self.events)
|
ret.events = copy.copy(self.events)
|
||||||
ret.path = copy.copy(self.path)
|
ret.path = copy.copy(self.path)
|
||||||
ret.locations_checked = copy.copy(self.locations_checked)
|
ret.locations_checked = copy.copy(self.locations_checked)
|
||||||
@@ -535,10 +553,10 @@ class CollectionState(object):
|
|||||||
def has(self, item, player: int, count: int = 1):
|
def has(self, item, player: int, count: int = 1):
|
||||||
return self.prog_items[item, player] >= count
|
return self.prog_items[item, player] >= count
|
||||||
|
|
||||||
def has_all(self, items: Set[str], player:int):
|
def has_all(self, items: Set[str], player: int):
|
||||||
return all(self.prog_items[item, player] for item in items)
|
return all(self.prog_items[item, player] for item in items)
|
||||||
|
|
||||||
def has_any(self, items: Set[str], player:int):
|
def has_any(self, items: Set[str], player: int):
|
||||||
return any(self.prog_items[item, player] for item in items)
|
return any(self.prog_items[item, player] for item in items)
|
||||||
|
|
||||||
def has_group(self, item_name_group: str, player: int, count: int = 1):
|
def has_group(self, item_name_group: str, player: int, count: int = 1):
|
||||||
@@ -638,10 +656,10 @@ class CollectionState(object):
|
|||||||
self.is_not_bunny(cave, player)
|
self.is_not_bunny(cave, player)
|
||||||
)
|
)
|
||||||
|
|
||||||
def can_retrieve_tablet(self, player:int) -> bool:
|
def can_retrieve_tablet(self, player: int) -> bool:
|
||||||
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
|
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
|
||||||
(self.world.swordless[player] and
|
(self.world.swordless[player] and
|
||||||
self.has("Hammer", player)))
|
self.has("Hammer", player)))
|
||||||
|
|
||||||
def has_sword(self, player: int) -> bool:
|
def has_sword(self, player: int) -> bool:
|
||||||
return self.has('Fighter Sword', player) \
|
return self.has('Fighter Sword', player) \
|
||||||
@@ -650,7 +668,8 @@ class CollectionState(object):
|
|||||||
or self.has('Golden Sword', player)
|
or self.has('Golden Sword', player)
|
||||||
|
|
||||||
def has_beam_sword(self, player: int) -> bool:
|
def has_beam_sword(self, player: int) -> bool:
|
||||||
return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword', player)
|
return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword',
|
||||||
|
player)
|
||||||
|
|
||||||
def has_melee_weapon(self, player: int) -> bool:
|
def has_melee_weapon(self, player: int) -> bool:
|
||||||
return self.has_sword(player) or self.has('Hammer', player)
|
return self.has_sword(player) or self.has('Hammer', player)
|
||||||
@@ -742,12 +761,13 @@ class CollectionState(object):
|
|||||||
self.blocked_connections[item.player] = set()
|
self.blocked_connections[item.player] = set()
|
||||||
self.stale[item.player] = True
|
self.stale[item.player] = True
|
||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
class RegionType(int, Enum):
|
class RegionType(int, Enum):
|
||||||
Generic = 0
|
Generic = 0
|
||||||
LightWorld = 1
|
LightWorld = 1
|
||||||
DarkWorld = 2
|
DarkWorld = 2
|
||||||
Cave = 3 # Also includes Houses
|
Cave = 3 # Also includes Houses
|
||||||
Dungeon = 4
|
Dungeon = 4
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -869,6 +889,7 @@ class Dungeon(object):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
||||||
|
|
||||||
|
|
||||||
class Boss():
|
class Boss():
|
||||||
def __init__(self, name, enemizer_name, defeat_rule, player: int):
|
def __init__(self, name, enemizer_name, defeat_rule, player: int):
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -882,6 +903,7 @@ class Boss():
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"Boss({self.name})"
|
return f"Boss({self.name})"
|
||||||
|
|
||||||
|
|
||||||
class Location():
|
class Location():
|
||||||
# If given as integer, then this is the shop's inventory index
|
# If given as integer, then this is the shop's inventory index
|
||||||
shop_slot: Optional[int] = None
|
shop_slot: Optional[int] = None
|
||||||
@@ -897,7 +919,7 @@ class Location():
|
|||||||
access_rule = staticmethod(lambda state: True)
|
access_rule = staticmethod(lambda state: True)
|
||||||
item_rule = staticmethod(lambda item: True)
|
item_rule = staticmethod(lambda item: True)
|
||||||
|
|
||||||
def __init__(self, player: int, name: str = '', address:int = None, parent=None):
|
def __init__(self, player: int, name: str = '', address: int = None, parent=None):
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.address: Optional[int] = address
|
self.address: Optional[int] = address
|
||||||
self.parent_region: Region = parent
|
self.parent_region: Region = parent
|
||||||
@@ -950,9 +972,12 @@ class Location():
|
|||||||
class Item():
|
class Item():
|
||||||
location: Optional[Location] = None
|
location: Optional[Location] = None
|
||||||
world: Optional[MultiWorld] = 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
|
code: Optional[int] = None # an item with ID None is called an Event, and does not get written to multidata
|
||||||
|
name: str
|
||||||
game: str = "Generic"
|
game: str = "Generic"
|
||||||
type: str = None
|
type: str = None
|
||||||
|
# indicates if this is a negative impact item. Causes these to be handled differently by various games.
|
||||||
|
trap: bool = False
|
||||||
# change manually to ensure that a specific non-progression 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
|
never_exclude = False
|
||||||
|
|
||||||
@@ -1018,56 +1043,80 @@ class Spoiler():
|
|||||||
|
|
||||||
def set_entrance(self, entrance, exit, direction, player):
|
def set_entrance(self, entrance, exit, direction, player):
|
||||||
if self.world.players == 1:
|
if self.world.players == 1:
|
||||||
self.entrances[(entrance, direction, player)] = OrderedDict([('entrance', entrance), ('exit', exit), ('direction', direction)])
|
self.entrances[(entrance, direction, player)] = OrderedDict(
|
||||||
|
[('entrance', entrance), ('exit', exit), ('direction', direction)])
|
||||||
else:
|
else:
|
||||||
self.entrances[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
|
self.entrances[(entrance, direction, player)] = OrderedDict(
|
||||||
|
[('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
|
||||||
|
|
||||||
def parse_data(self):
|
def parse_data(self):
|
||||||
self.medallions = OrderedDict()
|
self.medallions = OrderedDict()
|
||||||
for player in self.world.get_game_players("A Link to the Past"):
|
for player in self.world.get_game_players("A Link to the Past"):
|
||||||
self.medallions[f'Misery Mire ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][0]
|
self.medallions[f'Misery Mire ({self.world.get_player_name(player)})'] = \
|
||||||
self.medallions[f'Turtle Rock ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][1]
|
self.world.required_medallions[player][0]
|
||||||
|
self.medallions[f'Turtle Rock ({self.world.get_player_name(player)})'] = \
|
||||||
|
self.world.required_medallions[player][1]
|
||||||
|
|
||||||
self.locations = OrderedDict()
|
self.locations = OrderedDict()
|
||||||
listed_locations = set()
|
listed_locations = set()
|
||||||
|
|
||||||
lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld and loc.show_in_spoiler]
|
lw_locations = [loc for loc in self.world.get_locations() if
|
||||||
self.locations['Light World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in lw_locations])
|
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld and loc.show_in_spoiler]
|
||||||
|
self.locations['Light World'] = OrderedDict(
|
||||||
|
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
||||||
|
lw_locations])
|
||||||
listed_locations.update(lw_locations)
|
listed_locations.update(lw_locations)
|
||||||
|
|
||||||
dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld and loc.show_in_spoiler]
|
dw_locations = [loc for loc in self.world.get_locations() if
|
||||||
self.locations['Dark World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dw_locations])
|
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld and loc.show_in_spoiler]
|
||||||
|
self.locations['Dark World'] = OrderedDict(
|
||||||
|
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
||||||
|
dw_locations])
|
||||||
listed_locations.update(dw_locations)
|
listed_locations.update(dw_locations)
|
||||||
|
|
||||||
cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and loc.show_in_spoiler]
|
cave_locations = [loc for loc in self.world.get_locations() if
|
||||||
self.locations['Caves'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in cave_locations])
|
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and loc.show_in_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)
|
listed_locations.update(cave_locations)
|
||||||
|
|
||||||
for dungeon in self.world.dungeons.values():
|
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 and loc.show_in_spoiler]
|
dungeon_locations = [loc for loc in self.world.get_locations() if
|
||||||
self.locations[str(dungeon)] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dungeon_locations])
|
loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and loc.show_in_spoiler]
|
||||||
|
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)
|
listed_locations.update(dungeon_locations)
|
||||||
|
|
||||||
other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.show_in_spoiler]
|
other_locations = [loc for loc in self.world.get_locations() if
|
||||||
|
loc not in listed_locations and loc.show_in_spoiler]
|
||||||
if other_locations:
|
if other_locations:
|
||||||
self.locations['Other Locations'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in other_locations])
|
self.locations['Other Locations'] = OrderedDict(
|
||||||
|
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
||||||
|
other_locations])
|
||||||
listed_locations.update(other_locations)
|
listed_locations.update(other_locations)
|
||||||
|
|
||||||
self.shops = []
|
self.shops = []
|
||||||
from worlds.alttp.Shops import ShopType
|
from worlds.alttp.Shops import ShopType, price_type_display_name, price_rate_display
|
||||||
for shop in self.world.shops:
|
for shop in self.world.shops:
|
||||||
if not shop.custom:
|
if not shop.custom:
|
||||||
continue
|
continue
|
||||||
shopdata = {'location': str(shop.region),
|
shopdata = {
|
||||||
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
|
'location': str(shop.region),
|
||||||
}
|
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
|
||||||
|
}
|
||||||
for index, item in enumerate(shop.inventory):
|
for index, item in enumerate(shop.inventory):
|
||||||
if item is None:
|
if item is None:
|
||||||
continue
|
continue
|
||||||
shopdata['item_{}'.format(index)] = "{} — {}".format(item['item'], item['price']) if item['price'] else item['item']
|
my_price = item['price'] // price_rate_display.get(item['price_type'], 1)
|
||||||
|
shopdata['item_{}'.format(
|
||||||
|
index)] = f"{item['item']} — {my_price} {price_type_display_name[item['price_type']]}"
|
||||||
|
|
||||||
if item['player'] > 0:
|
if item['player'] > 0:
|
||||||
shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('—', '(Player {}) — '.format(item['player']))
|
shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('—',
|
||||||
|
'(Player {}) — '.format(
|
||||||
|
item['player']))
|
||||||
|
|
||||||
if item['max'] == 0:
|
if item['max'] == 0:
|
||||||
continue
|
continue
|
||||||
@@ -1075,7 +1124,8 @@ class Spoiler():
|
|||||||
|
|
||||||
if item['replacement'] is None:
|
if item['replacement'] is None:
|
||||||
continue
|
continue
|
||||||
shopdata['item_{}'.format(index)] += ", {} - {}".format(item['replacement'], item['replacement_price']) if item['replacement_price'] else item['replacement']
|
shopdata['item_{}'.format(
|
||||||
|
index)] += f", {item['replacement']} - {item['replacement_price']} {price_type_display_name[item['replacement_price_type']]}"
|
||||||
self.shops.append(shopdata)
|
self.shops.append(shopdata)
|
||||||
|
|
||||||
for player in self.world.get_game_players("A Link to the Past"):
|
for player in self.world.get_game_players("A Link to the Past"):
|
||||||
@@ -1084,7 +1134,8 @@ class Spoiler():
|
|||||||
self.bosses[str(player)]["Desert Palace"] = self.world.get_dungeon("Desert Palace", player).boss.name
|
self.bosses[str(player)]["Desert Palace"] = self.world.get_dungeon("Desert Palace", player).boss.name
|
||||||
self.bosses[str(player)]["Tower Of Hera"] = self.world.get_dungeon("Tower of Hera", player).boss.name
|
self.bosses[str(player)]["Tower Of Hera"] = self.world.get_dungeon("Tower of Hera", player).boss.name
|
||||||
self.bosses[str(player)]["Hyrule Castle"] = "Agahnim"
|
self.bosses[str(player)]["Hyrule Castle"] = "Agahnim"
|
||||||
self.bosses[str(player)]["Palace Of Darkness"] = self.world.get_dungeon("Palace of Darkness", player).boss.name
|
self.bosses[str(player)]["Palace Of Darkness"] = self.world.get_dungeon("Palace of Darkness",
|
||||||
|
player).boss.name
|
||||||
self.bosses[str(player)]["Swamp Palace"] = self.world.get_dungeon("Swamp Palace", player).boss.name
|
self.bosses[str(player)]["Swamp Palace"] = self.world.get_dungeon("Swamp Palace", player).boss.name
|
||||||
self.bosses[str(player)]["Skull Woods"] = self.world.get_dungeon("Skull Woods", player).boss.name
|
self.bosses[str(player)]["Skull Woods"] = self.world.get_dungeon("Skull Woods", player).boss.name
|
||||||
self.bosses[str(player)]["Thieves Town"] = self.world.get_dungeon("Thieves Town", player).boss.name
|
self.bosses[str(player)]["Thieves Town"] = self.world.get_dungeon("Thieves Town", player).boss.name
|
||||||
@@ -1092,13 +1143,19 @@ class Spoiler():
|
|||||||
self.bosses[str(player)]["Misery Mire"] = self.world.get_dungeon("Misery Mire", player).boss.name
|
self.bosses[str(player)]["Misery Mire"] = self.world.get_dungeon("Misery Mire", player).boss.name
|
||||||
self.bosses[str(player)]["Turtle Rock"] = self.world.get_dungeon("Turtle Rock", player).boss.name
|
self.bosses[str(player)]["Turtle Rock"] = self.world.get_dungeon("Turtle Rock", player).boss.name
|
||||||
if self.world.mode[player] != 'inverted':
|
if self.world.mode[player] != 'inverted':
|
||||||
self.bosses[str(player)]["Ganons Tower Basement"] = self.world.get_dungeon('Ganons Tower', player).bosses['bottom'].name
|
self.bosses[str(player)]["Ganons Tower Basement"] = \
|
||||||
self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Ganons Tower', player).bosses['middle'].name
|
self.world.get_dungeon('Ganons Tower', player).bosses['bottom'].name
|
||||||
self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Ganons Tower', player).bosses['top'].name
|
self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Ganons Tower', player).bosses[
|
||||||
|
'middle'].name
|
||||||
|
self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Ganons Tower', player).bosses[
|
||||||
|
'top'].name
|
||||||
else:
|
else:
|
||||||
self.bosses[str(player)]["Ganons Tower Basement"] = self.world.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].name
|
self.bosses[str(player)]["Ganons Tower Basement"] = \
|
||||||
self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].name
|
self.world.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].name
|
||||||
self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Inverted Ganons Tower', player).bosses['top'].name
|
self.bosses[str(player)]["Ganons Tower Middle"] = \
|
||||||
|
self.world.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].name
|
||||||
|
self.bosses[str(player)]["Ganons Tower Top"] = \
|
||||||
|
self.world.get_dungeon('Inverted Ganons Tower', player).bosses['top'].name
|
||||||
|
|
||||||
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
|
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
|
||||||
self.bosses[str(player)]["Ganon"] = "Ganon"
|
self.bosses[str(player)]["Ganon"] = "Ganon"
|
||||||
@@ -1171,7 +1228,7 @@ class Spoiler():
|
|||||||
outfile.write('Item Functionality: %s\n' % self.world.item_functionality[player])
|
outfile.write('Item Functionality: %s\n' % self.world.item_functionality[player])
|
||||||
outfile.write('Entrance Shuffle: %s\n' % self.world.shuffle[player])
|
outfile.write('Entrance Shuffle: %s\n' % self.world.shuffle[player])
|
||||||
if self.world.shuffle[player] != "vanilla":
|
if self.world.shuffle[player] != "vanilla":
|
||||||
outfile.write('Entrance Shuffle Seed %s\n' % self.world.er_seeds[player])
|
outfile.write('Entrance Shuffle Seed %s\n' % self.world.worlds[player].er_seed)
|
||||||
outfile.write('Pyramid hole pre-opened: %s\n' % (
|
outfile.write('Pyramid hole pre-opened: %s\n' % (
|
||||||
'Yes' if self.world.open_pyramid[player] else 'No'))
|
'Yes' if self.world.open_pyramid[player] else 'No'))
|
||||||
outfile.write('Shop inventory shuffle: %s\n' %
|
outfile.write('Shop inventory shuffle: %s\n' %
|
||||||
@@ -1212,22 +1269,30 @@ class Spoiler():
|
|||||||
outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}")
|
outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}")
|
||||||
|
|
||||||
outfile.write('\n\nLocations:\n\n')
|
outfile.write('\n\nLocations:\n\n')
|
||||||
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))
|
outfile.write('\n'.join(
|
||||||
|
['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in
|
||||||
|
grouping.items()]))
|
||||||
|
|
||||||
if self.shops:
|
if self.shops:
|
||||||
outfile.write('\n\nShops:\n\n')
|
outfile.write('\n\nShops:\n\n')
|
||||||
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
|
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(
|
||||||
|
item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if
|
||||||
|
item)) for shop in self.shops))
|
||||||
|
|
||||||
for player in self.world.get_game_players("A Link to the Past"):
|
for player in self.world.get_game_players("A Link to the Past"):
|
||||||
if self.world.boss_shuffle[player] != 'none':
|
if self.world.boss_shuffle[player] != 'none':
|
||||||
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses
|
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses
|
||||||
outfile.write(f'\n\nBosses{(f" ({self.world.get_player_name(player)})" if self.world.players > 1 else "")}:\n')
|
outfile.write(
|
||||||
outfile.write(' '+'\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
|
f'\n\nBosses{(f" ({self.world.get_player_name(player)})" if self.world.players > 1 else "")}:\n')
|
||||||
|
outfile.write(' ' + '\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
|
||||||
outfile.write('\n\nPlaythrough:\n\n')
|
outfile.write('\n\nPlaythrough:\n\n')
|
||||||
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
|
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join(
|
||||||
|
[' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [
|
||||||
|
f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
|
||||||
if self.unreachables:
|
if self.unreachables:
|
||||||
outfile.write('\n\nUnreachable Items:\n\n')
|
outfile.write('\n\nUnreachable Items:\n\n')
|
||||||
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
|
outfile.write(
|
||||||
|
'\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
|
||||||
|
|
||||||
if self.paths:
|
if self.paths:
|
||||||
outfile.write('\n\nPaths:\n\n')
|
outfile.write('\n\nPaths:\n\n')
|
||||||
@@ -1242,3 +1307,13 @@ class Spoiler():
|
|||||||
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
|
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
|
||||||
|
|
||||||
outfile.write('\n'.join(path_listings))
|
outfile.write('\n'.join(path_listings))
|
||||||
|
|
||||||
|
|
||||||
|
seeddigits = 20
|
||||||
|
|
||||||
|
|
||||||
|
def get_seed(seed=None):
|
||||||
|
if seed is None:
|
||||||
|
random.seed(None)
|
||||||
|
return random.randint(0, pow(10, seeddigits) - 1)
|
||||||
|
return seed
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import typing
|
|||||||
import asyncio
|
import asyncio
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
from MultiServer import CommandProcessor
|
from MultiServer import CommandProcessor
|
||||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, color, ClientStatus
|
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission
|
||||||
from Utils import Version
|
from Utils import Version
|
||||||
from worlds import network_data_package, AutoWorldRegister
|
from worlds import network_data_package, AutoWorldRegister
|
||||||
|
|
||||||
@@ -17,6 +18,9 @@ logger = logging.getLogger("Client")
|
|||||||
|
|
||||||
gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv
|
gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv
|
||||||
|
|
||||||
|
log_folder = Utils.local_path("logs")
|
||||||
|
os.makedirs(log_folder, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
class ClientCommandProcessor(CommandProcessor):
|
class ClientCommandProcessor(CommandProcessor):
|
||||||
def __init__(self, ctx: CommonContext):
|
def __init__(self, ctx: CommonContext):
|
||||||
@@ -198,7 +202,7 @@ class CommonContext():
|
|||||||
def event_invalid_game(self):
|
def event_invalid_game(self):
|
||||||
raise Exception('Invalid Game; please verify that you connected with the right game to the correct world.')
|
raise Exception('Invalid Game; please verify that you connected with the right game to the correct world.')
|
||||||
|
|
||||||
async def server_auth(self, password_requested):
|
async def server_auth(self, password_requested: bool = False):
|
||||||
if password_requested and not self.password:
|
if password_requested and not self.password:
|
||||||
logger.info('Enter the password required to join this game:')
|
logger.info('Enter the password required to join this game:')
|
||||||
self.password = await self.console_input()
|
self.password = await self.console_input()
|
||||||
@@ -315,16 +319,17 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
|
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
|
||||||
if args['password']:
|
if args['password']:
|
||||||
logger.info('Password required')
|
logger.info('Password required')
|
||||||
logger.info(f"Forfeit setting: {args['forfeit_mode']}")
|
|
||||||
logger.info(f"Remaining setting: {args['remaining_mode']}")
|
for permission_name, permission_flag in args.get("permissions", {}).items():
|
||||||
|
flag = Permission(permission_flag)
|
||||||
|
logger.info(f"{permission_name.capitalize()} permission: {flag.name}")
|
||||||
logger.info(
|
logger.info(
|
||||||
f"A !hint costs {args['hint_cost']}% of your total location count as points"
|
f"A !hint costs {args['hint_cost']}% of your total location count as points"
|
||||||
f" and you get {args['location_check_points']}"
|
f" and you get {args['location_check_points']}"
|
||||||
f" for each location checked. Use !hint for more information.")
|
f" for each location checked. Use !hint for more information.")
|
||||||
ctx.hint_cost = int(args['hint_cost'])
|
ctx.hint_cost = int(args['hint_cost'])
|
||||||
ctx.check_points = int(args['location_check_points'])
|
ctx.check_points = int(args['location_check_points'])
|
||||||
ctx.forfeit_mode = args['forfeit_mode']
|
|
||||||
ctx.remaining_mode = args['remaining_mode']
|
|
||||||
if len(args['players']) < 1:
|
if len(args['players']) < 1:
|
||||||
logger.info('No player connected')
|
logger.info('No player connected')
|
||||||
else:
|
else:
|
||||||
@@ -452,3 +457,78 @@ async def console_loop(ctx: CommonContext):
|
|||||||
commandprocessor(input_text)
|
commandprocessor(input_text)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
|
||||||
|
|
||||||
|
def init_logging(name: str):
|
||||||
|
if gui_enabled:
|
||||||
|
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
|
||||||
|
filename=os.path.join(log_folder, f"{name}.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(log_folder, f"{name}.txt"), "w"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Text Mode to use !hint and such with games that have no text entry
|
||||||
|
init_logging("TextClient")
|
||||||
|
|
||||||
|
class TextContext(CommonContext):
|
||||||
|
async def server_auth(self, password_requested: bool = False):
|
||||||
|
if password_requested and not self.password:
|
||||||
|
await super(TextContext, self).server_auth(password_requested)
|
||||||
|
if not self.auth:
|
||||||
|
logger.info('Enter slot name:')
|
||||||
|
self.auth = await self.console_input()
|
||||||
|
|
||||||
|
await self.send_msgs([{"cmd": 'Connect',
|
||||||
|
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
||||||
|
'tags': ['AP', 'IgnoreGame'],
|
||||||
|
'uuid': Utils.get_unique_identifier(), 'game': self.game
|
||||||
|
}])
|
||||||
|
|
||||||
|
async def main(args):
|
||||||
|
ctx = TextContext(args.connect, args.password)
|
||||||
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||||
|
if gui_enabled:
|
||||||
|
input_task = None
|
||||||
|
from kvui import TextManager
|
||||||
|
ctx.ui = TextManager(ctx)
|
||||||
|
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
|
||||||
|
else:
|
||||||
|
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||||
|
ui_task = None
|
||||||
|
await ctx.exit_event.wait()
|
||||||
|
|
||||||
|
ctx.server_address = None
|
||||||
|
if ctx.server and not ctx.server.socket.closed:
|
||||||
|
await ctx.server.socket.close()
|
||||||
|
if ctx.server_task:
|
||||||
|
await ctx.server_task
|
||||||
|
|
||||||
|
while ctx.input_requests > 0:
|
||||||
|
ctx.input_queue.put_nowait(None)
|
||||||
|
ctx.input_requests -= 1
|
||||||
|
|
||||||
|
if ui_task:
|
||||||
|
await ui_task
|
||||||
|
|
||||||
|
if input_task:
|
||||||
|
input_task.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import colorama
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Gameless Archipelago Client, for text interfaction.")
|
||||||
|
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
||||||
|
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
|
||||||
|
if not Utils.is_frozen(): # Frozen state has no cmd window in the first place
|
||||||
|
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
|
||||||
|
|
||||||
|
args, rest = parser.parse_known_args()
|
||||||
|
colorama.init()
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.run_until_complete(main(args))
|
||||||
|
loop.close()
|
||||||
|
colorama.deinit()
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import factorio_rcon
|
|||||||
import colorama
|
import colorama
|
||||||
import asyncio
|
import asyncio
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled
|
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \
|
||||||
|
init_logging
|
||||||
from MultiServer import mark_raw
|
from MultiServer import mark_raw
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
@@ -19,17 +20,7 @@ from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePar
|
|||||||
|
|
||||||
from worlds.factorio import Factorio
|
from worlds.factorio import Factorio
|
||||||
|
|
||||||
log_folder = Utils.local_path("logs")
|
init_logging("FactorioClient")
|
||||||
|
|
||||||
os.makedirs(log_folder, exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
if gui_enabled:
|
|
||||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
|
|
||||||
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(log_folder, "FactorioClient.txt"), "w"))
|
|
||||||
|
|
||||||
|
|
||||||
class FactorioCommandProcessor(ClientCommandProcessor):
|
class FactorioCommandProcessor(ClientCommandProcessor):
|
||||||
@@ -66,7 +57,7 @@ class FactorioContext(CommonContext):
|
|||||||
self.awaiting_bridge = False
|
self.awaiting_bridge = False
|
||||||
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
|
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
|
||||||
|
|
||||||
async def server_auth(self, password_requested):
|
async def server_auth(self, password_requested: bool = False):
|
||||||
if password_requested and not self.password:
|
if password_requested and not self.password:
|
||||||
await super(FactorioContext, self).server_auth(password_requested)
|
await super(FactorioContext, self).server_auth(password_requested)
|
||||||
|
|
||||||
@@ -77,11 +68,15 @@ class FactorioContext(CommonContext):
|
|||||||
raise Exception("Cannot connect to a server with unknown own identity, "
|
raise Exception("Cannot connect to a server with unknown own identity, "
|
||||||
"bridge to Factorio first.")
|
"bridge to Factorio first.")
|
||||||
|
|
||||||
await self.send_msgs([{"cmd": 'Connect',
|
await self.send_msgs([{
|
||||||
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
"cmd": 'Connect',
|
||||||
'tags': ['AP'],
|
'password': self.password,
|
||||||
'uuid': Utils.get_unique_identifier(), 'game': "Factorio"
|
'name': self.auth,
|
||||||
}])
|
'version': Utils.version_tuple,
|
||||||
|
'tags': ['AP'],
|
||||||
|
'uuid': Utils.get_unique_identifier(),
|
||||||
|
'game': "Factorio"
|
||||||
|
}])
|
||||||
|
|
||||||
def on_print(self, args: dict):
|
def on_print(self, args: dict):
|
||||||
logger.info(args["text"])
|
logger.info(args["text"])
|
||||||
@@ -99,14 +94,8 @@ class FactorioContext(CommonContext):
|
|||||||
return f"AP_{self.seed_name}_{self.auth}.zip"
|
return f"AP_{self.seed_name}_{self.auth}.zip"
|
||||||
|
|
||||||
def print_to_game(self, text):
|
def print_to_game(self, text):
|
||||||
# TODO: remove around version 0.2
|
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
|
||||||
if self.mod_version < Utils.Version(0, 1, 6):
|
f"{text}")
|
||||||
text = text.replace('"', '')
|
|
||||||
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
|
|
||||||
f"{text}\")")
|
|
||||||
else:
|
|
||||||
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
|
|
||||||
f"{text}")
|
|
||||||
|
|
||||||
def on_package(self, cmd: str, args: dict):
|
def on_package(self, cmd: str, args: dict):
|
||||||
if cmd == "Connected":
|
if cmd == "Connected":
|
||||||
@@ -194,10 +183,6 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
|||||||
factorio_server_logger.info(msg)
|
factorio_server_logger.info(msg)
|
||||||
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
||||||
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
||||||
# TODO: remove around version 0.2
|
|
||||||
if ctx.mod_version < Utils.Version(0, 1, 6):
|
|
||||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
|
||||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
|
||||||
if not ctx.server:
|
if not ctx.server:
|
||||||
logger.info("Established bridge to Factorio Server. "
|
logger.info("Established bridge to Factorio Server. "
|
||||||
"Ready to connect to Archipelago via /connect")
|
"Ready to connect to Archipelago via /connect")
|
||||||
@@ -278,13 +263,15 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
|||||||
ctx.exit_event.set()
|
ctx.exit_event.set()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.info(f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
|
logger.info(
|
||||||
|
f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
|
||||||
return True
|
return True
|
||||||
finally:
|
finally:
|
||||||
factorio_process.terminate()
|
factorio_process.terminate()
|
||||||
factorio_process.wait(5)
|
factorio_process.wait(5)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def main(args):
|
async def main(args):
|
||||||
ctx = FactorioContext(args.connect, args.password)
|
ctx = FactorioContext(args.connect, args.password)
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||||
@@ -355,7 +342,8 @@ if __name__ == '__main__':
|
|||||||
args, rest = parser.parse_known_args()
|
args, rest = parser.parse_known_args()
|
||||||
colorama.init()
|
colorama.init()
|
||||||
rcon_port = args.rcon_port
|
rcon_port = args.rcon_port
|
||||||
rcon_password = args.rcon_password if args.rcon_password else ''.join(random.choice(string.ascii_letters) for x in range(32))
|
rcon_password = args.rcon_password if args.rcon_password else ''.join(
|
||||||
|
random.choice(string.ascii_letters) for x in range(32))
|
||||||
|
|
||||||
factorio_server_logger = logging.getLogger("FactorioServer")
|
factorio_server_logger = logging.getLogger("FactorioServer")
|
||||||
options = Utils.get_options()
|
options = Utils.get_options()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from worlds.generic import PlandoItem, PlandoConnection
|
|||||||
from Utils import parse_yaml, version_tuple, __version__, tuplize_version, get_options
|
from Utils import parse_yaml, version_tuple, __version__, tuplize_version, get_options
|
||||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||||
from Main import main as ERmain
|
from Main import main as ERmain
|
||||||
from Main import get_seed, seeddigits
|
from BaseClasses import seeddigits, get_seed
|
||||||
import Options
|
import Options
|
||||||
from worlds.alttp import Bosses
|
from worlds.alttp import Bosses
|
||||||
from worlds.alttp.Text import TextTable
|
from worlds.alttp.Text import TextTable
|
||||||
|
|||||||
@@ -26,24 +26,14 @@ from NetUtils import *
|
|||||||
from worlds.alttp import Regions, Shops
|
from worlds.alttp import Regions, Shops
|
||||||
from worlds.alttp import Items
|
from worlds.alttp import Items
|
||||||
import Utils
|
import Utils
|
||||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled
|
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled, init_logging
|
||||||
|
|
||||||
|
init_logging("LttPClient")
|
||||||
|
|
||||||
snes_logger = logging.getLogger("SNES")
|
snes_logger = logging.getLogger("SNES")
|
||||||
|
|
||||||
from MultiServer import mark_raw
|
from MultiServer import mark_raw
|
||||||
|
|
||||||
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(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(log_folder, "LttPClient.txt"), "w"))
|
|
||||||
|
|
||||||
|
|
||||||
class LttPCommandProcessor(ClientCommandProcessor):
|
class LttPCommandProcessor(ClientCommandProcessor):
|
||||||
def _cmd_slow_mode(self, toggle: str = ""):
|
def _cmd_slow_mode(self, toggle: str = ""):
|
||||||
"""Toggle slow mode, which limits how fast you send / receive items."""
|
"""Toggle slow mode, which limits how fast you send / receive items."""
|
||||||
|
|||||||
97
Main.py
97
Main.py
@@ -1,14 +1,13 @@
|
|||||||
from itertools import zip_longest
|
from itertools import zip_longest, chain
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import random
|
|
||||||
import time
|
import time
|
||||||
import zlib
|
import zlib
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import pickle
|
import pickle
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from typing import Dict, Tuple
|
from typing import Dict, Tuple, Optional
|
||||||
|
|
||||||
from BaseClasses import MultiWorld, CollectionState, Region, RegionType
|
from BaseClasses import MultiWorld, CollectionState, Region, RegionType
|
||||||
from worlds.alttp.Items import item_name_groups
|
from worlds.alttp.Items import item_name_groups
|
||||||
@@ -19,17 +18,17 @@ from Utils import output_path, get_options, __version__, version_tuple
|
|||||||
from worlds.generic.Rules import locality_rules, exclusion_rules
|
from worlds.generic.Rules import locality_rules, exclusion_rules
|
||||||
from worlds import AutoWorld
|
from worlds import AutoWorld
|
||||||
|
|
||||||
seeddigits = 20
|
|
||||||
|
ordered_areas = (
|
||||||
|
'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
||||||
|
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
|
||||||
|
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_seed(seed=None):
|
def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None):
|
||||||
if seed is None:
|
if not baked_server_options:
|
||||||
random.seed(None)
|
baked_server_options = get_options()["server_options"]
|
||||||
return random.randint(0, pow(10, seeddigits) - 1)
|
|
||||||
return seed
|
|
||||||
|
|
||||||
|
|
||||||
def main(args, seed=None):
|
|
||||||
if args.outputpath:
|
if args.outputpath:
|
||||||
os.makedirs(args.outputpath, exist_ok=True)
|
os.makedirs(args.outputpath, exist_ok=True)
|
||||||
output_path.cached_path = args.outputpath
|
output_path.cached_path = args.outputpath
|
||||||
@@ -39,13 +38,8 @@ def main(args, seed=None):
|
|||||||
# initialize the world
|
# initialize the world
|
||||||
world = MultiWorld(args.multi)
|
world = MultiWorld(args.multi)
|
||||||
|
|
||||||
logger = logging.getLogger('')
|
logger = logging.getLogger()
|
||||||
world.seed = get_seed(seed)
|
world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed))
|
||||||
if args.race:
|
|
||||||
world.secure()
|
|
||||||
else:
|
|
||||||
world.random.seed(world.seed)
|
|
||||||
world.seed_name = str(args.outputname if args.outputname else world.seed)
|
|
||||||
|
|
||||||
world.shuffle = args.shuffle.copy()
|
world.shuffle = args.shuffle.copy()
|
||||||
world.logic = args.logic.copy()
|
world.logic = args.logic.copy()
|
||||||
@@ -81,7 +75,6 @@ def main(args, seed=None):
|
|||||||
world.plando_items = args.plando_items.copy()
|
world.plando_items = args.plando_items.copy()
|
||||||
world.plando_texts = args.plando_texts.copy()
|
world.plando_texts = args.plando_texts.copy()
|
||||||
world.plando_connections = args.plando_connections.copy()
|
world.plando_connections = args.plando_connections.copy()
|
||||||
world.er_seeds = getattr(args, "er_seeds", {})
|
|
||||||
world.required_medallions = args.required_medallions.copy()
|
world.required_medallions = args.required_medallions.copy()
|
||||||
world.game = args.game.copy()
|
world.game = args.game.copy()
|
||||||
world.set_options(args)
|
world.set_options(args)
|
||||||
@@ -90,9 +83,6 @@ def main(args, seed=None):
|
|||||||
world.sprite = args.sprite.copy()
|
world.sprite = args.sprite.copy()
|
||||||
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
||||||
|
|
||||||
world.slot_seeds = {player: random.Random(world.random.getrandbits(64)) for player in
|
|
||||||
range(1, world.players + 1)}
|
|
||||||
|
|
||||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
|
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
|
||||||
|
|
||||||
logger.info("Found World Types:")
|
logger.info("Found World Types:")
|
||||||
@@ -178,16 +168,15 @@ def main(args, seed=None):
|
|||||||
|
|
||||||
output = tempfile.TemporaryDirectory()
|
output = tempfile.TemporaryDirectory()
|
||||||
with output as temp_dir:
|
with output as temp_dir:
|
||||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
with concurrent.futures.ThreadPoolExecutor(world.players + 2) as pool:
|
||||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||||
|
|
||||||
output_file_futures = []
|
output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
|
||||||
|
|
||||||
for player in world.player_ids:
|
for player in world.player_ids:
|
||||||
# skip starting a thread for methods that say "pass".
|
# skip starting a thread for methods that say "pass".
|
||||||
if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
|
if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
|
||||||
output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
output_file_futures.append(
|
||||||
output_file_futures.append(pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir))
|
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
||||||
|
|
||||||
def get_entrance_to_region(region: Region):
|
def get_entrance_to_region(region: Region):
|
||||||
for entrance in region.entrances:
|
for entrance in region.entrances:
|
||||||
@@ -208,9 +197,7 @@ def main(args, seed=None):
|
|||||||
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
||||||
er_hint_data[region.player][location.address] = main_entrance.name
|
er_hint_data[region.player][location.address] = main_entrance.name
|
||||||
|
|
||||||
ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
|
||||||
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
|
|
||||||
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
|
|
||||||
|
|
||||||
checks_in_area = {player: {area: list() for area in ordered_areas}
|
checks_in_area = {player: {area: list() for area in ordered_areas}
|
||||||
for player in range(1, world.players + 1)}
|
for player in range(1, world.players + 1)}
|
||||||
@@ -239,8 +226,9 @@ def main(args, seed=None):
|
|||||||
for index, take_any in enumerate(takeanyregions):
|
for index, take_any in enumerate(takeanyregions):
|
||||||
for region in [world.get_region(take_any, player) for player in
|
for region in [world.get_region(take_any, player) for player in
|
||||||
world.get_game_players("A Link to the Past") if world.retro[player]]:
|
world.get_game_players("A Link to the Past") if world.retro[player]]:
|
||||||
item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
|
item = world.create_item(
|
||||||
region.player)
|
region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
|
||||||
|
region.player)
|
||||||
player = region.player
|
player = region.player
|
||||||
location_id = SHOP_ID_START + total_shop_slots + index
|
location_id = SHOP_ID_START + total_shop_slots + index
|
||||||
|
|
||||||
@@ -265,18 +253,22 @@ def main(args, seed=None):
|
|||||||
for slot in world.player_ids:
|
for slot in world.player_ids:
|
||||||
client_versions[slot] = world.worlds[slot].get_required_client_version()
|
client_versions[slot] = world.worlds[slot].get_required_client_version()
|
||||||
games[slot] = world.game[slot]
|
games[slot] = world.game[slot]
|
||||||
precollected_items = {player: [] for player in range(1, world.players + 1)}
|
precollected_items = {player: [item.code for item in world_precollected]
|
||||||
for item in world.precollected_items:
|
for player, world_precollected in world.precollected_items.items()}
|
||||||
precollected_items[item.player].append(item.code)
|
|
||||||
precollected_hints = {player: set() for player in range(1, world.players + 1)}
|
precollected_hints = {player: set() for player in range(1, world.players + 1)}
|
||||||
# for now special case Factorio tech_tree_information
|
# for now special case Factorio tech_tree_information
|
||||||
sending_visible_players = set()
|
sending_visible_players = set()
|
||||||
for player in world.get_game_players("Factorio"):
|
|
||||||
if world.tech_tree_information[player].value == 2:
|
|
||||||
sending_visible_players.add(player)
|
|
||||||
|
|
||||||
for slot in world.player_ids:
|
for slot in world.player_ids:
|
||||||
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
||||||
|
if world.worlds[slot].sending_visible:
|
||||||
|
sending_visible_players.add(slot)
|
||||||
|
|
||||||
|
def precollect_hint(location):
|
||||||
|
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||||
|
location.item.code, False)
|
||||||
|
precollected_hints[location.player].add(hint)
|
||||||
|
precollected_hints[location.item.player].add(hint)
|
||||||
|
|
||||||
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
|
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
|
||||||
for location in world.get_filled_locations():
|
for location in world.get_filled_locations():
|
||||||
@@ -284,17 +276,12 @@ def main(args, seed=None):
|
|||||||
# item code None should be event, location.address should then also be None
|
# item code None should be event, location.address should then also be None
|
||||||
assert location.item.code is not None
|
assert location.item.code is not None
|
||||||
locations_data[location.player][location.address] = location.item.code, location.item.player
|
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:
|
if location.player in sending_visible_players:
|
||||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
precollect_hint(location)
|
||||||
location.item.code, False)
|
elif location.name in world.start_location_hints[location.player]:
|
||||||
precollected_hints[location.player].add(hint)
|
precollect_hint(location)
|
||||||
precollected_hints[location.item.player].add(hint)
|
elif location.item.name in world.start_hints[location.item.player]:
|
||||||
elif location.item.name in args.start_hints[location.item.player]:
|
precollect_hint(location)
|
||||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
|
||||||
location.item.code, False,
|
|
||||||
er_hint_data.get(location.player, {}).get(location.address, ""))
|
|
||||||
precollected_hints[location.player].add(hint)
|
|
||||||
precollected_hints[location.item.player].add(hint)
|
|
||||||
|
|
||||||
multidata = {
|
multidata = {
|
||||||
"slot_data": slot_data,
|
"slot_data": slot_data,
|
||||||
@@ -307,7 +294,7 @@ def main(args, seed=None):
|
|||||||
world.worlds[player].remote_start_inventory},
|
world.worlds[player].remote_start_inventory},
|
||||||
"locations": locations_data,
|
"locations": locations_data,
|
||||||
"checks_in_area": checks_in_area,
|
"checks_in_area": checks_in_area,
|
||||||
"server_options": get_options()["server_options"],
|
"server_options": baked_server_options,
|
||||||
"er_hint_data": er_hint_data,
|
"er_hint_data": er_hint_data,
|
||||||
"precollected_items": precollected_items,
|
"precollected_items": precollected_items,
|
||||||
"precollected_hints": precollected_hints,
|
"precollected_hints": precollected_hints,
|
||||||
@@ -416,9 +403,9 @@ def create_playthrough(world):
|
|||||||
|
|
||||||
# second phase, sphere 0
|
# second phase, sphere 0
|
||||||
removed_precollected = []
|
removed_precollected = []
|
||||||
for item in (i for i in world.precollected_items if i.advancement):
|
for item in (i for i in chain.from_iterable(world.precollected_items.values()) if i.advancement):
|
||||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
||||||
world.precollected_items.remove(item)
|
world.precollected_items[item.player].remove(item)
|
||||||
world.state.remove(item)
|
world.state.remove(item)
|
||||||
if not world.can_beat_game():
|
if not world.can_beat_game():
|
||||||
world.push_precollected(item)
|
world.push_precollected(item)
|
||||||
@@ -482,7 +469,9 @@ def create_playthrough(world):
|
|||||||
get_path(state, 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
|
# we can finally output our playthrough
|
||||||
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}
|
world.spoiler.playthrough = {"0": sorted([str(item) for item in
|
||||||
|
chain.from_iterable(world.precollected_items.values())
|
||||||
|
if item.advancement])}
|
||||||
|
|
||||||
for i, sphere in enumerate(collection_spheres):
|
for i, sphere in enumerate(collection_spheres):
|
||||||
world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sorted(sphere)}
|
world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sorted(sphere)}
|
||||||
|
|||||||
@@ -26,12 +26,13 @@ from prompt_toolkit.patch_stdout import patch_stdout
|
|||||||
from fuzzywuzzy import process as fuzzy_process
|
from fuzzywuzzy import process as fuzzy_process
|
||||||
|
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()}
|
proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()}
|
||||||
from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||||
import Utils
|
import Utils
|
||||||
from Utils import get_item_name_from_id, get_location_name_from_id, \
|
from Utils import get_item_name_from_id, get_location_name_from_id, \
|
||||||
version_tuple, restricted_loads, Version
|
version_tuple, restricted_loads, Version
|
||||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer
|
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission
|
||||||
|
|
||||||
colorama.init()
|
colorama.init()
|
||||||
|
|
||||||
@@ -293,7 +294,7 @@ class Context:
|
|||||||
if not self.save_filename:
|
if not self.save_filename:
|
||||||
import os
|
import os
|
||||||
name, ext = os.path.splitext(self.data_filename)
|
name, ext = os.path.splitext(self.data_filename)
|
||||||
self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago','.zip') \
|
self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago', '.zip') \
|
||||||
else self.data_filename + '_' + 'apsave'
|
else self.data_filename + '_' + 'apsave'
|
||||||
try:
|
try:
|
||||||
with open(self.save_filename, 'rb') as f:
|
with open(self.save_filename, 'rb') as f:
|
||||||
@@ -460,7 +461,7 @@ async def server(websocket, path, ctx: Context):
|
|||||||
async def on_client_connected(ctx: Context, client: Client):
|
async def on_client_connected(ctx: Context, client: Client):
|
||||||
await ctx.send_msgs(client, [{
|
await ctx.send_msgs(client, [{
|
||||||
'cmd': 'RoomInfo',
|
'cmd': 'RoomInfo',
|
||||||
'password': ctx.password is not None,
|
'password': bool(ctx.password),
|
||||||
'players': [
|
'players': [
|
||||||
NetworkPlayer(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name),
|
NetworkPlayer(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name),
|
||||||
client.name) for client
|
client.name) for client
|
||||||
@@ -469,8 +470,10 @@ async def on_client_connected(ctx: Context, client: Client):
|
|||||||
# Name them by feature or fork, as you feel is appropriate.
|
# Name them by feature or fork, as you feel is appropriate.
|
||||||
'tags': ctx.tags,
|
'tags': ctx.tags,
|
||||||
'version': Utils.version_tuple,
|
'version': Utils.version_tuple,
|
||||||
|
# TODO ~0.2.0 remove forfeit_mode and remaining_mode in favor of permissions
|
||||||
'forfeit_mode': ctx.forfeit_mode,
|
'forfeit_mode': ctx.forfeit_mode,
|
||||||
'remaining_mode': ctx.remaining_mode,
|
'remaining_mode': ctx.remaining_mode,
|
||||||
|
'permissions': get_permissions(ctx),
|
||||||
'hint_cost': ctx.hint_cost,
|
'hint_cost': ctx.hint_cost,
|
||||||
'location_check_points': ctx.location_check_points,
|
'location_check_points': ctx.location_check_points,
|
||||||
'datapackage_version': network_data_package["version"],
|
'datapackage_version': network_data_package["version"],
|
||||||
@@ -480,6 +483,13 @@ async def on_client_connected(ctx: Context, client: Client):
|
|||||||
}])
|
}])
|
||||||
|
|
||||||
|
|
||||||
|
def get_permissions(ctx) -> typing.Dict[str, Permission]:
|
||||||
|
return {
|
||||||
|
"forfeit": Permission.from_text(ctx.forfeit_mode),
|
||||||
|
"remaining": Permission.from_text(ctx.remaining_mode),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def on_client_disconnected(ctx: Context, client: Client):
|
async def on_client_disconnected(ctx: Context, client: Client):
|
||||||
if client.auth:
|
if client.auth:
|
||||||
await on_client_left(ctx, client)
|
await on_client_left(ctx, client)
|
||||||
@@ -967,14 +977,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
self.output("Cheating is disabled.")
|
self.output("Cheating is disabled.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@mark_raw
|
def get_hints(self, input_text: str, explicit_location: bool = False) -> bool:
|
||||||
def _cmd_hint(self, item_or_location: str = "") -> bool:
|
|
||||||
"""Use !hint {item_name/location_name},
|
|
||||||
for example !hint Lamp or !hint Link's House to get a spoiler peek for that location or item.
|
|
||||||
If hint costs are on, this will only give you one new result,
|
|
||||||
you can rerun the command to get more in that case."""
|
|
||||||
points_available = get_client_points(self.ctx, self.client)
|
points_available = get_client_points(self.ctx, self.client)
|
||||||
if not item_or_location:
|
if not input_text:
|
||||||
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
||||||
self.ctx.hints[self.client.team, self.client.slot]}
|
self.ctx.hints[self.client.team, self.client.slot]}
|
||||||
self.ctx.hints[self.client.team, self.client.slot] = hints
|
self.ctx.hints[self.client.team, self.client.slot] = hints
|
||||||
@@ -984,16 +989,16 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
||||||
item_name, usable, response = get_intended_text(item_or_location, world.all_names)
|
item_name, usable, response = get_intended_text(input_text, world.all_names if not explicit_location else world.location_names)
|
||||||
if usable:
|
if usable:
|
||||||
if item_name in world.hint_blacklist:
|
if item_name in world.hint_blacklist:
|
||||||
self.output(f"Sorry, \"{item_name}\" is marked as non-hintable.")
|
self.output(f"Sorry, \"{item_name}\" is marked as non-hintable.")
|
||||||
hints = []
|
hints = []
|
||||||
elif item_name in world.item_name_groups:
|
elif item_name in world.item_name_groups and not explicit_location:
|
||||||
hints = []
|
hints = []
|
||||||
for item in world.item_name_groups[item_name]:
|
for item in world.item_name_groups[item_name]:
|
||||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
|
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
|
||||||
elif item_name in world.item_names: # item name
|
elif item_name in world.item_names and not explicit_location: # item name
|
||||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, item_name)
|
hints = collect_hints(self.ctx, self.client.team, self.client.slot, item_name)
|
||||||
else: # location name
|
else: # location name
|
||||||
hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, item_name)
|
hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, item_name)
|
||||||
@@ -1026,19 +1031,25 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
hints.append(hint)
|
hints.append(hint)
|
||||||
can_pay -= 1
|
can_pay -= 1
|
||||||
self.ctx.hints_used[self.client.team, self.client.slot] += 1
|
self.ctx.hints_used[self.client.team, self.client.slot] += 1
|
||||||
|
points_available = get_client_points(self.ctx, self.client)
|
||||||
|
|
||||||
if not hint.found:
|
if not hint.found:
|
||||||
self.ctx.hints[self.client.team, hint.finding_player].add(hint)
|
self.ctx.hints[self.client.team, hint.finding_player].add(hint)
|
||||||
self.ctx.hints[self.client.team, hint.receiving_player].add(hint)
|
self.ctx.hints[self.client.team, hint.receiving_player].add(hint)
|
||||||
|
|
||||||
if not_found_hints:
|
if not_found_hints:
|
||||||
if hints:
|
if hints and cost and int((points_available // cost) == 0):
|
||||||
|
self.output(
|
||||||
|
f"There may be more hintables, however, you cannot afford to pay for any more. "
|
||||||
|
f" You have {points_available} and need at least "
|
||||||
|
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||||
|
elif hints:
|
||||||
self.output(
|
self.output(
|
||||||
"There may be more hintables, you can rerun the command to find more.")
|
"There may be more hintables, you can rerun the command to find more.")
|
||||||
else:
|
else:
|
||||||
self.output(f"You can't afford the hint. "
|
self.output(f"You can't afford the hint. "
|
||||||
f"You have {points_available} points and need at least "
|
f"You have {points_available} points and need at least "
|
||||||
f"{self.ctx.get_hint_cost(self.client.slot)}")
|
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||||
notify_hints(self.ctx, self.client.team, hints)
|
notify_hints(self.ctx, self.client.team, hints)
|
||||||
self.ctx.save()
|
self.ctx.save()
|
||||||
return True
|
return True
|
||||||
@@ -1050,6 +1061,22 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
self.output(response)
|
self.output(response)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@mark_raw
|
||||||
|
def _cmd_hint(self, item_or_location: str = "") -> bool:
|
||||||
|
"""Use !hint {item_name/location_name},
|
||||||
|
for example !hint Lamp or !hint Link's House to get a spoiler peek for that location or item.
|
||||||
|
If hint costs are on, this will only give you one new result,
|
||||||
|
you can rerun the command to get more in that case."""
|
||||||
|
return self.get_hints(item_or_location)
|
||||||
|
|
||||||
|
@mark_raw
|
||||||
|
def _cmd_hint_location(self, location: str = "") -> bool:
|
||||||
|
"""Use !hint_location {location_name},
|
||||||
|
for example !hint atomic-bomb to get a spoiler peek for that location.
|
||||||
|
(In the case of factorio, or any other game where item names and location names are identical,
|
||||||
|
this command must be used explicitly.)"""
|
||||||
|
return self.get_hints(location, True)
|
||||||
|
|
||||||
|
|
||||||
def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]:
|
def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]:
|
||||||
return [location_id for
|
return [location_id for
|
||||||
@@ -1176,7 +1203,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
locs = []
|
locs = []
|
||||||
for location in args["locations"]:
|
for location in args["locations"]:
|
||||||
if type(location) is not int or location not in lookup_any_location_id_to_name:
|
if type(location) is not int or location not in lookup_any_location_id_to_name:
|
||||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts'}])
|
await ctx.send_msgs(client,
|
||||||
|
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts'}])
|
||||||
return
|
return
|
||||||
target_item, target_player = ctx.locations[client.slot][location]
|
target_item, target_player = ctx.locations[client.slot][location]
|
||||||
locs.append(NetworkItem(target_item, location, target_player))
|
locs.append(NetworkItem(target_item, location, target_player))
|
||||||
@@ -1402,6 +1430,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||||||
return input_text
|
return input_text
|
||||||
setattr(self.ctx, option_name, attrtype(option))
|
setattr(self.ctx, option_name, attrtype(option))
|
||||||
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
|
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
|
||||||
|
if option_name in {"forfeit_mode", "remaining_mode"}:
|
||||||
|
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
known = (f"{option}:{otype}" for option, otype in self.ctx.simple_options.items())
|
known = (f"{option}:{otype}" for option, otype in self.ctx.simple_options.items())
|
||||||
|
|||||||
19
NetUtils.py
19
NetUtils.py
@@ -25,6 +25,25 @@ class ClientStatus(enum.IntEnum):
|
|||||||
CLIENT_GOAL = 30
|
CLIENT_GOAL = 30
|
||||||
|
|
||||||
|
|
||||||
|
class Permission(enum.IntEnum):
|
||||||
|
disabled = 0b000 # 0, completely disables access
|
||||||
|
enabled = 0b001 # 1, allows manual use
|
||||||
|
goal = 0b010 # 2, allows manual use after goal completion
|
||||||
|
auto = 0b110 # 6, forces use after goal completion, only works for forfeit
|
||||||
|
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_text(text: str):
|
||||||
|
data = 0
|
||||||
|
if "auto" in text:
|
||||||
|
data |= 0b110
|
||||||
|
elif "goal" in text:
|
||||||
|
data |= 0b010
|
||||||
|
if "enabled" in text:
|
||||||
|
data |= 0b001
|
||||||
|
return Permission(data)
|
||||||
|
|
||||||
|
|
||||||
class NetworkPlayer(typing.NamedTuple):
|
class NetworkPlayer(typing.NamedTuple):
|
||||||
team: int
|
team: int
|
||||||
slot: int
|
slot: int
|
||||||
|
|||||||
34
Options.py
34
Options.py
@@ -29,7 +29,9 @@ class AssembleOptions(type):
|
|||||||
def validate(self, *args, **kwargs):
|
def validate(self, *args, **kwargs):
|
||||||
func(self, *args, **kwargs)
|
func(self, *args, **kwargs)
|
||||||
self.value = self.schema.validate(self.value)
|
self.value = self.schema.validate(self.value)
|
||||||
|
|
||||||
return validate
|
return validate
|
||||||
|
|
||||||
attrs["__init__"] = validate_decorator(attrs["__init__"])
|
attrs["__init__"] = validate_decorator(attrs["__init__"])
|
||||||
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
||||||
|
|
||||||
@@ -179,6 +181,8 @@ class Choice(Option):
|
|||||||
return other != self.value
|
return other != self.value
|
||||||
elif isinstance(other, bool):
|
elif isinstance(other, bool):
|
||||||
return other != bool(self.value)
|
return other != bool(self.value)
|
||||||
|
elif other is None:
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||||
|
|
||||||
@@ -215,7 +219,7 @@ class Range(Option, int):
|
|||||||
return cls.from_text(str(data))
|
return cls.from_text(str(data))
|
||||||
|
|
||||||
def get_option_name(self, value):
|
def get_option_name(self, value):
|
||||||
return str(self.value)
|
return str(value)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.value)
|
return str(self.value)
|
||||||
@@ -241,9 +245,10 @@ class OptionNameSet(Option):
|
|||||||
class OptionDict(Option):
|
class OptionDict(Option):
|
||||||
default = {}
|
default = {}
|
||||||
supports_weighting = False
|
supports_weighting = False
|
||||||
|
value: typing.Dict[str, typing.Any]
|
||||||
|
|
||||||
def __init__(self, value: typing.Dict[str, typing.Any]):
|
def __init__(self, value: typing.Dict[str, typing.Any]):
|
||||||
self.value: typing.Dict[str, typing.Any] = value
|
self.value = value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
|
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
|
||||||
@@ -253,10 +258,13 @@ class OptionDict(Option):
|
|||||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||||
|
|
||||||
def get_option_name(self, value):
|
def get_option_name(self, value):
|
||||||
return ", ".join(f"{key}: {value}" for key, value in self.value.items())
|
return ", ".join(f"{key}: {v}" for key, v in value.items())
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
return item in self.value
|
||||||
|
|
||||||
|
|
||||||
class OptionList(Option, list):
|
class OptionList(Option):
|
||||||
default = []
|
default = []
|
||||||
supports_weighting = False
|
supports_weighting = False
|
||||||
value: list
|
value: list
|
||||||
@@ -276,10 +284,13 @@ class OptionList(Option, list):
|
|||||||
return cls.from_text(str(data))
|
return cls.from_text(str(data))
|
||||||
|
|
||||||
def get_option_name(self, value):
|
def get_option_name(self, value):
|
||||||
return ", ".join(self.value)
|
return ", ".join(value)
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
return item in self.value
|
||||||
|
|
||||||
|
|
||||||
class OptionSet(Option, set):
|
class OptionSet(Option):
|
||||||
default = frozenset()
|
default = frozenset()
|
||||||
supports_weighting = False
|
supports_weighting = False
|
||||||
value: set
|
value: set
|
||||||
@@ -301,7 +312,10 @@ class OptionSet(Option, set):
|
|||||||
return cls.from_text(str(data))
|
return cls.from_text(str(data))
|
||||||
|
|
||||||
def get_option_name(self, value):
|
def get_option_name(self, value):
|
||||||
return ", ".join(self.value)
|
return ", ".join(value)
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
return item in self.value
|
||||||
|
|
||||||
|
|
||||||
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
|
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
|
||||||
@@ -356,6 +370,10 @@ class StartHints(ItemSet):
|
|||||||
displayname = "Start Hints"
|
displayname = "Start Hints"
|
||||||
|
|
||||||
|
|
||||||
|
class StartLocationHints(OptionSet):
|
||||||
|
displayname = "Start Location Hints"
|
||||||
|
|
||||||
|
|
||||||
class ExcludeLocations(OptionSet):
|
class ExcludeLocations(OptionSet):
|
||||||
"""Prevent these locations from having an important item"""
|
"""Prevent these locations from having an important item"""
|
||||||
displayname = "Excluded Locations"
|
displayname = "Excluded Locations"
|
||||||
@@ -363,11 +381,11 @@ class ExcludeLocations(OptionSet):
|
|||||||
|
|
||||||
|
|
||||||
per_game_common_options = {
|
per_game_common_options = {
|
||||||
# placeholder until they're actually implemented
|
|
||||||
"local_items": LocalItems,
|
"local_items": LocalItems,
|
||||||
"non_local_items": NonLocalItems,
|
"non_local_items": NonLocalItems,
|
||||||
"start_inventory": StartInventory,
|
"start_inventory": StartInventory,
|
||||||
"start_hints": StartHints,
|
"start_hints": StartHints,
|
||||||
|
"start_location_hints": StartLocationHints,
|
||||||
"exclude_locations": OptionSet
|
"exclude_locations": OptionSet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ Currently, the following games are supported:
|
|||||||
* Slay the Spire
|
* Slay the Spire
|
||||||
* Risk of Rain 2
|
* Risk of Rain 2
|
||||||
* The Legend of Zelda: Ocarina of Time
|
* The Legend of Zelda: Ocarina of Time
|
||||||
|
* Timespinner
|
||||||
|
|
||||||
For setup and instructions check out our [tutorials page](http://archipelago.gg:48484/tutorial).
|
For setup and instructions check out our [tutorials page](/tutorial).
|
||||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||||
windows binaries.
|
windows binaries.
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ If you are running Archipelago from a non-Windows system then the likely scenari
|
|||||||
## Related Repositories
|
## Related Repositories
|
||||||
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.
|
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.
|
||||||
|
|
||||||
* [z3randomizer](https://github.com/CaitSith2/z3randomizer)
|
* [z3randomizer](https://github.com/ArchipelagoMW/z3randomizer)
|
||||||
* [Enemizer](https://github.com/Ijwu/Enemizer)
|
* [Enemizer](https://github.com/Ijwu/Enemizer)
|
||||||
* [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer)
|
* [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer)
|
||||||
|
|
||||||
|
|||||||
2
Utils.py
2
Utils.py
@@ -13,7 +13,7 @@ class Version(typing.NamedTuple):
|
|||||||
build: int
|
build: int
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.1.8"
|
__version__ = "0.1.9"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
import builtins
|
import builtins
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ def page_not_found(err):
|
|||||||
return render_template('404.html'), 404
|
return render_template('404.html'), 404
|
||||||
|
|
||||||
|
|
||||||
|
# Start Playing Page
|
||||||
|
@app.route('/start-playing')
|
||||||
|
def start_playing():
|
||||||
|
return render_template(f"startPlaying.html")
|
||||||
|
|
||||||
|
|
||||||
# Player settings pages
|
# Player settings pages
|
||||||
@app.route('/games/<string:game>/player-settings')
|
@app.route('/games/<string:game>/player-settings')
|
||||||
def player_settings(game):
|
def player_settings(game):
|
||||||
@@ -180,6 +186,9 @@ def favicon():
|
|||||||
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
||||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||||
|
|
||||||
|
@app.route('/discord')
|
||||||
|
def discord():
|
||||||
|
return redirect("https://discord.gg/archipelago")
|
||||||
|
|
||||||
from WebHostLib.customserver import run_server_process
|
from WebHostLib.customserver import run_server_process
|
||||||
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
|
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
|||||||
options = restricted_loads(generation.options)
|
options = restricted_loads(generation.options)
|
||||||
logging.info(f"Generating {generation.id} for {len(options)} players")
|
logging.info(f"Generating {generation.id} for {len(options)} players")
|
||||||
pool.apply_async(gen_game, (options,),
|
pool.apply_async(gen_game, (options,),
|
||||||
{"race": meta["race"],
|
{"meta": meta,
|
||||||
"sid": generation.id,
|
"sid": generation.id,
|
||||||
"owner": generation.owner},
|
"owner": generation.owner},
|
||||||
handle_generation_success, handle_generation_failure)
|
handle_generation_success, handle_generation_failure)
|
||||||
|
|||||||
@@ -49,9 +49,7 @@ def get_yaml_data(file) -> Union[Dict[str, str], str]:
|
|||||||
for file in infolist:
|
for file in infolist:
|
||||||
if file.filename.endswith(banned_zip_contents):
|
if file.filename.endswith(banned_zip_contents):
|
||||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
|
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
|
||||||
elif file.filename.endswith(".yaml"):
|
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
|
||||||
options[file.filename] = zfile.open(file, "r").read()
|
|
||||||
elif file.filename.endswith(".txt"):
|
|
||||||
options[file.filename] = zfile.open(file, "r").read()
|
options[file.filename] = zfile.open(file, "r").read()
|
||||||
else:
|
else:
|
||||||
options = {file.filename: file.read()}
|
options = {file.filename: file.read()}
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import random
|
|||||||
import json
|
import json
|
||||||
import zipfile
|
import zipfile
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
from typing import Dict, Optional as TypeOptional
|
||||||
|
|
||||||
from flask import request, flash, redirect, url_for, session, render_template
|
from flask import request, flash, redirect, url_for, session, render_template
|
||||||
|
|
||||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||||
from Main import main as ERmain
|
from Main import main as ERmain
|
||||||
from Main import get_seed, seeddigits
|
from BaseClasses import seeddigits, get_seed
|
||||||
from Generate import handle_name
|
from Generate import handle_name
|
||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
@@ -33,6 +34,14 @@ def generate(race=False):
|
|||||||
flash(options)
|
flash(options)
|
||||||
else:
|
else:
|
||||||
results, gen_options = roll_options(options)
|
results, gen_options = roll_options(options)
|
||||||
|
# get form data -> server settings
|
||||||
|
hint_cost = int(request.form.get("hint_cost", 10))
|
||||||
|
forfeit_mode = request.form.get("forfeit_mode", "goal")
|
||||||
|
meta = {"race": race, "hint_cost": hint_cost, "forfeit_mode": forfeit_mode}
|
||||||
|
if race:
|
||||||
|
meta["item_cheat"] = False
|
||||||
|
meta["remaining"] = False
|
||||||
|
|
||||||
if any(type(result) == str for result in results.values()):
|
if any(type(result) == str for result in results.values()):
|
||||||
return render_template("checkResult.html", results=results)
|
return render_template("checkResult.html", results=results)
|
||||||
elif len(gen_options) > app.config["MAX_ROLL"]:
|
elif len(gen_options) > app.config["MAX_ROLL"]:
|
||||||
@@ -42,7 +51,8 @@ def generate(race=False):
|
|||||||
gen = Generation(
|
gen = Generation(
|
||||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||||
# convert to json compatible
|
# convert to json compatible
|
||||||
meta=json.dumps({"race": race}), state=STATE_QUEUED,
|
meta=json.dumps(meta),
|
||||||
|
state=STATE_QUEUED,
|
||||||
owner=session["_id"])
|
owner=session["_id"])
|
||||||
commit()
|
commit()
|
||||||
|
|
||||||
@@ -50,18 +60,24 @@ def generate(race=False):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
||||||
race=race, owner=session["_id"].int)
|
meta=meta, owner=session["_id"].int)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
from .autolauncher import handle_generation_failure
|
from .autolauncher import handle_generation_failure
|
||||||
handle_generation_failure(e)
|
handle_generation_failure(e)
|
||||||
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": "+ str(e)))
|
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
|
||||||
|
|
||||||
return redirect(url_for("viewSeed", seed=seed_id))
|
return redirect(url_for("viewSeed", seed=seed_id))
|
||||||
|
|
||||||
return render_template("generate.html", race=race)
|
return render_template("generate.html", race=race)
|
||||||
|
|
||||||
|
|
||||||
def gen_game(gen_options, race=False, owner=None, sid=None):
|
def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=None, sid=None):
|
||||||
|
if not meta:
|
||||||
|
meta: Dict[str, object] = {}
|
||||||
|
|
||||||
|
meta.setdefault("hint_cost", 10)
|
||||||
|
race = meta.get("race", False)
|
||||||
|
del (meta["race"])
|
||||||
try:
|
try:
|
||||||
target = tempfile.TemporaryDirectory()
|
target = tempfile.TemporaryDirectory()
|
||||||
playercount = len(gen_options)
|
playercount = len(gen_options)
|
||||||
@@ -95,7 +111,7 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
|||||||
erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0]
|
erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0]
|
||||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||||
|
|
||||||
ERmain(erargs, seed)
|
ERmain(erargs, seed, baked_server_options=meta)
|
||||||
|
|
||||||
return upload_to_db(target.name, sid, owner, race)
|
return upload_to_db(target.name, sid, owner, race)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
@@ -105,7 +121,7 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
|||||||
if gen is not None:
|
if gen is not None:
|
||||||
gen.state = STATE_ERROR
|
gen.state = STATE_ERROR
|
||||||
meta = json.loads(gen.meta)
|
meta = json.loads(gen.meta)
|
||||||
meta["error"] = (e.__class__.__name__ + ": "+ str(e))
|
meta["error"] = (e.__class__.__name__ + ": " + str(e))
|
||||||
gen.meta = json.dumps(meta)
|
gen.meta = json.dumps(meta)
|
||||||
|
|
||||||
commit()
|
commit()
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ def create():
|
|||||||
dictify_range=dictify_range, default_converter=default_converter,
|
dictify_range=dictify_range, default_converter=default_converter,
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(os.path.join(target_folder, game_name + ".yaml"), "w") as f:
|
if not os.path.isdir(os.path.join(target_folder, 'configs')):
|
||||||
|
os.mkdir(os.path.join(target_folder, 'configs'))
|
||||||
|
|
||||||
|
with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f:
|
||||||
f.write(res)
|
f.write(res)
|
||||||
|
|
||||||
# Generate JSON files for player-settings pages
|
# Generate JSON files for player-settings pages
|
||||||
@@ -78,5 +81,8 @@ def create():
|
|||||||
|
|
||||||
player_settings["gameOptions"] = game_options
|
player_settings["gameOptions"] = game_options
|
||||||
|
|
||||||
with open(os.path.join(target_folder, game_name + ".json"), "w") as f:
|
if not os.path.isdir(os.path.join(target_folder, 'player-settings')):
|
||||||
|
os.mkdir(os.path.join(target_folder, 'player-settings'))
|
||||||
|
|
||||||
|
with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f:
|
||||||
f.write(json.dumps(player_settings, indent=2, separators=(',', ': ')))
|
f.write(json.dumps(player_settings, indent=2, separators=(',', ': ')))
|
||||||
|
|||||||
28
WebHostLib/static/assets/gameInfo/en_Timespinner.md
Normal file
28
WebHostLib/static/assets/gameInfo/en_Timespinner.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Timespinner
|
||||||
|
|
||||||
|
## Where is the settings page?
|
||||||
|
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||||
|
you need to configure and export a config file.
|
||||||
|
|
||||||
|
## What does randomization do to this game?
|
||||||
|
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game
|
||||||
|
is always able to be completed, but because of the item shuffle the player may need to access certain areas before
|
||||||
|
they would in the vanilla game. All rings and spells are also randomized into those item locations, therefor you can no longer craft them at the alchemist
|
||||||
|
|
||||||
|
## What is the goal of Timespinner when randomized?
|
||||||
|
The goal remains unchanged. Kill the Sandman\Nightmare!
|
||||||
|
|
||||||
|
## What items and locations get shuffled?
|
||||||
|
All main inventory items, orbs, collectables, and familiers can be shuffled, and all locations in the game which could
|
||||||
|
contain any of those items may have their contents changed.
|
||||||
|
|
||||||
|
## Which items can be in another player's world?
|
||||||
|
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to
|
||||||
|
limit certain items to your own world.
|
||||||
|
|
||||||
|
## What does another world's item look like in Timespinner?
|
||||||
|
Items belonging to other worlds are represented by the vanilla item [Elemental Beads](https://timespinnerwiki.com/Use_Items), Elemental Beads have no use in the randomizer
|
||||||
|
|
||||||
|
## When the player receives an item, what happens?
|
||||||
|
When the player receives an item, the same items popup will be displayed as when you would normally obtain the item
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ const fetchSettingData = () => new Promise((resolve, reject) => {
|
|||||||
try{ resolve(JSON.parse(ajax.responseText)); }
|
try{ resolve(JSON.parse(ajax.responseText)); }
|
||||||
catch(error){ reject(error); }
|
catch(error){ reject(error); }
|
||||||
};
|
};
|
||||||
ajax.open('GET', `${window.location.origin}/static/generated/${gameName}.json`, true);
|
ajax.open('GET', `${window.location.origin}/static/generated/player-settings/${gameName}.json`, true);
|
||||||
ajax.send();
|
ajax.send();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
27
WebHostLib/static/assets/tutorial/archipelago/setup_en.md
Normal file
27
WebHostLib/static/assets/tutorial/archipelago/setup_en.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Archipelago Setup Guide
|
||||||
|
|
||||||
|
## Installing the Archipelago software
|
||||||
|
The most recent public release of Archipelago can be found [here](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||||
|
Run the exe file, and after accepting the license agreement you will be prompted on which components you would like to install. The generator allows you to generate multiworld games on your computer. The ROM setups are optional but are required if anyone in the game that you generate wants to play any of those games as they are needed to generate the relevant patch files. The server will allow you to host the multiworld on your machine but this also requires you to port forward. The default port for Archipelago is `38281` If you are unsure how to do this there are plenty of other guides on the internet that will be more suited to your hardware. The Clients are what you use to connect your game to the multiworld. If the game/games you plan to play are available here go ahead and install these as well. If the game you choose to play is supported by Archipelago but not listed check the relevant tutorial.
|
||||||
|
|
||||||
|
## Generating a game
|
||||||
|
### Gather all player YAMLS
|
||||||
|
All players that wish to play in the generated multiworld must have a YAML file which contains all of the settings that they wish to play with.
|
||||||
|
A YAML is a file which contains human readable markup. In other words, this is a settings file kind of like an INI file or a TOML file.
|
||||||
|
Each player can go to the game's player settings page in order to determine the settings how they want them and then download a YAML file containing these settings.
|
||||||
|
After getting all of the YAML files these can all either be placed together in the `Archipelago\Players` folder or compressed into a ZIP folder to then be uploaded to the [website generator](/generate).
|
||||||
|
If rolling locally ensure that the folder is clear of any files you do not wish to include in the game such as the included default player settings files.
|
||||||
|
|
||||||
|
### Rolling the seed
|
||||||
|
After gathering all of the YAML files together in the `Archipelago\Players` folder, run the program `ArchipelagoGenerate.exe` in the base `Archipelago` folder. This will then open a console window and either silently close itself or spit out an error. If you receive an error, it is likely due to an error in the YAML file. If the error is unhelpful in you figuring out the issue asking in the ***#tech-support*** channel of our Discord. The generator will put a zip folder into your `Archipelago\output` folder with the format `AP_XXXXXXXXX`.zip. This contains all of the patch files and relevant mods for the players as well as the serverdata for the host.
|
||||||
|
|
||||||
|
### Changing host settings
|
||||||
|
Sometimes there are various settings that you may want to change before rolling a seed such as enabling race mode, auto-forfeit, or set a password. All of these settings plus other options are able to be changed by modifying the `host.yaml` file in the base `Archipelago` folder.
|
||||||
|
|
||||||
|
## Hosting a multiworld
|
||||||
|
### Uploading the seed to the website
|
||||||
|
The easiest and most recommended method is to upload the zip file that you generated to the website [here](/uploads). This will give a page with the seed info and have a link to the spoiler if it exists. Click on Create New room and then share the link fo rhe room with the other players so that they can download their patches or mods. The room will also have a link to a Multiworld Tracker and tell you what the players need to connect to from their clients.
|
||||||
|
|
||||||
|
### Hosting a seed locally
|
||||||
|
For this we'll assume you have already port forwarding `38281` and have generated a seed that is still in the `outputs` folder. Next, you'll want to run `ArchipelagoServer.exe`. A window will open in order to open the multiworld data for the game. You can either use the generated zip folder or extract the .archipelago file and use it. If everything worked correctly the console window should tell you it's now hosting a game with the IP, port, and password that clients will need in order to connect.
|
||||||
|
Extract the patch and mod files then send those to your friends and you're done!
|
||||||
@@ -4,13 +4,14 @@
|
|||||||
### Install r2modman
|
### Install r2modman
|
||||||
Head on over to the r2modman page on Thunderstore and follow the installation instructions.
|
Head on over to the r2modman page on Thunderstore and follow the installation instructions.
|
||||||
|
|
||||||
https://thunderstore.io/package/ebkr/r2modman/
|
[https://thunderstore.io/package/ebkr/r2modman/](https://thunderstore.io/package/ebkr/r2modman/)
|
||||||
|
|
||||||
### Install Archipelago Mod using r2modman
|
### Install Archipelago Mod using r2modman
|
||||||
You can install the Archipelago mod using r2modman in one of two ways.
|
You can install the Archipelago mod using r2modman in one of two ways.
|
||||||
One, you can use the Thunderstore website and click on the "Install with Mod Manager" link.
|
|
||||||
|
|
||||||
https://thunderstore.io/package/ArchipelagoMW/Archipelago/
|
[https://thunderstore.io/package/ArchipelagoMW/Archipelago/](https://thunderstore.io/package/ArchipelagoMW/Archipelago/)
|
||||||
|
|
||||||
|
One, you can use the Thunderstore website and click on the "Install with Mod Manager" link.
|
||||||
|
|
||||||
You can also search for the "Archipelago" mod in the r2modman interface.
|
You can also search for the "Archipelago" mod in the r2modman interface.
|
||||||
The mod manager should automatically install all necessary dependencies as well.
|
The mod manager should automatically install all necessary dependencies as well.
|
||||||
@@ -72,7 +73,7 @@ Risk of Rain 2:
|
|||||||
|
|
||||||
| Name | Description | Allowed values |
|
| Name | Description | Allowed values |
|
||||||
| ---- | ----------- | -------------- |
|
| ---- | ----------- | -------------- |
|
||||||
| total_locations | The total number of location checks that will be attributed to the Risk of Rain player. This option is ALSO the total number of items in the item pool for the Risk of Rain player. | 10 - 50 |
|
| total_locations | The total number of location checks that will be attributed to the Risk of Rain player. This option is ALSO the total number of items in the item pool for the Risk of Rain player. | 10 - 100 |
|
||||||
| total_revivals | The total number of items in the Risk of Rain player's item pool (items other players pick up for them) replaced with `Dio's Best Friend`. | 0 - 5 |
|
| total_revivals | The total number of items in the Risk of Rain player's item pool (items other players pick up for them) replaced with `Dio's Best Friend`. | 0 - 5 |
|
||||||
| start_with_revive | Starts the player off with a `Dio's Best Friend`. Functionally equivalent to putting a `Dio's Best Friend` in your `starting_inventory`. | true/false |
|
| start_with_revive | Starts the player off with a `Dio's Best Friend`. Functionally equivalent to putting a `Dio's Best Friend` in your `starting_inventory`. | true/false |
|
||||||
| item_pickup_step | The number of item pickups which you are allowed to claim before they become an Archipelago location check. | 0 - 5 |
|
| item_pickup_step | The number of item pickups which you are allowed to claim before they become an Archipelago location check. | 0 - 5 |
|
||||||
|
|||||||
60
WebHostLib/static/assets/tutorial/timespinner/setup_en.md
Normal file
60
WebHostLib/static/assets/tutorial/timespinner/setup_en.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Timespinner Randomizer Setup Guide
|
||||||
|
|
||||||
|
## Required Software
|
||||||
|
|
||||||
|
- [Timespinner (steam)](https://store.steampowered.com/app/368620/Timespinner/) or [Timespinner (drm free)](https://www.humblebundle.com/store/timespinner)
|
||||||
|
- [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer)
|
||||||
|
|
||||||
|
## General Concept
|
||||||
|
|
||||||
|
The timespinner Randomizer loads Timespinner.exe from the same folder, and alters its state in memory to allow for randomization of the items
|
||||||
|
|
||||||
|
## Installation Procedures
|
||||||
|
|
||||||
|
Download latest version of [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer) you can find the .zip files on the releases page, download the zip for your current platform. Then extract the zip to the folder where your Timespinner game is installed. Then just run TsRandomizer.exe instead of Timespinner.exe to start the game in randomized mode, for more info see the [readme](https://github.com/JarnoWesthof/TsRandomizer)
|
||||||
|
|
||||||
|
## Joining a MultiWorld Game
|
||||||
|
|
||||||
|
1. Run TsRandomizer.exe
|
||||||
|
2. Select "New Game"
|
||||||
|
3. Switch "<< Select Seed >>" to "<< Archiplago >>" by pressing left on the controller or keyboard
|
||||||
|
4. Select "<< Archiplago >>" to open a new menu where you can enter your Archipelago login credentails
|
||||||
|
* NOTE: the input fields support Ctrl + V pasting of values
|
||||||
|
5. Select "Connect"
|
||||||
|
6. If all went well you will be taken back the difficulty selection menu and the game will start as soon as you select a difficulty
|
||||||
|
|
||||||
|
## YAML Settings
|
||||||
|
An example YAML would look like this:
|
||||||
|
```yaml
|
||||||
|
description: Default Timespinner Template
|
||||||
|
name: Lunais{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
||||||
|
game:
|
||||||
|
Timespinner: 1
|
||||||
|
requires:
|
||||||
|
version: 0.1.8
|
||||||
|
Timespinner:
|
||||||
|
StartWithJewelryBox: # Start with Jewelry Box unlocked
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
DownloadableItems: # With the tablet you will be able to download items at terminals
|
||||||
|
false: 50
|
||||||
|
true: 50
|
||||||
|
FacebookMode: # Requires Oculus Rift(ng) to spot the weakspots in walls and floors
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
StartWithMeyef: # Start with Meyef, ideal for when you want to play multiplayer
|
||||||
|
false: 50
|
||||||
|
true: 50
|
||||||
|
QuickSeed: # Start with Talaria Attachment, Nyoom!
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
SpecificKeycards: # Keycards can only open corresponding doors
|
||||||
|
false: 0
|
||||||
|
true: 50
|
||||||
|
Inverted: # Start in the past
|
||||||
|
false: 50
|
||||||
|
true: 50
|
||||||
|
```
|
||||||
|
* All Options are either enabled or not, if values are specified for both true & false the generator will select one based on weight
|
||||||
|
* The Timespinner Randomizer option "StinkyMaw" is currently always enabled for Archipelago generated seeds
|
||||||
|
* The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported on Archipelago generated seeds
|
||||||
@@ -1,4 +1,23 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"gameTitle": "Archipelago",
|
||||||
|
"tutorials": [
|
||||||
|
{
|
||||||
|
"name": "Multiworld Setup Tutorial",
|
||||||
|
"description": "A Guide to setting up the Archipelago software to generate multiworld games on your computer.",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"language": "English",
|
||||||
|
"filename": "archipelago/setup_en.md",
|
||||||
|
"link": "archipelago/setup/en",
|
||||||
|
"authors": [
|
||||||
|
"alwaysintreble"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"gameTitle": "The Legend of Zelda: A Link to the Past",
|
"gameTitle": "The Legend of Zelda: A Link to the Past",
|
||||||
"tutorials": [
|
"tutorials": [
|
||||||
@@ -86,6 +105,33 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"gameTitle": "The Legend of Zelda: Ocarina of Time",
|
||||||
|
"tutorials": [
|
||||||
|
{
|
||||||
|
"name": "Multiworld Setup Tutorial",
|
||||||
|
"description": "A guide to setting up the Archipelago Ocarina of Time software on your computer.",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"language": "English",
|
||||||
|
"filename": "zelda5/setup_en.md",
|
||||||
|
"link": "zelda5/setup/en",
|
||||||
|
"authors": [
|
||||||
|
"Edos"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "Spanish",
|
||||||
|
"filename": "zelda5/setup_es.md",
|
||||||
|
"link": "zelda5/setup/es",
|
||||||
|
"authors": [
|
||||||
|
"Edos"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"gameTitle": "Factorio",
|
"gameTitle": "Factorio",
|
||||||
"tutorials": [
|
"tutorials": [
|
||||||
@@ -158,5 +204,24 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"gameTitle": "Timespinner",
|
||||||
|
"tutorials": [
|
||||||
|
{
|
||||||
|
"name": "Multiworld Setup Guide",
|
||||||
|
"description": "A guide to setting up the Timespinner randomizer connected to an Archipelago Multiworld",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"language": "English",
|
||||||
|
"filename": "timespinner/setup_en.md",
|
||||||
|
"link": "timespinner/setup/en",
|
||||||
|
"authors": [
|
||||||
|
"Jarno"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ If you would like to validate your YAML file to make sure it works, you may do s
|
|||||||
[YAML Validator](/mysterycheck) page.
|
[YAML Validator](/mysterycheck) page.
|
||||||
|
|
||||||
## Generating a Single-Player Game
|
## Generating a Single-Player Game
|
||||||
1. Navigate to the [Generate Game](/player-settings), configure your options, and click the "Generate Game" button.
|
1. Navigate to the [Player Settings](/games/A%20Link%20to%20the%20Past/player-settings) page, configure your options, and click the "Generate Game" button.
|
||||||
2. You will be presented with a "Seed Info" page, where you can download your patch file.
|
2. You will be presented with a "Seed Info" page, where you can download your patch file.
|
||||||
3. Double-click on your patch file, and the emulator should launch with your game automatically. As the
|
3. Double-click on your patch file, and the emulator should launch with your game automatically. As the
|
||||||
Client is unnecessary for single player games, you may close it and the WebUI.
|
Client is unnecessary for single player games, you may close it and the WebUI.
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ Cada jugador en una partida de multiworld proveerá su propio fichero YAML. Esta
|
|||||||
que cada jugador disfrute de una experiencia personalizada a su gusto, y cada jugador dentro de la misma partida de multiworld puede tener diferentes opciones.
|
que cada jugador disfrute de una experiencia personalizada a su gusto, y cada jugador dentro de la misma partida de multiworld puede tener diferentes opciones.
|
||||||
|
|
||||||
### Donde puedo obtener un fichero YAML?
|
### Donde puedo obtener un fichero YAML?
|
||||||
La página "[Generate Game](/player-settings)" en el sitio web te permite configurar tu configuración personal y
|
La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-settings)" en el sitio web te permite configurar tu configuración personal y
|
||||||
descargar un fichero "YAML".
|
descargar un fichero "YAML".
|
||||||
|
|
||||||
### Configuración YAML avanzada
|
### Configuración YAML avanzada
|
||||||
@@ -67,7 +67,7 @@ Si quieres validar que tu fichero YAML para asegurarte que funciona correctament
|
|||||||
[YAML Validator](/mysterycheck).
|
[YAML Validator](/mysterycheck).
|
||||||
|
|
||||||
## Generar una partida para un jugador
|
## Generar una partida para un jugador
|
||||||
1. Navega a [la pagina Generate game](/player-settings), configura tus opciones, haz click en el boton "Generate game".
|
1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-settings), configura tus opciones, haz click en el boton "Generate game".
|
||||||
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
|
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
|
||||||
3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el
|
3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el
|
||||||
Cliente no es necesario para partidas de un jugador, puedes cerrarlo junto a la pagina web (que tiene como titulo "Multiworld WebUI") que se ha abierto automáticamente.
|
Cliente no es necesario para partidas de un jugador, puedes cerrarlo junto a la pagina web (que tiene como titulo "Multiworld WebUI") que se ha abierto automáticamente.
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ sur comment il devrait générer votre seed. Chaque joueur d'un multiwolrd devra
|
|||||||
joueur d'apprécier une expérience customisée selon ses goûts, et les différents joueurs d'un même multiworld peuvent avoir différentes options.
|
joueur d'apprécier une expérience customisée selon ses goûts, et les différents joueurs d'un même multiworld peuvent avoir différentes options.
|
||||||
|
|
||||||
### Où est-ce que j'obtiens un fichier YAML ?
|
### Où est-ce que j'obtiens un fichier YAML ?
|
||||||
La page [Génération de partie](/player-settings) vous permet de configurer vos paramètres personnels et de les exporter vers un fichier YAML.
|
La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings) vous permet de configurer vos paramètres personnels et de les exporter vers un fichier YAML.
|
||||||
|
|
||||||
### Configuration avancée du fichier YAML
|
### Configuration avancée du fichier YAML
|
||||||
Une version plus avancée du fichier YAML peut être créée en utilisant la page des [paramètres de pondération](/weighted-settings), qui vous permet
|
Une version plus avancée du fichier YAML peut être créée en utilisant la page des [paramètres de pondération](/weighted-settings), qui vous permet
|
||||||
@@ -71,7 +71,7 @@ Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous
|
|||||||
[Validateur de YAML](/mysterycheck).
|
[Validateur de YAML](/mysterycheck).
|
||||||
|
|
||||||
## Générer une partie pour un joueur
|
## Générer une partie pour un joueur
|
||||||
1. Aller sur la page [Génération de partie](/player-settings), configurez vos options, et cliquez sur le bouton "Generate Game".
|
1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings), configurez vos options, et cliquez sur le bouton "Generate Game".
|
||||||
2. Il vous sera alors présenté une page d'informations sur la seed, où vous pourrez télécharger votre patch.
|
2. Il vous sera alors présenté une page d'informations sur la seed, où vous pourrez télécharger votre patch.
|
||||||
3. Double-cliquez sur le patch et l'émulateur devrait se lancer automatiquement avec la seed. Etant donné que le client
|
3. Double-cliquez sur le patch et l'émulateur devrait se lancer automatiquement avec la seed. Etant donné que le client
|
||||||
n'est pas requis pour les parties à un joueur, vous pouvez le fermer ainsi que l'interface Web (WebUI).
|
n'est pas requis pour les parties à un joueur, vous pouvez le fermer ainsi que l'interface Web (WebUI).
|
||||||
|
|||||||
387
WebHostLib/static/assets/tutorial/zelda5/setup_en.md
Normal file
387
WebHostLib/static/assets/tutorial/zelda5/setup_en.md
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
# Setup Guide for Ocarina of time Archipelago
|
||||||
|
|
||||||
|
## Important
|
||||||
|
|
||||||
|
As we are using Z5Client and Bizhawk, this guide is only applicable to Windows.
|
||||||
|
|
||||||
|
## Required Software
|
||||||
|
|
||||||
|
- [bizhawk+script+Z5Client](https://github.com/ArchipelagoMW/Z5Client/releases) We recommend download Z5Client-setup as it makes some steps automatic.
|
||||||
|
|
||||||
|
## Install Emulator and client
|
||||||
|
|
||||||
|
Download getBizhawk.ps1 from previous link. Place it on the folder where you want your emulator to be installed, right click on it and select "Run with PowerShell". This will download all the needed dependencies used by the emulator. This can take a while.
|
||||||
|
|
||||||
|
It is strongly recommended to associate N64 rom extension (\*.n64) to the Bizhawk we've just installed. To do so, we simple have to search any N64 rom we happened to own, right click and select "Open with...", we unfold the list that appears and select the bottom option "Look for another application", we browse to Bizhawk folder and select EmuHawk.exe
|
||||||
|
|
||||||
|
Place the ootMulti.lua file from the previous link inside the "lua" folder from the just installed emulator.
|
||||||
|
|
||||||
|
Install the Z5Client using its setup.
|
||||||
|
|
||||||
|
## Configuring your YAML file
|
||||||
|
|
||||||
|
### What is a YAML file and why do I need one?
|
||||||
|
Your YAML file contains a set of configuration options which provide the generator with information about how
|
||||||
|
it should generate your game. Each player of a multiworld will provide their own YAML file. This setup allows
|
||||||
|
each player to enjoy an experience customized for their taste, and different players in the same multiworld
|
||||||
|
can all have different options.
|
||||||
|
|
||||||
|
### Where do I get a YAML file?
|
||||||
|
|
||||||
|
A basic OOT yaml will look like this. (There are lots of cosmetic options that have been removed for the sake of this tutorial, if you want to see a complete list, download (Archipelago)[https://github.com/ArchipelagoMW/Archipelago/releases] and look for the sample file in the "Players" folder))
|
||||||
|
```yaml
|
||||||
|
description: Default Ocarina of Time Template # Used to describe your yaml. Useful if you have multiple files
|
||||||
|
# Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
||||||
|
name: YourName
|
||||||
|
game:
|
||||||
|
Ocarina of Time: 1
|
||||||
|
requires:
|
||||||
|
version: 0.1.7 # Version of Archipelago required for this yaml to work as expected.
|
||||||
|
# Shared Options supported by all games:
|
||||||
|
accessibility:
|
||||||
|
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
|
||||||
|
locations: 50 # Guarantees you will be able to access all locations, and therefore all items
|
||||||
|
none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items
|
||||||
|
progression_balancing:
|
||||||
|
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do
|
||||||
|
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
|
||||||
|
Ocarina of Time:
|
||||||
|
logic_rules: # Set the logic used for the generator.
|
||||||
|
glitchless: 50
|
||||||
|
glitched: 0
|
||||||
|
no_logic: 0
|
||||||
|
logic_no_night_tokens_without_suns_song: # Nighttime skulltulas will logically require Sun's Song.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
open_forest: # Set the state of Kokiri Forest and the path to Deku Tree.
|
||||||
|
open: 50
|
||||||
|
closed_deku: 0
|
||||||
|
closed: 0
|
||||||
|
open_kakariko: # Set the state of the Kakariko Village gate.
|
||||||
|
open: 50
|
||||||
|
zelda: 0
|
||||||
|
closed: 0
|
||||||
|
open_door_of_time: # Open the Door of Time by default, without the Song of Time.
|
||||||
|
false: 0
|
||||||
|
true: 50
|
||||||
|
zora_fountain: # Set the state of King Zora, blocking the way to Zora's Fountain.
|
||||||
|
open: 0
|
||||||
|
adult: 0
|
||||||
|
closed: 50
|
||||||
|
gerudo_fortress: # Set the requirements for access to Gerudo Fortress.
|
||||||
|
normal: 0
|
||||||
|
fast: 50
|
||||||
|
open: 0
|
||||||
|
bridge: # Set the requirements for the Rainbow Bridge.
|
||||||
|
open: 0
|
||||||
|
vanilla: 0
|
||||||
|
stones: 0
|
||||||
|
medallions: 50
|
||||||
|
dungeons: 0
|
||||||
|
tokens: 0
|
||||||
|
trials: # Set the number of required trials in Ganon's Castle.
|
||||||
|
# you can add additional values between minimum and maximum
|
||||||
|
0: 50 # minimum value
|
||||||
|
6: 0 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
starting_age: # Choose which age Link will start as.
|
||||||
|
child: 50
|
||||||
|
adult: 0
|
||||||
|
triforce_hunt: # Gather pieces of the Triforce scattered around the world to complete the game.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
triforce_goal: # Number of Triforce pieces required to complete the game. Total number placed determined by the Item Pool setting.
|
||||||
|
# you can add additional values between minimum and maximum
|
||||||
|
1: 0 # minimum value
|
||||||
|
50: 0 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
20: 50
|
||||||
|
bombchus_in_logic: # Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell refills; bombchus open Bombchu Bowling.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
bridge_stones: # Set the number of Spiritual Stones required for the rainbow bridge.
|
||||||
|
# you can add additional values between minimum and maximum
|
||||||
|
0: 0 # minimum value
|
||||||
|
3: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
bridge_medallions: # Set the number of medallions required for the rainbow bridge.
|
||||||
|
# you can add additional values between minimum and maximum
|
||||||
|
0: 0 # minimum value
|
||||||
|
6: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
bridge_rewards: # Set the number of dungeon rewards required for the rainbow bridge.
|
||||||
|
# you can add additional values between minimum and maximum
|
||||||
|
0: 0 # minimum value
|
||||||
|
9: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
bridge_tokens: # Set the number of Gold Skulltula Tokens required for the rainbow bridge.
|
||||||
|
# you can add additional values between minimum and maximum
|
||||||
|
0: 0 # minimum value
|
||||||
|
100: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
shuffle_mapcompass: # Control where to shuffle dungeon maps and compasses.
|
||||||
|
remove: 0
|
||||||
|
startwith: 50
|
||||||
|
vanilla: 0
|
||||||
|
dungeon: 0
|
||||||
|
overworld: 0
|
||||||
|
any_dungeon: 0
|
||||||
|
keysanity: 0
|
||||||
|
shuffle_smallkeys: # Control where to shuffle dungeon small keys.
|
||||||
|
remove: 0
|
||||||
|
vanilla: 0
|
||||||
|
dungeon: 50
|
||||||
|
overworld: 0
|
||||||
|
any_dungeon: 0
|
||||||
|
keysanity: 0
|
||||||
|
shuffle_fortresskeys: # Control where to shuffle the Gerudo Fortress small keys.
|
||||||
|
vanilla: 50
|
||||||
|
overworld: 0
|
||||||
|
any_dungeon: 0
|
||||||
|
keysanity: 0
|
||||||
|
shuffle_bosskeys: # Control where to shuffle boss keys, except the Ganon's Castle Boss Key.
|
||||||
|
remove: 0
|
||||||
|
vanilla: 0
|
||||||
|
dungeon: 50
|
||||||
|
overworld: 0
|
||||||
|
any_dungeon: 0
|
||||||
|
keysanity: 0
|
||||||
|
shuffle_ganon_bosskey: # Control where to shuffle the Ganon's Castle Boss Key.
|
||||||
|
remove: 50
|
||||||
|
vanilla: 0
|
||||||
|
dungeon: 0
|
||||||
|
overworld: 0
|
||||||
|
any_dungeon: 0
|
||||||
|
keysanity: 0
|
||||||
|
on_lacs: 0
|
||||||
|
enhance_map_compass: # Map tells if a dungeon is vanilla or MQ. Compass tells what the dungeon reward is.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
lacs_condition: # Set the requirements for the Light Arrow Cutscene in the Temple of Time.
|
||||||
|
vanilla: 50
|
||||||
|
stones: 0
|
||||||
|
medallions: 0
|
||||||
|
dungeons: 0
|
||||||
|
tokens: 0
|
||||||
|
lacs_stones: # Set the number of Spiritual Stones required for LACS.
|
||||||
|
# you can add additional values between minimum and maximum
|
||||||
|
0: 0 # minimum value
|
||||||
|
3: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
lacs_medallions: # Set the number of medallions required for LACS.
|
||||||
|
# you can add additional values between minimum and maximum
|
||||||
|
0: 0 # minimum value
|
||||||
|
6: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
lacs_rewards: # Set the number of dungeon rewards required for LACS.
|
||||||
|
# you can add additional values between minimum and maximum
|
||||||
|
0: 0 # minimum value
|
||||||
|
9: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
lacs_tokens: # Set the number of Gold Skulltula Tokens required for LACS.
|
||||||
|
# you can add additional values between minimum and maximum
|
||||||
|
0: 0 # minimum value
|
||||||
|
100: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
shuffle_song_items: # Set where songs can appear.
|
||||||
|
song: 50
|
||||||
|
dungeon: 0
|
||||||
|
any: 0
|
||||||
|
shopsanity: # Randomizes shop contents. Set to "off" to not shuffle shops; "0" shuffles shops but does not allow multiworld items in shops.
|
||||||
|
0: 0
|
||||||
|
1: 0
|
||||||
|
2: 0
|
||||||
|
3: 0
|
||||||
|
4: 0
|
||||||
|
random_value: 0
|
||||||
|
off: 50
|
||||||
|
tokensanity: # Token rewards from Gold Skulltulas are shuffled into the pool.
|
||||||
|
off: 50
|
||||||
|
dungeons: 0
|
||||||
|
overworld: 0
|
||||||
|
all: 0
|
||||||
|
shuffle_scrubs: # Shuffle the items sold by Business Scrubs, and set the prices.
|
||||||
|
off: 50
|
||||||
|
low: 0
|
||||||
|
regular: 0
|
||||||
|
random_prices: 0
|
||||||
|
shuffle_cows: # Cows give items when Epona's Song is played.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
shuffle_kokiri_sword: # Shuffle Kokiri Sword into the item pool.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
shuffle_ocarinas: # Shuffle the Fairy Ocarina and Ocarina of Time into the item pool.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
shuffle_weird_egg: # Shuffle the Weird Egg from Malon at Hyrule Castle.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
shuffle_gerudo_card: # Shuffle the Gerudo Membership Card into the item pool.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
shuffle_beans: # Adds a pack of 10 beans to the item pool and changes the bean salesman to sell one item for 60 rupees.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
shuffle_medigoron_carpet_salesman: # Shuffle the items sold by Medigoron and the Haunted Wasteland Carpet Salesman.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
skip_child_zelda: # Game starts with Zelda's Letter, the item at Zelda's Lullaby, and the relevant events already completed.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
no_escape_sequence: # Skips the tower collapse sequence between the Ganondorf and Ganon fights.
|
||||||
|
false: 0
|
||||||
|
true: 50
|
||||||
|
no_guard_stealth: # The crawlspace into Hyrule Castle skips straight to Zelda.
|
||||||
|
false: 0
|
||||||
|
true: 50
|
||||||
|
no_epona_race: # Epona can always be summoned with Epona's Song.
|
||||||
|
false: 0
|
||||||
|
true: 50
|
||||||
|
skip_some_minigame_phases: # Dampe Race and Horseback Archery give both rewards if the second condition is met on the first attempt.
|
||||||
|
false: 0
|
||||||
|
true: 50
|
||||||
|
complete_mask_quest: # All masks are immediately available to borrow from the Happy Mask Shop.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
useful_cutscenes: # Reenables the Poe cutscene in Forest Temple, Darunia in Fire Temple, and Twinrova introduction. Mostly useful for glitched.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
fast_chests: # All chest animations are fast. If disabled, major items have a slow animation.
|
||||||
|
false: 0
|
||||||
|
true: 50
|
||||||
|
free_scarecrow: # Pulling out the ocarina near a scarecrow spot spawns Pierre without needing the song.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
fast_bunny_hood: # Bunny Hood lets you move 1.5x faster like in Majora's Mask.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
chicken_count: # Controls the number of Cuccos for Anju to give an item as child.
|
||||||
|
\# you can add additional values between minimum and maximum
|
||||||
|
0: 0 # minimum value
|
||||||
|
7: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
hints: # Gossip Stones can give hints about item locations.
|
||||||
|
none: 0
|
||||||
|
mask: 0
|
||||||
|
agony: 0
|
||||||
|
always: 50
|
||||||
|
hint_dist: # Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.
|
||||||
|
balanced: 50
|
||||||
|
ddr: 0
|
||||||
|
league: 0
|
||||||
|
mw2: 0
|
||||||
|
scrubs: 0
|
||||||
|
strong: 0
|
||||||
|
tournament: 0
|
||||||
|
useless: 0
|
||||||
|
very_strong: 0
|
||||||
|
text_shuffle: # Randomizes text in the game for comedic effect.
|
||||||
|
none: 50
|
||||||
|
except_hints: 0
|
||||||
|
complete: 0
|
||||||
|
damage_multiplier: # Controls the amount of damage Link takes.
|
||||||
|
half: 0
|
||||||
|
normal: 50
|
||||||
|
double: 0
|
||||||
|
quadruple: 0
|
||||||
|
ohko: 0
|
||||||
|
no_collectible_hearts: # Hearts will not drop from enemies or objects.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
starting_tod: # Change the starting time of day.
|
||||||
|
default: 50
|
||||||
|
sunrise: 0
|
||||||
|
morning: 0
|
||||||
|
noon: 0
|
||||||
|
afternoon: 0
|
||||||
|
sunset: 0
|
||||||
|
evening: 0
|
||||||
|
midnight: 0
|
||||||
|
witching_hour: 0
|
||||||
|
start_with_consumables: # Start the game with full Deku Sticks and Deku Nuts.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
start_with_rupees: # Start with a full wallet. Wallet upgrades will also fill your wallet.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
item_pool_value: # Changes the number of items available in the game.
|
||||||
|
plentiful: 0
|
||||||
|
balanced: 50
|
||||||
|
scarce: 0
|
||||||
|
minimal: 0
|
||||||
|
junk_ice_traps: # Adds ice traps to the item pool.
|
||||||
|
off: 0
|
||||||
|
normal: 50
|
||||||
|
on: 0
|
||||||
|
mayhem: 0
|
||||||
|
onslaught: 0
|
||||||
|
ice_trap_appearance: # Changes the appearance of ice traps as freestanding items.
|
||||||
|
major_only: 50
|
||||||
|
junk_only: 0
|
||||||
|
anything: 0
|
||||||
|
logic_earliest_adult_trade: # Earliest item that can appear in the adult trade sequence.
|
||||||
|
pocket_egg: 0
|
||||||
|
pocket_cucco: 0
|
||||||
|
cojiro: 0
|
||||||
|
odd_mushroom: 0
|
||||||
|
poachers_saw: 0
|
||||||
|
broken_sword: 0
|
||||||
|
prescription: 50
|
||||||
|
eyeball_frog: 0
|
||||||
|
eyedrops: 0
|
||||||
|
claim_check: 0
|
||||||
|
logic_latest_adult_trade: # Latest item that can appear in the adult trade sequence.
|
||||||
|
pocket_egg: 0
|
||||||
|
pocket_cucco: 0
|
||||||
|
cojiro: 0
|
||||||
|
odd_mushroom: 0
|
||||||
|
poachers_saw: 0
|
||||||
|
broken_sword: 0
|
||||||
|
prescription: 0
|
||||||
|
eyeball_frog: 0
|
||||||
|
eyedrops: 0
|
||||||
|
claim_check: 50
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Joining a MultiWorld Game
|
||||||
|
|
||||||
|
### Obtain your OOT patch file
|
||||||
|
|
||||||
|
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that
|
||||||
|
is done, the host will provide you with either a link to download your data file, or with a zip file containing
|
||||||
|
everyone's data files. Your data file should have a `.z5ap` extension.
|
||||||
|
|
||||||
|
Double click on your `.z5ap` file to start Z5Client and start the ROM patch process. Once the process is finished (this can take a while), the emulator will be started automatically (If we associated the extension to the emulator as recommended)
|
||||||
|
|
||||||
|
### Connect to multiserver
|
||||||
|
Once both the Z5Client and the emulator are started we must connect them. Within the emulator we click on the "Tools" menu and select "Lua console". In the new window click on the folder icon and look for the ootMulti.lua file. Once the file is loaded it will connect automatically to Z5Client.
|
||||||
|
|
||||||
|
Note: We strongly advise you don't open any emulator menu while it and Z5client are connected, as the script will halt and disconnects can happen. If you get disconnected just double click on the script again.
|
||||||
|
|
||||||
|
To connect the client to the multiserver simply put address:port on the textfield on top and press enter (if the server uses password, type on the bottom textfield /connect <address>:<port> [password], to connect)
|
||||||
|
|
||||||
|
Now you are ready to start your adventure in Hyrule.
|
||||||
372
WebHostLib/static/assets/tutorial/zelda5/setup_es.md
Normal file
372
WebHostLib/static/assets/tutorial/zelda5/setup_es.md
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
# Guia instalación de Ocarina of time Archipelago
|
||||||
|
|
||||||
|
## Nota importante
|
||||||
|
|
||||||
|
Al usar el cliente y bizhawk, esta guia solo es aplicable en Windows.
|
||||||
|
|
||||||
|
## Software Requerido
|
||||||
|
|
||||||
|
- [bizhawk+script+Z5Client](https://github.com/ArchipelagoMW/Z5Client/releases) Recomendamos bajar el setup de Z5client ya que automatizara varios pasos mas adelante
|
||||||
|
|
||||||
|
## Instala emulador y cliente
|
||||||
|
|
||||||
|
Descarga el fichero getBizhawk.ps1 del enlace anterior. Colocalo en la carpeta donde desees instalar el emulador, haz click derecho en él y selecciona "Ejecutar con PowerShell". Esto descargará todas las dependencias necesarias para el emulador. Puede tardar un rato.
|
||||||
|
|
||||||
|
Es recomendable asociar la extensión de las roms de N64 (\*.n64) al bizhawk que hemos instalado anteriormente. Para hacerlo simplemente debemos buscar alguna rom de n64 que tengamos, hacer click derecho, seleccionar "Abrir con...", desplegar la lista y buscar la opción "Buscar otra aplicación", navegar hasta el directorio de bizhawk y seleccionar EmuHawk.exe
|
||||||
|
|
||||||
|
Situa el fichero ootMulti.lua del enlace anterior en la carpeta "lua" del emulador recien instalado.
|
||||||
|
|
||||||
|
Instala el cliente Z5Client.
|
||||||
|
|
||||||
|
## Configura tu fichero YAML
|
||||||
|
|
||||||
|
### Que es un fichero YAML y por qué necesito uno?
|
||||||
|
Tu fichero YAML contiene un numero de opciones que proveen al generador con información sobre como debe generar tu juego.
|
||||||
|
Cada jugador de un multiworld entregara u propio fichero YAML.
|
||||||
|
Esto permite que cada jugador disfrute de una experiencia personalizada a su gusto y diferentes jugadores dentro del mismo multiworld
|
||||||
|
pueden tener diferentes opciones
|
||||||
|
|
||||||
|
### Where do I get a YAML file?
|
||||||
|
Un fichero basico yaml para OOT tendra este aspecto. (Hay muchas opciones cosméticas que se han ignorado para este tutorial, si quieres ver una lista completa, descarga (Archipelago)[https://github.com/ArchipelagoMW/Archipelago/releases] y buscar el fichero de ejemplo en el directorio "Players"))
|
||||||
|
```yaml
|
||||||
|
description: Default Ocarina of Time Template # Describe tu fichero yalm
|
||||||
|
\# Tu nombre en el juego. Los espacio seran reemplazados por _ y hay un limite de 16 caracteres
|
||||||
|
name: YourName{number}
|
||||||
|
game:
|
||||||
|
Ocarina of Time: 1
|
||||||
|
requires:
|
||||||
|
version: 0.1.7 # Version de archipelago minima.
|
||||||
|
\# Opciones compartidas por todos los juegos:
|
||||||
|
accessibility:
|
||||||
|
items: 0 # Garantiza que puedes obtener todos los objetos pero no todas las localizaciones
|
||||||
|
locations: 50 # Garantiza que puedes obtener todas las localizaciones
|
||||||
|
none: 0 # Solo garantiza que el juego pueda completarse.
|
||||||
|
progression_balancing:
|
||||||
|
on: 50 # Un sistema para reducir tiempos de espera en una partida multiworld
|
||||||
|
off: 0
|
||||||
|
Ocarina of Time:
|
||||||
|
logic_rules: # Logica usada por el randomizer.
|
||||||
|
glitchless: 50
|
||||||
|
glitched: 0
|
||||||
|
no_logic: 0
|
||||||
|
logic_no_night_tokens_without_suns_song: # Las skulltulas nocturnas requeriran la cancion del sol por logica
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
open_forest: # Indica el estado del bosque Kokiri y el camino al Arbol Deku.
|
||||||
|
open: 50
|
||||||
|
closed_deku: 0
|
||||||
|
closed: 0
|
||||||
|
open_kakariko: # Indica el estado de la puerta de Kakariko hacia la montaña de la muerte.
|
||||||
|
open: 50
|
||||||
|
zelda: 0
|
||||||
|
closed: 0
|
||||||
|
open_door_of_time: # Abre la puerta del tiempo sin la cancion del tiempo.
|
||||||
|
false: 0
|
||||||
|
true: 50
|
||||||
|
zora_fountain: # Indica el estado del rey zora bloqueando el camino a la fuente Zora.
|
||||||
|
open: 0
|
||||||
|
adult: 0
|
||||||
|
closed: 50
|
||||||
|
gerudo_fortress: # Indica los requerimientos para acceder a la fortaleza Gerudo.
|
||||||
|
normal: 0
|
||||||
|
fast: 50
|
||||||
|
open: 0
|
||||||
|
bridge: # Indica los requerimientos para el puente arco iris.
|
||||||
|
open: 0
|
||||||
|
vanilla: 0
|
||||||
|
stones: 0
|
||||||
|
medallions: 50
|
||||||
|
dungeons: 0
|
||||||
|
tokens: 0
|
||||||
|
trials: # Numero de pruebas dentro del castillo de Ganon.
|
||||||
|
0: 50 # minimum value
|
||||||
|
6: 0 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
starting_age: # Indica la edad con la que empieza link.
|
||||||
|
child: 50
|
||||||
|
adult: 0
|
||||||
|
triforce_hunt: # Reune piezas de trifuerza para completar el juego.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
triforce_goal: # Numero de piezas de trifuerza requeridas. El numero de piezas disponibles es determinado por la opcion "Item pool".
|
||||||
|
1: 0 # minimum value
|
||||||
|
50: 0 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
20: 50
|
||||||
|
bombchus_in_logic: # Los bombchus son considerados para la logica. El primer pack encontrado da 20 chus y las tiendas kokiri y el bazaar los venden. Bombchus abren la bolera.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
bridge_stones: # Numero de piedras para abrir el puente arco iris.
|
||||||
|
0: 0 # minimum value
|
||||||
|
3: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
bridge_medallions: # Numero de medallones para abrir el puente arco iris.
|
||||||
|
0: 0 # minimum value
|
||||||
|
6: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
bridge_rewards: # Numero de mazmorras (cualquier combinacion de medallones y piedras) para abrir el puente arco iris.
|
||||||
|
0: 0 # minimum value
|
||||||
|
9: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
bridge_tokens: # Numero de skultullas de oro requeridas para el puente arco iris.
|
||||||
|
0: 0 # minimum value
|
||||||
|
100: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
shuffle_mapcompass: # Controla donde pueden aparecer los mapas y las brujulas.
|
||||||
|
remove: 0
|
||||||
|
startwith: 50
|
||||||
|
vanilla: 0
|
||||||
|
dungeon: 0
|
||||||
|
overworld: 0
|
||||||
|
any_dungeon: 0
|
||||||
|
keysanity: 0
|
||||||
|
shuffle_smallkeys: # Controla donde pueden aparecer las llaves pequeñas.
|
||||||
|
remove: 0
|
||||||
|
vanilla: 0
|
||||||
|
dungeon: 50
|
||||||
|
overworld: 0
|
||||||
|
any_dungeon: 0
|
||||||
|
keysanity: 0
|
||||||
|
shuffle_fortresskeys: # Controla donde pueden aparecer las llaves de la fortaleza Gerudo.
|
||||||
|
vanilla: 50
|
||||||
|
overworld: 0
|
||||||
|
any_dungeon: 0
|
||||||
|
keysanity: 0
|
||||||
|
shuffle_bosskeys: # Controla donde pueden aparecer las llaves de jefe (excepto la llave del castillo de ganon).
|
||||||
|
remove: 0
|
||||||
|
vanilla: 0
|
||||||
|
dungeon: 50
|
||||||
|
overworld: 0
|
||||||
|
any_dungeon: 0
|
||||||
|
keysanity: 0
|
||||||
|
shuffle_ganon_bosskey: # Controla donde puede aparecer la llave del jefe del castillo de Ganon.
|
||||||
|
remove: 50
|
||||||
|
vanilla: 0
|
||||||
|
dungeon: 0
|
||||||
|
overworld: 0
|
||||||
|
any_dungeon: 0
|
||||||
|
keysanity: 0
|
||||||
|
on_lacs: 0
|
||||||
|
enhance_map_compass: # El mapa indica si una dungeon es clasica o Master Quest. Las brujulas indican la recompensa de mazmorra.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
lacs_condition: # Marca el requerimiento para la escena de las flechas de luz (LACS) en el templo del tiempo.
|
||||||
|
vanilla: 50
|
||||||
|
stones: 0
|
||||||
|
medallions: 0
|
||||||
|
dungeons: 0
|
||||||
|
tokens: 0
|
||||||
|
lacs_stones: # Marca el numero de piedras espirituales requeridas para LACS
|
||||||
|
0: 0 # minimum value
|
||||||
|
3: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
lacs_medallions: # Marca el numero de medallones requeridas para LACS.
|
||||||
|
0: 0 # minimum value
|
||||||
|
6: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
lacs_rewards: # Marca el numero de recompensas de mazmorra requeridas para LACS.
|
||||||
|
0: 0 # minimum value
|
||||||
|
9: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
lacs_tokens: # Marca el numero de Skulltulas de oro requeridas para LACS.
|
||||||
|
0: 0 # minimum value
|
||||||
|
100: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
shuffle_song_items: # Marca donde pueden aparecer las canciones.
|
||||||
|
song: 50
|
||||||
|
dungeon: 0
|
||||||
|
any: 0
|
||||||
|
shopsanity: # Aleatoriza el contenido de las tiendas. "off" para no mezclar las tiendas; "0" mezcla las tiendas pero no permite objetos unicos en ellas.
|
||||||
|
0: 0
|
||||||
|
1: 0
|
||||||
|
2: 0
|
||||||
|
3: 0
|
||||||
|
4: 0
|
||||||
|
random_value: 0
|
||||||
|
off: 50
|
||||||
|
tokensanity: # Indica si las Skulltulas de oro pueden tener objetos que no sean su ficha.
|
||||||
|
off: 50
|
||||||
|
dungeons: 0
|
||||||
|
overworld: 0
|
||||||
|
all: 0
|
||||||
|
shuffle_scrubs: # Aleatoriza los objetos de los Scrubs vendedores y marca su precio.
|
||||||
|
off: 50
|
||||||
|
low: 0
|
||||||
|
regular: 0
|
||||||
|
random_prices: 0
|
||||||
|
shuffle_cows: # Las vacas dan objetos cuando les tocas las cancion de Epona.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
shuffle_kokiri_sword: # Aleatoriza la posicion de la espada Kokiri.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
shuffle_ocarinas: # Aleatoriza la posicion de las ocarinas.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
shuffle_weird_egg: # Aleatoriza la posicion del huevo extraño.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
shuffle_gerudo_card: # Aleatoriza la posicion de la tarjeta de membresia Gerudo.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
shuffle_beans: # Añade un pack de 10 judias magicas al juego y el vendedor vende un solo objeto por 60 rupias.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
shuffle_medigoron_carpet_salesman: # Aleatoriza el objeto que vende Medigoron y el vendedor de la alfombra voladora del paramo maldito.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
skip_child_zelda: # Empieza el juego con la carta de zelda, el objeto que daria impa al enseñar la nana de zelda. Y zelda se considera ya visitada (puedes ir directamente a ver a Saria al bosque y a Malon al rancho)
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
no_escape_sequence: # Elimina la huida de link y zelda despues de ganar a Ganondorf.
|
||||||
|
false: 0
|
||||||
|
true: 50
|
||||||
|
no_guard_stealth: # Elimina la escena de sigilo antes de ver a Zelda.
|
||||||
|
false: 0
|
||||||
|
true: 50
|
||||||
|
no_epona_race: # No necesitas hacer la carrera para invocar a Epona.
|
||||||
|
false: 0
|
||||||
|
true: 50
|
||||||
|
skip_some_minigame_phases: # La carrera de Dampe y el minijuego de arco a caballo dan ambras recompensas a la vez si se cumplen las condiciones.
|
||||||
|
false: 0
|
||||||
|
true: 50
|
||||||
|
complete_mask_quest: # Todas las mascaras estan disponibles.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
useful_cutscenes: # Ciertas escenas se mantienen (como los Poes del templo del bosque, Darunia o Twinrova. Principalmente util para modos con Glitches.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
fast_chests: # Los cofres siempre se cogen rapido. Si se desactiva, los objetos importantes tienen animacion lenta. (IMPORTANTE: TODOS LOS OBJETOS QUE VAYAN A OTROS MUNDOS SE CONSIDERAN IMPORTANTES)
|
||||||
|
false: 0
|
||||||
|
true: 50
|
||||||
|
free_scarecrow: # Sacara la ocraina cerca de un punto con espantapajaros invoca a Pierre sin necesidad de la cancion.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
fast_bunny_hood: # La capucha conejo mejora tu velocidad como en Majora's Mask.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
chicken_count: # Numero de Cuccos que Anju necesita en el corral para que te de el objeto.
|
||||||
|
0: 0 # minimum value
|
||||||
|
7: 50 # maximum value
|
||||||
|
random: 0
|
||||||
|
random-low: 0
|
||||||
|
random-high: 0
|
||||||
|
hints: # Marca el requerimiento para que las piedras chivatas den pistas.
|
||||||
|
none: 0
|
||||||
|
mask: 0
|
||||||
|
agony: 0
|
||||||
|
always: 50
|
||||||
|
hint_dist: # Elije la distribucion de pistas
|
||||||
|
balanced: 50
|
||||||
|
ddr: 0
|
||||||
|
league: 0
|
||||||
|
mw2: 0
|
||||||
|
scrubs: 0
|
||||||
|
strong: 0
|
||||||
|
tournament: 0
|
||||||
|
useless: 0
|
||||||
|
very_strong: 0
|
||||||
|
damage_multiplier: # Controla el daño que recibe Link.
|
||||||
|
half: 0
|
||||||
|
normal: 50
|
||||||
|
double: 0
|
||||||
|
quadruple: 0
|
||||||
|
ohko: 0
|
||||||
|
no_collectible_hearts: # No caen corazones de enemigos u objetos.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
starting_tod: # Cambia el momento del dia al empezar el juego.
|
||||||
|
default: 50
|
||||||
|
sunrise: 0
|
||||||
|
morning: 0
|
||||||
|
noon: 0
|
||||||
|
afternoon: 0
|
||||||
|
sunset: 0
|
||||||
|
evening: 0
|
||||||
|
midnight: 0
|
||||||
|
witching_hour: 0
|
||||||
|
start_with_consumables: # Empieza el juego con el maximo de palos y nueves Deku que pueda llevar Link.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
start_with_rupees: # Empieza el juego con la cartera llena. Las mejoras de cartera vienen llenas.
|
||||||
|
false: 50
|
||||||
|
true: 0
|
||||||
|
item_pool_value: # Cambia el numero de objetos disponibles en el juego.
|
||||||
|
plentiful: 0
|
||||||
|
balanced: 50
|
||||||
|
scarce: 0
|
||||||
|
minimal: 0
|
||||||
|
junk_ice_traps: # Añade trampas de hielo.
|
||||||
|
off: 0
|
||||||
|
normal: 50
|
||||||
|
on: 0
|
||||||
|
mayhem: 0
|
||||||
|
onslaught: 0
|
||||||
|
ice_trap_appearance: # Cambia la apariencia de las trampas de hielo cuando aparecen como objetos fuera de cofres.
|
||||||
|
major_only: 50
|
||||||
|
junk_only: 0
|
||||||
|
anything: 0
|
||||||
|
logic_earliest_adult_trade: # Objeto mas bajo que puede aparecer en la secuencia de cambios de Link Adulto.
|
||||||
|
pocket_egg: 0
|
||||||
|
pocket_cucco: 0
|
||||||
|
cojiro: 0
|
||||||
|
odd_mushroom: 0
|
||||||
|
poachers_saw: 0
|
||||||
|
broken_sword: 0
|
||||||
|
prescription: 50
|
||||||
|
eyeball_frog: 0
|
||||||
|
eyedrops: 0
|
||||||
|
claim_check: 0
|
||||||
|
logic_latest_adult_trade: # Objeto mas tardio que puede aparecer en la secuencia de cambios de Link Adulto.
|
||||||
|
pocket_egg: 0
|
||||||
|
pocket_cucco: 0
|
||||||
|
cojiro: 0
|
||||||
|
odd_mushroom: 0
|
||||||
|
poachers_saw: 0
|
||||||
|
broken_sword: 0
|
||||||
|
prescription: 0
|
||||||
|
eyeball_frog: 0
|
||||||
|
eyedrops: 0
|
||||||
|
claim_check: 50
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unirse a un juego MultiWorld
|
||||||
|
|
||||||
|
### Obten tu parche
|
||||||
|
|
||||||
|
|
||||||
|
Cuando te unes a un juego multiworld, se te pedirá que entregues tu fichero YAML a quien sea que hospede el juego multiworld.
|
||||||
|
Una vez la generación acabe, el anfitrión te dará un enlace a tu fichero de datos o un zip con los ficheros de todos.
|
||||||
|
Tu fichero de datos tiene una extensión `.z5ap`.
|
||||||
|
|
||||||
|
Haz doble click en tu fichero `.z5ap` para que se arranque el Z5Client y realize el parcheado de la ROM. Una vez acabe el parcheado de la rom (esto puede llevar un tiempo) se abrira automaticamente el emulador (Si se ha asociado la extensión al emulador tal como hemos recomendado)
|
||||||
|
|
||||||
|
### Conectar al multiserver
|
||||||
|
Una vez arrancado tanto el Z5Client como el emulador hay que conectarlo entre ellos, para ello simplemente accede al menú "Tools" y selecciona "Lua console". En la nueva ventana, dale al icono de la carpeta y busca el fichero ootMulti.lua. Al cargar dicho fichero se conectara automaticamente con el cliente.
|
||||||
|
|
||||||
|
Nota: Es muy recomendable que no se abra ningún menú del emulador mientras esten emulador y Z5Client conectados, ya que el script de conexión se para en ese caso y pueden provocar desconexiones. Si se pierde la conexion, simplemente haz doble click en el script de nuevo.
|
||||||
|
|
||||||
|
Para conectar el cliente con el servidor simplemente pon la direccion_IP:puerto en la caja de texto de arriba y presiona enter (si el servidor tiene contraseña, en la caja de texto de abajo escribir /connect direccion:puerto contraseña, para conectar)
|
||||||
|
|
||||||
|
Y ya estas listo, para emprender tu aventura por Hyrule.
|
||||||
@@ -25,6 +25,21 @@
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#generate-game-form{
|
#generate-game-form-wrapper table td{
|
||||||
|
text-align: left;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#generate-form-button-row{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-input{
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.interactive{
|
||||||
|
color: #ffef00;
|
||||||
|
}
|
||||||
|
|||||||
12
WebHostLib/static/styles/startPlaying.css
Normal file
12
WebHostLib/static/styles/startPlaying.css
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#start-playing-wrapper{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#start-playing{
|
||||||
|
width: 700px;
|
||||||
|
min-height: 240px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@@ -11,33 +11,67 @@
|
|||||||
{% include 'header/oceanHeader.html' %}
|
{% include 'header/oceanHeader.html' %}
|
||||||
<div id="generate-game-wrapper">
|
<div id="generate-game-wrapper">
|
||||||
<div id="generate-game" class="grass-island">
|
<div id="generate-game" class="grass-island">
|
||||||
<h1>Upload Config{% if race %} (Race Mode){% endif %}</h1>
|
<h1>Generate Game{% if race %} (Race Mode){% endif %}</h1>
|
||||||
<p>
|
<p>
|
||||||
This page allows you to generate a game by uploading a yaml file or a zip file containing yaml files.
|
This page allows you to generate a game by uploading a config file or a zip file containing config
|
||||||
If you do not have a config (yaml) file yet, you may create one on the
|
files. If you do not have a config (.yaml) file yet, you may create one on the game's settings page,
|
||||||
<a href="/player-settings">Player Settings</a> page.
|
which you can find via the <a href="{{ url_for("games") }}">supported games list</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{% if race -%}
|
{% if race -%}
|
||||||
This game will be generated in race mode, meaning the spoiler log will be unavailable,
|
This game will be generated in race mode,
|
||||||
roms will be encrypted, and single-player games will have no multidata files.
|
meaning the spoiler log will be unavailable and game specific protections will be in place,
|
||||||
|
like ROM encryption or cheat mode removal.
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
If you would like to generate a race game,
|
If you would like to generate a race game,
|
||||||
<a href="{{ url_for("generate", race=True) }}">click here.</a> Race games are generated without
|
<a href="{{ url_for("generate", race=True) }}">click here.</a><br />
|
||||||
a spoiler log, the ROMs are encrypted, and single-player games will not include a multidata file.
|
Race games are generated without a spoiler log and game specific protections will be in place,
|
||||||
|
like ROM encryption or cheat mode removal.
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
After generation is complete, you will have the option to download a patch file.
|
|
||||||
This patch file can be opened with the
|
|
||||||
<a href="https://github.com/ArchipelagoMW/Archipelago/releases">client</a>, which can be
|
|
||||||
used to to create a rom file. In-browser patching is planned for the future.
|
|
||||||
</p>
|
|
||||||
<div id="generate-game-form-wrapper">
|
<div id="generate-game-form-wrapper">
|
||||||
<form id="generate-game-form" method="post" enctype="multipart/form-data">
|
<form id="generate-game-form" method="post" enctype="multipart/form-data">
|
||||||
<input id="file-input" type="file" name="file">
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><label for="forfeit_mode">Forfeit Permission:</label></td>
|
||||||
|
<td>
|
||||||
|
<select name="forfeit_mode" id="forfeit_mode">
|
||||||
|
<option value="auto">Automatic on goal completion</option>
|
||||||
|
<option value="goal">Allow !forfeit after goal completion</option>
|
||||||
|
<option value="auto-enabled">Automatic on goal completion and manual !forfeit</option>
|
||||||
|
<option value="enabled">Manual !forfeit</option>
|
||||||
|
<option value="disabled">Disabled</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<label for="hint_cost"> Hint Cost:</label>
|
||||||
|
<span
|
||||||
|
class="interactive"
|
||||||
|
data-tooltip="After gathering this many checks, players can !hint <itemname>
|
||||||
|
to get the location of that hint item.">(?)
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="hint_cost" id="hint_cost">
|
||||||
|
{% for n in range(0, 110, 5) %}
|
||||||
|
<option {% if n == 10 %}selected="selected" {% endif %} value="{{ n }}">
|
||||||
|
{% if n > 100 %}Off{% else %}{{ n }}%{% endif %}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div id="generate-form-button-row">
|
||||||
|
<input id="file-input" type="file" name="file">
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<button id="generate-game-button">Upload</button>
|
<button id="generate-game-button">Upload File</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<div id="base-header-right">
|
<div id="base-header-right">
|
||||||
<a href="/games">supported games</a>
|
<a href="/games">supported games</a>
|
||||||
<a href="/tutorial">setup guides</a>
|
<a href="/tutorial">setup guides</a>
|
||||||
<a href="/uploads">start game</a>
|
<a href="/start-playing">start playing</a>
|
||||||
<a href="/faq/en">f.a.q.</a>
|
<a href="/faq/en">f.a.q.</a>
|
||||||
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
|
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,17 +15,17 @@
|
|||||||
<h1>Host Game</h1>
|
<h1>Host Game</h1>
|
||||||
<p>
|
<p>
|
||||||
This page allows you to host a game which was not generated by the website. For example, if you have
|
This page allows you to host a game which was not generated by the website. For example, if you have
|
||||||
generated a doors game on your own computer, you may upload the zip file created by the generator to
|
generated a game on your own computer, you may upload the zip file created by the generator to
|
||||||
host the game here. This will also provide the tracker, and the ability for your players to download
|
host the game here. This will also provide a tracker, and the ability for your players to download
|
||||||
their patch files.
|
their patch files.
|
||||||
<br /><br />
|
<br /><br />
|
||||||
In addition to a zip file created by the generator, you may upload a multidata file here as well.
|
In addition to the zip file created by the generator, you may upload a multidata file here as well.
|
||||||
</p>
|
</p>
|
||||||
<div id="host-game-form-wrapper">
|
<div id="host-game-form-wrapper">
|
||||||
<form id="host-game-form" method="post" enctype="multipart/form-data">
|
<form id="host-game-form" method="post" enctype="multipart/form-data">
|
||||||
<input id="file-input" type="file" name="file">
|
<input id="file-input" type="file" name="file">
|
||||||
</form>
|
</form>
|
||||||
<button id="host-game-button">Upload</button>
|
<button id="host-game-button">Upload File</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<h4>multiworld multi-game randomizer</h4>
|
<h4>multiworld multi-game randomizer</h4>
|
||||||
</div>
|
</div>
|
||||||
<div id="landing-links">
|
<div id="landing-links">
|
||||||
<a href="/uploads" id="mid-button">start<br />game</a>
|
<a href="/start-playing" id="mid-button">start<br />playing</a>
|
||||||
<a href="/games" id="far-left-button">supported<br />games</a>
|
<a href="/games" id="far-left-button">supported<br />games</a>
|
||||||
<a href="/tutorial" id="mid-left-button">setup guides</a>
|
<a href="/tutorial" id="mid-left-button">setup guides</a>
|
||||||
<a href="https://discord.gg/8Z65BR2" id="far-right-button" target="_blank">discord</a>
|
<a href="https://discord.gg/8Z65BR2" id="far-right-button" target="_blank">discord</a>
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span class="variable">{{ seeds }}</span>
|
<span class="variable">{{ seeds }}</span>
|
||||||
games were created and
|
games were generated and
|
||||||
<span class="variable">{{ rooms }}</span>
|
<span class="variable">{{ rooms }}</span>
|
||||||
were hosted in the last 7 days.
|
were hosted in the last 7 days.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
A list of all games you have generated can be found <a href="/user-content">here</a>.
|
A list of all games you have generated can be found <a href="/user-content">here</a>.
|
||||||
<br />
|
<br />
|
||||||
Advanced users can download a template file for this game
|
Advanced users can download a template file for this game
|
||||||
<a href="/static/generated/{{ game }}.yaml">here</a>.
|
<a href="/static/generated/configs/{{ game }}.yaml">here</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
||||||
|
|||||||
32
WebHostLib/templates/startPlaying.html
Normal file
32
WebHostLib/templates/startPlaying.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{% extends 'pageWrapper.html' %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<title>Start Playing</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/startPlaying.css") }}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% include 'header/oceanHeader.html' %}
|
||||||
|
|
||||||
|
<div id="start-playing-wrapper">
|
||||||
|
<div id="start-playing" class="grass-island {% if rooms %}wider{% endif %}">
|
||||||
|
<h1>Start Playing</h1>
|
||||||
|
<p>
|
||||||
|
If you're ready to start playing but don't know where to begin, check out the
|
||||||
|
<a href="/tutorial">tutorials</a> page. It has all the resources you need to create a config file
|
||||||
|
and get started. If you already have a config file, or a zip file containing them, read on.
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
To start playing a game, you'll first need to <a href="/generate">generate a randomized game</a>.
|
||||||
|
You'll need to upload either a config file or a zip file containing one more more config files.
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
If you have already generated a game and just need to host it, this site can<br />
|
||||||
|
<a href="uploads">host a pre-generated game</a> for you.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include 'islandFooter.html' %}
|
||||||
|
{% endblock %}
|
||||||
@@ -10,8 +10,12 @@
|
|||||||
<div id="games">
|
<div id="games">
|
||||||
<h1>Currently Supported Games</h1>
|
<h1>Currently Supported Games</h1>
|
||||||
{% for game, description in worlds.items() %}
|
{% for game, description in worlds.items() %}
|
||||||
<h3><a href="{{ url_for("player_settings", game=game) }}">{{ game }}</a></h3>
|
<h3><a href="{{ url_for("game_info", game=game, lang="en") }}">{{ game }}</a></h3>
|
||||||
<p>{{ description }}</p>
|
<p>
|
||||||
|
<a href="{{ url_for("player_settings", game=game) }}">Settings Page</a>
|
||||||
|
<br />
|
||||||
|
{{ description }}
|
||||||
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -12,10 +12,6 @@
|
|||||||
<div id="view-seed-wrapper">
|
<div id="view-seed-wrapper">
|
||||||
<div id="view-seed" class="grass-island">
|
<div id="view-seed" class="grass-island">
|
||||||
<h1>Seed Info</h1>
|
<h1>Seed Info</h1>
|
||||||
{% if not seed.multidata and not seed.spoiler %}
|
|
||||||
<p>Single Player Race Rom: No spoiler or multidata exists, parts of the rom are encrypted and rooms
|
|
||||||
cannot be created.</p>
|
|
||||||
{% endif %}
|
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -33,18 +29,6 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if seed.multidata %}
|
{% if seed.multidata %}
|
||||||
<tr>
|
|
||||||
<td>Players: </td>
|
|
||||||
<td>
|
|
||||||
<ul>
|
|
||||||
{% for patch in seed.slots|sort(attribute='player_id') %}
|
|
||||||
<li>
|
|
||||||
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=patch.player_id) }}">{{ patch.player_name }}</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Rooms: </td>
|
<td>Rooms: </td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
Binary file not shown.
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# How do I add a game to Archipelago?
|
# How do I add a game to Archipelago?
|
||||||
This guide is going to try and be a broad summary of how you can do just that.
|
This guide is going to try and be a broad summary of how you can do just that.
|
||||||
There are three key steps to incorporating a game into Archipelago:
|
There are two key steps to incorporating a game into Archipelago:
|
||||||
- Game Modification
|
- Game Modification
|
||||||
- Archipelago Server Integration
|
- Archipelago Server Integration
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,7 @@ Sent to clients when they connect to an Archipelago server.
|
|||||||
| version | NetworkVersion | Object denoting the version of Archipelago which the server is running. See [NetworkVersion](#NetworkVersion) for more details. |
|
| version | NetworkVersion | Object denoting the version of Archipelago which the server is running. See [NetworkVersion](#NetworkVersion) for more details. |
|
||||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
|
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
|
||||||
| password | bool | Denoted whether a password is required to join this room.|
|
| password | bool | Denoted whether a password is required to join this room.|
|
||||||
| forfeit_mode | str | `auto`, `enabled`, `disabled`, `auto-enabled` or `goal`. |
|
| permissions | dict\[str, Permission\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit" and "remaining". |
|
||||||
| remaining_mode | str | `enabled`, `disabled`, `goal` |
|
|
||||||
| hint_cost | int | The amount of points it costs to receive a hint from the server. |
|
| hint_cost | int | The amount of points it costs to receive a hint from the server. |
|
||||||
| location_check_points | int | The amount of hint points you receive per item/location check completed. ||
|
| location_check_points | int | The amount of hint points you receive per item/location check completed. ||
|
||||||
| players | list\[NetworkPlayer\] | Sent only if the client is properly authenticated (see [Archipelago Connection Handshake](#Archipelago-Connection-Handshake)). Information on the players currently connected to the server. See [NetworkPlayer](#NetworkPlayer) for more details. |
|
| players | list\[NetworkPlayer\] | Sent only if the client is properly authenticated (see [Archipelago Connection Handshake](#Archipelago-Connection-Handshake)). Information on the players currently connected to the server. See [NetworkPlayer](#NetworkPlayer) for more details. |
|
||||||
@@ -219,7 +218,7 @@ Sent to the server to update on the sender's status. Examples include readiness
|
|||||||
#### Arguments
|
#### Arguments
|
||||||
| Name | Type | Notes |
|
| Name | Type | Notes |
|
||||||
| ---- | ---- | ----- |
|
| ---- | ---- | ----- |
|
||||||
| status | int | One of [Client States](#Client-States). Follow the link for more information. |
|
| status | ClientStatus\[int\] | One of [Client States](#Client-States). Send as int. Follow the link for more information. |
|
||||||
|
|
||||||
### Say
|
### Say
|
||||||
Basic chat command which sends text to the server to be distributed to other clients.
|
Basic chat command which sends text to the server to be distributed to other clients.
|
||||||
@@ -341,7 +340,7 @@ An enumeration containing the possible client states that may be used to inform
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
import enum
|
import enum
|
||||||
class CLientStatus(enum.IntEnum):
|
class ClientStatus(enum.IntEnum):
|
||||||
CLIENT_UNKNOWN = 0
|
CLIENT_UNKNOWN = 0
|
||||||
CLIENT_READY = 10
|
CLIENT_READY = 10
|
||||||
CLIENT_PLAYING = 20
|
CLIENT_PLAYING = 20
|
||||||
@@ -358,6 +357,18 @@ class Version(NamedTuple):
|
|||||||
build: int
|
build: int
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Permission
|
||||||
|
An enumeration containing the possible command permission, for commands that may be restricted.
|
||||||
|
```python
|
||||||
|
import enum
|
||||||
|
class Permission(enum.IntEnum):
|
||||||
|
disabled = 0b000 # 0, completely disables access
|
||||||
|
enabled = 0b001 # 1, allows manual use
|
||||||
|
goal = 0b010 # 2, allows manual use after goal completion
|
||||||
|
auto = 0b110 # 6, forces use after goal completion, only works for forfeit
|
||||||
|
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
|
||||||
|
```
|
||||||
|
|
||||||
### Data Package Contents
|
### Data Package Contents
|
||||||
A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their own mappings.
|
A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their own mappings.
|
||||||
|
|
||||||
|
|||||||
1006
docs/network.graphml
1006
docs/network.graphml
File diff suppressed because it is too large
Load Diff
BIN
docs/network.png
BIN
docs/network.png
Binary file not shown.
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 110 KiB |
@@ -27,6 +27,7 @@ server_options:
|
|||||||
# "enabled" -> clients can always forfeit
|
# "enabled" -> clients can always forfeit
|
||||||
# "auto" -> automatic forfeit on goal completion, "goal" -> clients can forfeit after achieving their goal
|
# "auto" -> automatic forfeit on goal completion, "goal" -> clients can forfeit after achieving their goal
|
||||||
# "auto-enabled" -> automatic forfeit on goal completion and manual forfeit is also enabled
|
# "auto-enabled" -> automatic forfeit on goal completion and manual forfeit is also enabled
|
||||||
|
# "goal" -> forfeit is allowed after goal completion
|
||||||
forfeit_mode: "goal"
|
forfeit_mode: "goal"
|
||||||
# Remaining modes
|
# Remaining modes
|
||||||
# !remaining handling, that tells a client which items remain in their pool
|
# !remaining handling, that tells a client which items remain in their pool
|
||||||
|
|||||||
@@ -50,13 +50,14 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom
|
|||||||
[Components]
|
[Components]
|
||||||
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
|
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
|
||||||
Name: "generator"; Description: "Generator"; Types: full hosting
|
Name: "generator"; Description: "Generator"; Types: full hosting
|
||||||
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup"; Types: full hosting
|
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
|
||||||
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting
|
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296
|
||||||
Name: "server"; Description: "Server"; Types: full hosting
|
Name: "server"; Description: "Server"; Types: full hosting
|
||||||
Name: "client"; Description: "Clients"; Types: full playing
|
Name: "client"; Description: "Clients"; Types: full playing
|
||||||
Name: "client/lttp"; Description: "A Link to the Past"; Types: full playing hosting
|
Name: "client/lttp"; Description: "A Link to the Past"; Types: full playing
|
||||||
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
||||||
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
||||||
|
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
||||||
|
|
||||||
[Dirs]
|
[Dirs]
|
||||||
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
|
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
|
||||||
@@ -66,11 +67,12 @@ Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Ka
|
|||||||
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: generator/oot
|
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: generator/oot
|
||||||
Source: "{#sourcepath}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "{#sourcepath}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/lttp
|
Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/lttp
|
||||||
Source: "{#sourcepath}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator
|
Source: "{#sourcepath}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
|
||||||
|
|
||||||
Source: "{#sourcepath}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
|
Source: "{#sourcepath}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
|
||||||
Source: "{#sourcepath}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
|
Source: "{#sourcepath}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
|
||||||
Source: "{#sourcepath}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
Source: "{#sourcepath}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
||||||
|
Source: "{#sourcepath}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
|
||||||
Source: "{#sourcepath}\ArchipelagoLttPClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
|
Source: "{#sourcepath}\ArchipelagoLttPClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
|
||||||
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
|
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
|
||||||
Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
|
Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
|
||||||
@@ -82,6 +84,7 @@ Source: "{tmp}\forge-installer.jar"; DestDir: "{app}"; Flags: skipifsourcedoesnt
|
|||||||
[Icons]
|
[Icons]
|
||||||
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
|
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
|
||||||
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server
|
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server
|
||||||
|
Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/lttp
|
||||||
Name: "{group}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Components: client/lttp
|
Name: "{group}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Components: client/lttp
|
||||||
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
|
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
|
||||||
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
|
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
|
||||||
|
|||||||
10
kvui.py
10
kvui.py
@@ -16,6 +16,7 @@ from kivy.lang import Builder
|
|||||||
import Utils
|
import Utils
|
||||||
from NetUtils import JSONtoTextParser, JSONMessagePart
|
from NetUtils import JSONtoTextParser, JSONMessagePart
|
||||||
|
|
||||||
|
|
||||||
class GameManager(App):
|
class GameManager(App):
|
||||||
logging_pairs = [
|
logging_pairs = [
|
||||||
("Client", "Archipelago"),
|
("Client", "Archipelago"),
|
||||||
@@ -83,6 +84,7 @@ class FactorioManager(GameManager):
|
|||||||
]
|
]
|
||||||
title = "Archipelago Factorio Client"
|
title = "Archipelago Factorio Client"
|
||||||
|
|
||||||
|
|
||||||
class LttPManager(GameManager):
|
class LttPManager(GameManager):
|
||||||
logging_pairs = [
|
logging_pairs = [
|
||||||
("Client", "Archipelago"),
|
("Client", "Archipelago"),
|
||||||
@@ -90,6 +92,14 @@ class LttPManager(GameManager):
|
|||||||
]
|
]
|
||||||
title = "Archipelago LttP Client"
|
title = "Archipelago LttP Client"
|
||||||
|
|
||||||
|
|
||||||
|
class TextManager(GameManager):
|
||||||
|
logging_pairs = [
|
||||||
|
("Client", "Archipelago")
|
||||||
|
]
|
||||||
|
title = "Archipelago Text Client"
|
||||||
|
|
||||||
|
|
||||||
class LogtoUI(logging.Handler):
|
class LogtoUI(logging.Handler):
|
||||||
def __init__(self, on_log):
|
def __init__(self, on_log):
|
||||||
super(LogtoUI, self).__init__(logging.DEBUG)
|
super(LogtoUI, self).__init__(logging.DEBUG)
|
||||||
|
|||||||
1
setup.py
1
setup.py
@@ -74,6 +74,7 @@ scripts = {
|
|||||||
# Core
|
# Core
|
||||||
"MultiServer.py": ("ArchipelagoServer", False, icon),
|
"MultiServer.py": ("ArchipelagoServer", False, icon),
|
||||||
"Generate.py": ("ArchipelagoGenerate", False, icon),
|
"Generate.py": ("ArchipelagoGenerate", False, icon),
|
||||||
|
"CommonClient.py": ("ArchipelagoTextClient", True, icon),
|
||||||
# LttP
|
# LttP
|
||||||
"LttPClient.py": ("ArchipelagoLttPClient", True, icon),
|
"LttPClient.py": ("ArchipelagoLttPClient", True, icon),
|
||||||
"LttPAdjuster.py": ("ArchipelagoLttPAdjuster", True, icon),
|
"LttPAdjuster.py": ("ArchipelagoLttPAdjuster", True, icon),
|
||||||
|
|||||||
30
test/Reachability.py
Normal file
30
test/Reachability.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import unittest
|
||||||
|
from argparse import Namespace
|
||||||
|
from BaseClasses import MultiWorld
|
||||||
|
from worlds.AutoWorld import AutoWorldRegister, call_all
|
||||||
|
|
||||||
|
|
||||||
|
class TestBase(unittest.TestCase):
|
||||||
|
_state_cache = {}
|
||||||
|
gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]
|
||||||
|
|
||||||
|
def testAllStateCanReachEverything(self):
|
||||||
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
|
if game_name != "Ori and the Blind Forest": # TODO: fix Ori Logic
|
||||||
|
with self.subTest("Game", game=game_name):
|
||||||
|
|
||||||
|
world = MultiWorld(1)
|
||||||
|
world.game[1] = game_name
|
||||||
|
world.player_name = {1: "Tester"}
|
||||||
|
world.set_seed()
|
||||||
|
args = Namespace()
|
||||||
|
for name, option in world_type.options.items():
|
||||||
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
|
world.set_options(args)
|
||||||
|
world.set_default_common_options()
|
||||||
|
for step in self.gen_steps:
|
||||||
|
call_all(world, step)
|
||||||
|
state = world.get_all_state(False)
|
||||||
|
for location in world.get_locations():
|
||||||
|
with self.subTest("Location should be reached", location=location):
|
||||||
|
self.assertTrue(location.can_reach(state))
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
from test.vanilla.TestVanilla import TestVanilla
|
|
||||||
|
|
||||||
|
|
||||||
class TestDeathMountain(TestVanilla):
|
|
||||||
|
|
||||||
def testWestDeathMountain(self):
|
|
||||||
self.run_location_tests([
|
|
||||||
["Ether Tablet", False, []],
|
|
||||||
["Ether Tablet", False, [], ['Progressive Glove', 'Flute']],
|
|
||||||
["Ether Tablet", False, [], ['Lamp', 'Flute']],
|
|
||||||
["Ether Tablet", False, [], ['Magic Mirror', 'Hookshot']],
|
|
||||||
["Ether Tablet", False, [], ['Magic Mirror', 'Hammer']],
|
|
||||||
["Ether Tablet", False, ['Progressive Sword'], ['Progressive Sword']],
|
|
||||||
["Ether Tablet", False, [], ['Book of Mudora']],
|
|
||||||
["Ether Tablet", True, ['Flute', 'Magic Mirror', 'Book of Mudora', 'Progressive Sword', 'Progressive Sword']],
|
|
||||||
["Ether Tablet", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Book of Mudora', 'Progressive Sword', 'Progressive Sword']],
|
|
||||||
["Ether Tablet", True, ['Flute', 'Hammer', 'Hookshot', 'Book of Mudora', 'Progressive Sword', 'Progressive Sword']],
|
|
||||||
["Ether Tablet", True, ['Progressive Glove', 'Lamp', 'Hammer', 'Hookshot', 'Book of Mudora', 'Progressive Sword', 'Progressive Sword']],
|
|
||||||
|
|
||||||
["Old Man", False, []],
|
|
||||||
["Old Man", False, [], ['Progressive Glove', 'Flute']],
|
|
||||||
["Old Man", False, [], ['Lamp']],
|
|
||||||
["Old Man", True, ['Flute', 'Lamp']],
|
|
||||||
["Old Man", True, ['Progressive Glove', 'Lamp']],
|
|
||||||
|
|
||||||
["Spectacle Rock Cave", False, []],
|
|
||||||
["Spectacle Rock Cave", False, [], ['Progressive Glove', 'Flute']],
|
|
||||||
["Spectacle Rock Cave", False, [], ['Lamp', 'Flute']],
|
|
||||||
["Spectacle Rock Cave", True, ['Flute']],
|
|
||||||
["Spectacle Rock Cave", True, ['Progressive Glove', 'Lamp']],
|
|
||||||
|
|
||||||
["Spectacle Rock", False, []],
|
|
||||||
["Spectacle Rock", False, [], ['Progressive Glove', 'Flute']],
|
|
||||||
["Spectacle Rock", False, [], ['Lamp', 'Flute']],
|
|
||||||
["Spectacle Rock", False, [], ['Magic Mirror']],
|
|
||||||
["Spectacle Rock", True, ['Flute', 'Magic Mirror']],
|
|
||||||
["Spectacle Rock", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']],
|
|
||||||
])
|
|
||||||
23
test/TestUniqueness.py
Normal file
23
test/TestUniqueness.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import unittest
|
||||||
|
from BaseClasses import MultiWorld
|
||||||
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
|
|
||||||
|
class TestBase(unittest.TestCase):
|
||||||
|
world: MultiWorld
|
||||||
|
_state_cache = {}
|
||||||
|
|
||||||
|
def testUniqueItems(self):
|
||||||
|
known_item_ids = set()
|
||||||
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
|
current = len(known_item_ids)
|
||||||
|
known_item_ids |= set(world_type.item_id_to_name)
|
||||||
|
self.assertEqual(len(known_item_ids) - len(world_type.item_id_to_name), current)
|
||||||
|
|
||||||
|
def testUniqueLocations(self):
|
||||||
|
known_location_ids = set()
|
||||||
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
|
current = len(known_location_ids)
|
||||||
|
known_location_ids |= set(world_type.location_id_to_name)
|
||||||
|
self.assertEqual(len(known_location_ids) - len(world_type.location_id_to_name), current)
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ class TestInvertedOWG(TestBase):
|
|||||||
self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
|
self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
|
||||||
self.world.get_location('Agahnim 1', 1).item = None
|
self.world.get_location('Agahnim 1', 1).item = None
|
||||||
self.world.get_location('Agahnim 2', 1).item = None
|
self.world.get_location('Agahnim 2', 1).item = None
|
||||||
self.world.precollected_items.clear()
|
self.world.precollected_items[1].clear()
|
||||||
self.world.itempool.append(ItemFactory('Pegasus Boots', 1))
|
self.world.itempool.append(ItemFactory('Pegasus Boots', 1))
|
||||||
mark_light_world_regions(self.world, 1)
|
mark_light_world_regions(self.world, 1)
|
||||||
self.world.worlds[1].set_rules()
|
self.world.worlds[1].set_rules()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class TestVanillaOWG(TestBase):
|
|||||||
self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
|
self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
|
||||||
self.world.get_location('Agahnim 1', 1).item = None
|
self.world.get_location('Agahnim 1', 1).item = None
|
||||||
self.world.get_location('Agahnim 2', 1).item = None
|
self.world.get_location('Agahnim 2', 1).item = None
|
||||||
self.world.precollected_items.clear()
|
self.world.precollected_items[1].clear()
|
||||||
self.world.itempool.append(ItemFactory('Pegasus Boots', 1))
|
self.world.itempool.append(ItemFactory('Pegasus Boots', 1))
|
||||||
mark_dark_world_regions(self.world, 1)
|
mark_dark_world_regions(self.world, 1)
|
||||||
self.world.worlds[1].set_rules()
|
self.world.worlds[1].set_rules()
|
||||||
@@ -48,6 +48,7 @@ def call_all(world: MultiWorld, method_name: str, *args):
|
|||||||
for player in world.player_ids:
|
for player in world.player_ids:
|
||||||
world_types.add(world.worlds[player].__class__)
|
world_types.add(world.worlds[player].__class__)
|
||||||
call_single(world, method_name, player, *args)
|
call_single(world, method_name, player, *args)
|
||||||
|
|
||||||
for world_type in world_types:
|
for world_type in world_types:
|
||||||
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
||||||
if stage_callable:
|
if stage_callable:
|
||||||
@@ -81,7 +82,7 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
# increment this every time something in your world's names/id mappings changes.
|
# increment this every time something in your world's names/id mappings changes.
|
||||||
# While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
|
# While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
|
||||||
# retrieved by clients on every connection.
|
# retrieved by clients on every connection.
|
||||||
data_version = 1
|
data_version: int = 1
|
||||||
|
|
||||||
hint_blacklist: Set[str] = frozenset() # any names that should not be hintable
|
hint_blacklist: Set[str] = frozenset() # any names that should not be hintable
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
forced_auto_forfeit: bool = False
|
forced_auto_forfeit: bool = False
|
||||||
|
|
||||||
# Hide World Type from various views. Does not remove functionality.
|
# Hide World Type from various views. Does not remove functionality.
|
||||||
hidden = False
|
hidden: bool = False
|
||||||
|
|
||||||
# autoset on creation:
|
# autoset on creation:
|
||||||
world: MultiWorld
|
world: MultiWorld
|
||||||
@@ -113,6 +114,9 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
item_names: Set[str] # set of all potential item names
|
item_names: Set[str] # set of all potential item names
|
||||||
location_names: Set[str] # set of all potential location names
|
location_names: Set[str] # set of all potential location names
|
||||||
|
|
||||||
|
# If there is visibility in what is being sent, this is where it will be known.
|
||||||
|
sending_visible: bool = False
|
||||||
|
|
||||||
def __init__(self, world: MultiWorld, player: int):
|
def __init__(self, world: MultiWorld, player: int):
|
||||||
self.world = world
|
self.world = world
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|||||||
@@ -38,3 +38,5 @@ network_data_package = {
|
|||||||
# Set entire datapackage to version 0 if any of them are set to 0
|
# Set entire datapackage to version 0 if any of them are set to 0
|
||||||
if any(not world.data_version for world in AutoWorldRegister.world_types.values()):
|
if any(not world.data_version for world in AutoWorldRegister.world_types.values()):
|
||||||
network_data_package["version"] = 0
|
network_data_package["version"] = 0
|
||||||
|
import logging
|
||||||
|
logging.warning("Datapackage is in custom mode.")
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import copy
|
|||||||
import textwrap
|
import textwrap
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
|
"""Legacy module, undergoing dismantling."""
|
||||||
|
|
||||||
|
|
||||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||||
|
|
||||||
def _get_help_string(self, action):
|
def _get_help_string(self, action):
|
||||||
return textwrap.dedent(action.help)
|
return textwrap.dedent(action.help)
|
||||||
|
|
||||||
|
|
||||||
def parse_arguments(argv, no_defaults=False):
|
def parse_arguments(argv, no_defaults=False):
|
||||||
def defval(value):
|
def defval(value):
|
||||||
return value if not no_defaults else None
|
return value if not no_defaults else None
|
||||||
@@ -241,7 +244,6 @@ def parse_arguments(argv, no_defaults=False):
|
|||||||
parser.add_argument('--game', default="A Link to the Past")
|
parser.add_argument('--game', default="A Link to the Past")
|
||||||
parser.add_argument('--race', default=defval(False), action='store_true')
|
parser.add_argument('--race', default=defval(False), action='store_true')
|
||||||
parser.add_argument('--outputname')
|
parser.add_argument('--outputname')
|
||||||
parser.add_argument('--start_hints')
|
|
||||||
if multiargs.multi:
|
if multiargs.multi:
|
||||||
for player in range(1, multiargs.multi + 1):
|
for player in range(1, multiargs.multi + 1):
|
||||||
parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS)
|
parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS)
|
||||||
@@ -255,7 +257,6 @@ def parse_arguments(argv, no_defaults=False):
|
|||||||
ret.plando_items = []
|
ret.plando_items = []
|
||||||
ret.plando_texts = {}
|
ret.plando_texts = {}
|
||||||
ret.plando_connections = []
|
ret.plando_connections = []
|
||||||
ret.er_seeds = {}
|
|
||||||
|
|
||||||
if ret.timer == "none":
|
if ret.timer == "none":
|
||||||
ret.timer = False
|
ret.timer = False
|
||||||
@@ -277,8 +278,8 @@ def parse_arguments(argv, no_defaults=False):
|
|||||||
'sprite',
|
'sprite',
|
||||||
"triforce_pieces_available",
|
"triforce_pieces_available",
|
||||||
"triforce_pieces_required", "shop_shuffle",
|
"triforce_pieces_required", "shop_shuffle",
|
||||||
"required_medallions", "start_hints",
|
"required_medallions",
|
||||||
"plando_items", "plando_texts", "plando_connections", "er_seeds",
|
"plando_items", "plando_texts", "plando_connections",
|
||||||
'dungeon_counters',
|
'dungeon_counters',
|
||||||
'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
|
'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
|
||||||
'game']:
|
'game']:
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ class ItemData(typing.NamedTuple):
|
|||||||
witch_credit: typing.Optional[str]
|
witch_credit: typing.Optional[str]
|
||||||
flute_boy_credit: typing.Optional[str]
|
flute_boy_credit: typing.Optional[str]
|
||||||
hint_text: typing.Optional[str]
|
hint_text: typing.Optional[str]
|
||||||
|
trap: bool = False
|
||||||
|
|
||||||
|
|
||||||
# Format: Name: (Advancement, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text)
|
# Format: Name: (Advancement, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text)
|
||||||
item_table = {'Bow': ItemData(True, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'),
|
item_table = {'Bow': ItemData(True, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'),
|
||||||
@@ -128,8 +130,8 @@ item_table = {'Bow': ItemData(True, None, 0x0B, 'You have\nchosen the\narcher cl
|
|||||||
'Rupees (50)': ItemData(False, None, 0x41, 'A rupee pile!\nOkay?', 'and the rupee pile', 'the well-off kid', 'life lesson for sale', 'buying okay drugs', 'destitute boy has dinner again', 'fifty rupees'),
|
'Rupees (50)': ItemData(False, None, 0x41, 'A rupee pile!\nOkay?', 'and the rupee pile', 'the well-off kid', 'life lesson for sale', 'buying okay drugs', 'destitute boy has dinner again', 'fifty rupees'),
|
||||||
'Rupees (100)': ItemData(False, None, 0x40, 'A rupee stash!\nHell yeah!', 'and the rupee stash', 'the kind-of-rich kid', 'life lesson for sale', 'buying good drugs', 'affluent boy goes drinking again', 'one hundred rupees'),
|
'Rupees (100)': ItemData(False, None, 0x40, 'A rupee stash!\nHell yeah!', 'and the rupee stash', 'the kind-of-rich kid', 'life lesson for sale', 'buying good drugs', 'affluent boy goes drinking again', 'one hundred rupees'),
|
||||||
'Rupees (300)': ItemData(False, None, 0x46, 'A rupee hoard!\nHell yeah!', 'and the rupee hoard', 'the really-rich kid', 'life lesson for sale', 'buying the best drugs', 'fat-cat boy is rich again', 'three hundred rupees'),
|
'Rupees (300)': ItemData(False, None, 0x46, 'A rupee hoard!\nHell yeah!', 'and the rupee hoard', 'the really-rich kid', 'life lesson for sale', 'buying the best drugs', 'fat-cat boy is rich again', 'three hundred rupees'),
|
||||||
'Rupoor': ItemData(False, None, 0x59, 'a debt collector', 'and the toll-booth', 'the toll-booth kid', 'double loss for sale', 'witch stole your rupees', 'affluent boy steals rupees', 'a rupoor'),
|
'Rupoor': ItemData(False, None, 0x59, 'a debt collector', 'and the toll-booth', 'the toll-booth kid', 'double loss for sale', 'witch stole your rupees', 'affluent boy steals rupees', 'a rupoor', True),
|
||||||
'Red Clock': ItemData(False, None, 0x5B, 'a waste of time', 'the ruby clock', 'the ruby-time kid', 'red time for sale', 'for ruby time', 'moment boy travels time again', 'a red clock'),
|
'Red Clock': ItemData(False, None, 0x5B, 'a waste of time', 'the ruby clock', 'the ruby-time kid', 'red time for sale', 'for ruby time', 'moment boy travels time again', 'a red clock', True),
|
||||||
'Blue Clock': ItemData(False, None, 0x5C, 'a bit of time', 'the sapphire clock', 'sapphire-time kid', 'blue time for sale', 'for sapphire time', 'moment boy time travels again', 'a blue clock'),
|
'Blue Clock': ItemData(False, None, 0x5C, 'a bit of time', 'the sapphire clock', 'sapphire-time kid', 'blue time for sale', 'for sapphire time', 'moment boy time travels again', 'a blue clock'),
|
||||||
'Green Clock': ItemData(False, None, 0x5D, 'a lot of time', 'the emerald clock', 'the emerald-time kid', 'green time for sale', 'for emerald time', 'moment boy adjusts time again', 'a red clock'),
|
'Green Clock': ItemData(False, None, 0x5D, 'a lot of time', 'the emerald clock', 'the emerald-time kid', 'green time for sale', 'for emerald time', 'moment boy adjusts time again', 'a red clock'),
|
||||||
'Single RNG': ItemData(False, None, 0x62, 'something you don\'t yet have', None, None, None, None, 'unknown boy somethings again', 'a new mystery'),
|
'Single RNG': ItemData(False, None, 0x62, 'something you don\'t yet have', None, None, None, None, 'unknown boy somethings again', 'a new mystery'),
|
||||||
@@ -192,7 +194,7 @@ item_table = {'Bow': ItemData(True, None, 0x0B, 'You have\nchosen the\narcher cl
|
|||||||
'Map (Ganons Tower)': ItemData(False, 'Map', 0x72, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Ganon\'s Tower'),
|
'Map (Ganons Tower)': ItemData(False, 'Map', 0x72, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Ganon\'s Tower'),
|
||||||
'Small Key (Universal)': ItemData(False, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key'),
|
'Small Key (Universal)': ItemData(False, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key'),
|
||||||
'Nothing': ItemData(False, None, 0x5A, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again', 'nothing'),
|
'Nothing': ItemData(False, None, 0x5A, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again', 'nothing'),
|
||||||
'Bee Trap': ItemData(False, None, 0xB0, 'We will sting your face a whole lot!', 'and the sting buddies', 'the beekeeper kid', 'insects for sale', 'shroom pollenation', 'bottle boy has mad bees again', 'Friendship'),
|
'Bee Trap': ItemData(False, None, 0xB0, 'We will sting your face a whole lot!', 'and the sting buddies', 'the beekeeper kid', 'insects for sale', 'shroom pollenation', 'bottle boy has mad bees again', 'Friendship', True),
|
||||||
'Faerie': ItemData(False, None, 0xB1, 'Save me and I will revive you', 'and the captive', 'the tingle kid','hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a faerie'),
|
'Faerie': ItemData(False, None, 0xB1, 'Save me and I will revive you', 'and the captive', 'the tingle kid','hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a faerie'),
|
||||||
'Good Bee': ItemData(False, None, 0xB2, 'Save me and I will sting you (sometimes)', 'and the captive', 'the tingle kid','hostage for sale', 'good dust and shrooms', 'bottle boy has friend again', 'a bee'),
|
'Good Bee': ItemData(False, None, 0xB2, 'Save me and I will sting you (sometimes)', 'and the captive', 'the tingle kid','hostage for sale', 'good dust and shrooms', 'bottle boy has friend again', 'a bee'),
|
||||||
'Magic Jar': ItemData(False, None, 0xB3, '', '', '','', '', '', ''),
|
'Magic Jar': ItemData(False, None, 0xB3, '', '', '','', '', '', ''),
|
||||||
@@ -202,7 +204,7 @@ item_table = {'Bow': ItemData(True, None, 0x0B, 'You have\nchosen the\narcher cl
|
|||||||
'Red Potion': ItemData(False, None, 0x2E, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a red potion'),
|
'Red Potion': ItemData(False, None, 0x2E, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a red potion'),
|
||||||
'Green Potion': ItemData(False, None, 0x2F, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a green potion'),
|
'Green Potion': ItemData(False, None, 0x2F, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a green potion'),
|
||||||
'Blue Potion': ItemData(False, None, 0x30, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a blue potion'),
|
'Blue Potion': ItemData(False, None, 0x30, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a blue potion'),
|
||||||
'Bee': ItemData(False, None, 0x0E, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bee'),
|
'Bee': ItemData(False, None, 0x0E, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bee', True),
|
||||||
'Small Heart': ItemData(False, None, 0x42, 'Just a little\npiece of love!', 'and the heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart'),
|
'Small Heart': ItemData(False, None, 0x42, 'Just a little\npiece of love!', 'and the heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart'),
|
||||||
'Activated Flute': ItemData(True, None, 0x4A, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'flute boy plays again', 'the Flute'),
|
'Activated Flute': ItemData(True, None, 0x4A, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'flute boy plays again', 'the Flute'),
|
||||||
'Beat Agahnim 1': ItemData(True, 'Event', None, None, None, None, None, None, None, None),
|
'Beat Agahnim 1': ItemData(True, 'Event', None, None, None, None, None, None, None, None),
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ class Progressive(Choice):
|
|||||||
alias_false = 0
|
alias_false = 0
|
||||||
alias_true = 2
|
alias_true = 2
|
||||||
default = 2
|
default = 2
|
||||||
alias_random = 1
|
|
||||||
|
|
||||||
def want_progressives(self, random):
|
def want_progressives(self, random):
|
||||||
return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)
|
return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)
|
||||||
@@ -189,7 +188,6 @@ class Palette(Choice):
|
|||||||
option_negative = 6
|
option_negative = 6
|
||||||
option_dizzy = 7
|
option_dizzy = 7
|
||||||
option_sick = 8
|
option_sick = 8
|
||||||
alias_random = 1
|
|
||||||
|
|
||||||
|
|
||||||
class OWPalette(Palette):
|
class OWPalette(Palette):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Utils
|
|||||||
from Patch import read_rom
|
from Patch import read_rom
|
||||||
|
|
||||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||||
RANDOMIZERBASEHASH = '13a75c5dd28055fbcf8f69bd8161871d'
|
RANDOMIZERBASEHASH = 'e397fef0e947d1bd760c68c4fe99a600'
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
@@ -22,7 +22,7 @@ from typing import Optional
|
|||||||
|
|
||||||
from BaseClasses import CollectionState, Region
|
from BaseClasses import CollectionState, Region
|
||||||
from worlds.alttp.SubClasses import ALttPLocation
|
from worlds.alttp.SubClasses import ALttPLocation
|
||||||
from worlds.alttp.Shops import ShopType
|
from worlds.alttp.Shops import ShopType, ShopPriceType
|
||||||
from worlds.alttp.Dungeons import dungeon_music_addresses
|
from worlds.alttp.Dungeons import dungeon_music_addresses
|
||||||
from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address
|
from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address
|
||||||
from worlds.alttp.Text import MultiByteTextMapper, text_addresses, Credits, TextTable
|
from worlds.alttp.Text import MultiByteTextMapper, text_addresses, Credits, TextTable
|
||||||
@@ -766,7 +766,10 @@ def patch_rom(world, rom, player, enemized):
|
|||||||
|
|
||||||
if location.item is not None:
|
if location.item is not None:
|
||||||
if not location.native_item:
|
if not location.native_item:
|
||||||
itemid = get_nonnative_item_sprite(location.item.game)
|
if location.item.trap:
|
||||||
|
itemid = 0x5A # Nothing, which disguises
|
||||||
|
else:
|
||||||
|
itemid = get_nonnative_item_sprite(location.item.name)
|
||||||
# Keys in their native dungeon should use the orignal item code for keys
|
# Keys in their native dungeon should use the orignal item code for keys
|
||||||
elif location.parent_region.dungeon:
|
elif location.parent_region.dungeon:
|
||||||
if location.parent_region.dungeon.is_dungeon_item(location.item):
|
if location.parent_region.dungeon.is_dungeon_item(location.item):
|
||||||
@@ -829,7 +832,9 @@ def patch_rom(world, rom, player, enemized):
|
|||||||
'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit',
|
'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit',
|
||||||
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit',
|
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit',
|
||||||
'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)',
|
'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)',
|
||||||
'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)'}:
|
'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)'} and \
|
||||||
|
(world.logic[player] not in ['hybridglitches', 'nologic'] or
|
||||||
|
exit.name not in {'Palace of Darkness Exit', 'Tower of Hera Exit', 'Swamp Palace Exit'}):
|
||||||
# For exits that connot be reached from another, no need to apply offset fixes.
|
# For exits that connot be reached from another, no need to apply offset fixes.
|
||||||
rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else
|
rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else
|
||||||
elif room_id == 0x0059 and world.fix_skullwoods_exit[player]:
|
elif room_id == 0x0059 and world.fix_skullwoods_exit[player]:
|
||||||
@@ -1310,9 +1315,7 @@ def patch_rom(world, rom, player, enemized):
|
|||||||
equip[0x37B] = 1
|
equip[0x37B] = 1
|
||||||
equip[0x36E] = 0x80
|
equip[0x36E] = 0x80
|
||||||
|
|
||||||
for item in world.precollected_items:
|
for item in world.precollected_items[player]:
|
||||||
if item.player != player:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
|
if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
|
||||||
'Titans Mitts', 'Power Glove', 'Progressive Glove',
|
'Titans Mitts', 'Power Glove', 'Progressive Glove',
|
||||||
@@ -1673,6 +1676,16 @@ def patch_race_rom(rom, world, player):
|
|||||||
rom.encrypt(world, player)
|
rom.encrypt(world, player)
|
||||||
|
|
||||||
|
|
||||||
|
def get_price_data(price: int, price_type: int) -> bytes:
|
||||||
|
if price_type != ShopPriceType.Rupees:
|
||||||
|
# Set special price flag 0x8000
|
||||||
|
# Then set the type of price we're setting 0x7F00 (this starts from Hearts, not Rupees, subtract 1)
|
||||||
|
# Then append the price/index into the second byte 0x00FF
|
||||||
|
return int16_as_bytes(0x8000 | 0x100 * (price_type - 1) | price)
|
||||||
|
else:
|
||||||
|
return int16_as_bytes(price)
|
||||||
|
|
||||||
|
|
||||||
def write_custom_shops(rom, world, player):
|
def write_custom_shops(rom, world, player):
|
||||||
shops = sorted([shop for shop in world.shops if shop.custom and shop.region.player == player],
|
shops = sorted([shop for shop in world.shops if shop.custom and shop.region.player == player],
|
||||||
key=lambda shop: shop.sram_offset)
|
key=lambda shop: shop.sram_offset)
|
||||||
@@ -1704,9 +1717,11 @@ def write_custom_shops(rom, world, player):
|
|||||||
|
|
||||||
# [id][item][price-low][price-high][max][repl_id][repl_price-low][repl_price-high][player]
|
# [id][item][price-low][price-high][max][repl_id][repl_price-low][repl_price-high][player]
|
||||||
for index, item in enumerate(shop.inventory):
|
for index, item in enumerate(shop.inventory):
|
||||||
slot = 0 if shop.type == ShopType.TakeAny else index
|
|
||||||
if item is None:
|
if item is None:
|
||||||
break
|
break
|
||||||
|
price_data = get_price_data(item['price'], item["price_type"])
|
||||||
|
replacement_price_data = get_price_data(item['replacement_price'], item['replacement_price_type'])
|
||||||
|
slot = 0 if shop.type == ShopType.TakeAny else index
|
||||||
if not item['item'] in item_table: # item not native to ALTTP
|
if not item['item'] in item_table: # item not native to ALTTP
|
||||||
item_code = get_nonnative_item_sprite(item['item'])
|
item_code = get_nonnative_item_sprite(item['item'])
|
||||||
else:
|
else:
|
||||||
@@ -1714,9 +1729,9 @@ def write_custom_shops(rom, world, player):
|
|||||||
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]:
|
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]:
|
||||||
rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask)
|
rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask)
|
||||||
|
|
||||||
item_data = [shop_id, item_code] + int16_as_bytes(item['price']) + \
|
item_data = [shop_id, item_code] + price_data + \
|
||||||
[item['max'], ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + \
|
[item['max'], ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + \
|
||||||
int16_as_bytes(item['replacement_price']) + [0 if item['player'] == player else item['player']]
|
replacement_price_data + [0 if item['player'] == player else item['player']]
|
||||||
items_data.extend(item_data)
|
items_data.extend(item_data)
|
||||||
|
|
||||||
rom.write_bytes(0x184800, shop_data)
|
rom.write_bytes(0x184800, shop_data)
|
||||||
@@ -1784,12 +1799,16 @@ def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, spri
|
|||||||
rom.write_bytes(0x123FE, [0x72]) # set lightning flash in misery mire (and standard) to brightness 0x72
|
rom.write_bytes(0x123FE, [0x72]) # set lightning flash in misery mire (and standard) to brightness 0x72
|
||||||
rom.write_bytes(0x3FA7B, [0x80, 0xac - 0x7b]) # branch from palette writing lightning on death mountain
|
rom.write_bytes(0x3FA7B, [0x80, 0xac - 0x7b]) # branch from palette writing lightning on death mountain
|
||||||
rom.write_byte(0x10817F, 0x01) # internal rom option
|
rom.write_byte(0x10817F, 0x01) # internal rom option
|
||||||
|
rom.write_byte(0x3FAB6, 0x80) # GT flashing
|
||||||
|
rom.write_byte(0x3FAC2, 0x80) # GT flashing
|
||||||
else:
|
else:
|
||||||
rom.write_bytes(0x17E07, [0x00])
|
rom.write_bytes(0x17E07, [0x00])
|
||||||
rom.write_bytes(0x17EAB, [0x85, 0x00, 0x29, 0x1F, 0x00, 0x18])
|
rom.write_bytes(0x17EAB, [0x85, 0x00, 0x29, 0x1F, 0x00, 0x18])
|
||||||
rom.write_bytes(0x123FE, [0x32]) # original weather flash value
|
rom.write_bytes(0x123FE, [0x32]) # original weather flash value
|
||||||
rom.write_bytes(0x3FA7B, [0xc2, 0x20]) # rep #$20
|
rom.write_bytes(0x3FA7B, [0xc2, 0x20]) # rep #$20
|
||||||
rom.write_byte(0x10817F, 0x00) # internal rom option
|
rom.write_byte(0x10817F, 0x00) # internal rom option
|
||||||
|
rom.write_byte(0x3FAB6, 0xF0) # GT flashing
|
||||||
|
rom.write_byte(0x3FAC2, 0xD0) # GT flashing
|
||||||
|
|
||||||
rom.write_byte(0x18004B, 0x01 if quickswap else 0x00)
|
rom.write_byte(0x18004B, 0x01 if quickswap else 0x00)
|
||||||
|
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ def global_rules(world, player):
|
|||||||
if world.accessibility[player] != 'locations':
|
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_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))
|
set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player))
|
||||||
if not world.smallkey_shuffle[player] and world.logic[player] != 'nologic':
|
if not world.smallkey_shuffle[player] and world.logic[player] not in ['hybridglitches', 'nologic']:
|
||||||
forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
|
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('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from enum import unique, Enum
|
from enum import unique, IntEnum
|
||||||
from typing import List, Optional, Set, NamedTuple, Dict
|
from typing import List, Optional, Set, NamedTuple, Dict
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -13,12 +13,27 @@ logger = logging.getLogger("Shops")
|
|||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
class ShopType(Enum):
|
class ShopType(IntEnum):
|
||||||
Shop = 0
|
Shop = 0
|
||||||
TakeAny = 1
|
TakeAny = 1
|
||||||
UpgradeShop = 2
|
UpgradeShop = 2
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class ShopPriceType(IntEnum):
|
||||||
|
Rupees = 0
|
||||||
|
Hearts = 1
|
||||||
|
Magic = 2
|
||||||
|
Bombs = 3
|
||||||
|
Arrows = 4
|
||||||
|
HeartContainer = 5
|
||||||
|
BombUpgrade = 6
|
||||||
|
ArrowUpgrade = 7
|
||||||
|
Keys = 8
|
||||||
|
Potion = 9
|
||||||
|
Item = 10
|
||||||
|
|
||||||
|
|
||||||
class Shop():
|
class Shop():
|
||||||
slots: int = 3 # slot count is not dynamic in asm, however inventory can have None as empty slots
|
slots: int = 3 # slot count is not dynamic in asm, however inventory can have None as empty slots
|
||||||
blacklist: Set[str] = set() # items that don't work, todo: actually check against this
|
blacklist: Set[str] = set() # items that don't work, todo: actually check against this
|
||||||
@@ -87,18 +102,22 @@ class Shop():
|
|||||||
|
|
||||||
def add_inventory(self, slot: int, item: str, price: int, max: int = 0,
|
def add_inventory(self, slot: int, item: str, price: int, max: int = 0,
|
||||||
replacement: Optional[str] = None, replacement_price: int = 0, create_location: bool = False,
|
replacement: Optional[str] = None, replacement_price: int = 0, create_location: bool = False,
|
||||||
player: int = 0):
|
player: int = 0, price_type: int = ShopPriceType.Rupees,
|
||||||
|
replacement_price_type: int = ShopPriceType.Rupees):
|
||||||
self.inventory[slot] = {
|
self.inventory[slot] = {
|
||||||
'item': item,
|
'item': item,
|
||||||
'price': price,
|
'price': price,
|
||||||
|
'price_type': price_type,
|
||||||
'max': max,
|
'max': max,
|
||||||
'replacement': replacement,
|
'replacement': replacement,
|
||||||
'replacement_price': replacement_price,
|
'replacement_price': replacement_price,
|
||||||
|
'replacement_price_type': replacement_price_type,
|
||||||
'create_location': create_location,
|
'create_location': create_location,
|
||||||
'player': player
|
'player': player
|
||||||
}
|
}
|
||||||
|
|
||||||
def push_inventory(self, slot: int, item: str, price: int, max: int = 1, player: int = 0):
|
def push_inventory(self, slot: int, item: str, price: int, max: int = 1, player: int = 0,
|
||||||
|
price_type: int = ShopPriceType.Rupees):
|
||||||
if not self.inventory[slot]:
|
if not self.inventory[slot]:
|
||||||
raise ValueError("Inventory can't be pushed back if it doesn't exist")
|
raise ValueError("Inventory can't be pushed back if it doesn't exist")
|
||||||
|
|
||||||
@@ -108,9 +127,11 @@ class Shop():
|
|||||||
self.inventory[slot] = {
|
self.inventory[slot] = {
|
||||||
'item': item,
|
'item': item,
|
||||||
'price': price,
|
'price': price,
|
||||||
|
'price_type': price_type,
|
||||||
'max': max,
|
'max': max,
|
||||||
'replacement': self.inventory[slot]["item"],
|
'replacement': self.inventory[slot]["item"],
|
||||||
'replacement_price': self.inventory[slot]["price"],
|
'replacement_price': self.inventory[slot]["price"],
|
||||||
|
'replacement_price_type': self.inventory[slot]["price_type"],
|
||||||
'create_location': self.inventory[slot]["create_location"],
|
'create_location': self.inventory[slot]["create_location"],
|
||||||
'player': player
|
'player': player
|
||||||
}
|
}
|
||||||
@@ -170,7 +191,8 @@ def ShopSlotFill(world):
|
|||||||
blacklist_word in item_name for blacklist_word in blacklist_words)}
|
blacklist_word in item_name for blacklist_word in blacklist_words)}
|
||||||
blacklist_words.add("Bee")
|
blacklist_words.add("Bee")
|
||||||
|
|
||||||
locations_per_sphere = list(sorted(sphere, key=lambda location: location.name) for sphere in world.get_spheres())
|
locations_per_sphere = list(
|
||||||
|
sorted(sphere, key=lambda location: location.name) for sphere in world.get_spheres())
|
||||||
|
|
||||||
# currently special care needs to be taken so that Shop.region.locations.item is identical to Shop.inventory
|
# currently special care needs to be taken so that Shop.region.locations.item is identical to Shop.inventory
|
||||||
# Potentially create Locations as needed and make inventory the only source, to prevent divergence
|
# Potentially create Locations as needed and make inventory the only source, to prevent divergence
|
||||||
@@ -226,7 +248,8 @@ def ShopSlotFill(world):
|
|||||||
item_name = location.item.name
|
item_name = location.item.name
|
||||||
if location.item.game != "A Link to the Past":
|
if location.item.game != "A Link to the Past":
|
||||||
price = world.random.randrange(1, 28)
|
price = world.random.randrange(1, 28)
|
||||||
elif any(x in item_name for x in ['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
|
elif any(x in item_name for x in
|
||||||
|
['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
|
||||||
price = world.random.randrange(1, 7)
|
price = world.random.randrange(1, 7)
|
||||||
elif any(x in item_name for x in ['Arrow', 'Bomb', 'Clock']):
|
elif any(x in item_name for x in ['Arrow', 'Bomb', 'Clock']):
|
||||||
price = world.random.randrange(2, 14)
|
price = world.random.randrange(2, 14)
|
||||||
@@ -237,6 +260,8 @@ def ShopSlotFill(world):
|
|||||||
|
|
||||||
shop.push_inventory(location.shop_slot, 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)
|
location.item.player if location.item.player != location.player else 0)
|
||||||
|
if 'P' in world.shop_shuffle[location.player]:
|
||||||
|
price_to_funny_price(shop.inventory[location.shop_slot], world, location.player)
|
||||||
|
|
||||||
|
|
||||||
def create_shops(world, player: int):
|
def create_shops(world, player: int):
|
||||||
@@ -254,7 +279,9 @@ def create_shops(world, player: int):
|
|||||||
world.random.shuffle(single_purchase_slots)
|
world.random.shuffle(single_purchase_slots)
|
||||||
|
|
||||||
if 'g' in option or 'f' in option:
|
if 'g' in option or 'f' in option:
|
||||||
default_shop_table = [i for l in [shop_generation_types[x] for x in ['arrows', 'bombs', 'potions', 'shields', 'bottle'] if not world.retro[player] or x != 'arrows'] for i in l]
|
default_shop_table = [i for l in
|
||||||
|
[shop_generation_types[x] for x in ['arrows', 'bombs', 'potions', 'shields', 'bottle'] if
|
||||||
|
not world.retro[player] or x != 'arrows'] for i in l]
|
||||||
new_basic_shop = world.random.sample(default_shop_table, k=3)
|
new_basic_shop = world.random.sample(default_shop_table, k=3)
|
||||||
new_dark_shop = world.random.sample(default_shop_table, k=3)
|
new_dark_shop = world.random.sample(default_shop_table, k=3)
|
||||||
for name, shop in player_shop_table.items():
|
for name, shop in player_shop_table.items():
|
||||||
@@ -272,7 +299,8 @@ def create_shops(world, player: int):
|
|||||||
# make sure that blue potion is available in inverted, special case locked = None; lock when done.
|
# make sure that blue potion is available in inverted, special case locked = None; lock when done.
|
||||||
player_shop_table["Dark Lake Hylia Shop"] = \
|
player_shop_table["Dark Lake Hylia Shop"] = \
|
||||||
player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None)
|
player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None)
|
||||||
chance_100 = int(world.retro[player])*0.25+int(world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) * 0.5
|
chance_100 = int(world.retro[player]) * 0.25 + int(
|
||||||
|
world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) * 0.5
|
||||||
for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram_offset) in player_shop_table.items():
|
for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram_offset) in player_shop_table.items():
|
||||||
region = world.get_region(region_name, player)
|
region = world.get_region(region_name, player)
|
||||||
shop: Shop = shop_class_mapping[type](region, room_id, shopkeeper, custom, locked, sram_offset)
|
shop: Shop = shop_class_mapping[type](region, room_id, shopkeeper, custom, locked, sram_offset)
|
||||||
@@ -344,7 +372,8 @@ total_dynamic_shop_slots = sum(3 for shopname, data in shop_table.items() if not
|
|||||||
|
|
||||||
SHOP_ID_START = 0x400000
|
SHOP_ID_START = 0x400000
|
||||||
shop_table_by_location_id = dict(enumerate(
|
shop_table_by_location_id = dict(enumerate(
|
||||||
(f"{name} {Shop.slot_names[num]}" for name, shop_data in sorted(shop_table.items(), key=lambda item: item[1].sram_offset)
|
(f"{name} {Shop.slot_names[num]}" for name, shop_data in
|
||||||
|
sorted(shop_table.items(), key=lambda item: item[1].sram_offset)
|
||||||
for num in range(3)), start=SHOP_ID_START))
|
for num in range(3)), start=SHOP_ID_START))
|
||||||
|
|
||||||
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots)] = "Old Man Sword Cave"
|
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots)] = "Old Man Sword Cave"
|
||||||
@@ -371,7 +400,8 @@ def set_up_shops(world, player: int):
|
|||||||
if world.retro[player]:
|
if world.retro[player]:
|
||||||
rss = world.get_region('Red Shield Shop', player).shop
|
rss = world.get_region('Red Shield Shop', player).shop
|
||||||
replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50],
|
replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50],
|
||||||
['Blue Shield', 50], ['Small Heart', 10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them.
|
['Blue Shield', 50], ['Small Heart',
|
||||||
|
10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them.
|
||||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||||
replacement_items.append(['Small Key (Universal)', 100])
|
replacement_items.append(['Small Key (Universal)', 100])
|
||||||
replacement_item = world.random.choice(replacement_items)
|
replacement_item = world.random.choice(replacement_items)
|
||||||
@@ -396,7 +426,7 @@ def shuffle_shops(world, items, player: int):
|
|||||||
option = world.shop_shuffle[player]
|
option = world.shop_shuffle[player]
|
||||||
if 'u' in option:
|
if 'u' in option:
|
||||||
progressive = world.progressive[player]
|
progressive = world.progressive[player]
|
||||||
progressive = world.random.choice([True, False]) if progressive == 'random' else progressive == 'on'
|
progressive = world.random.choice([True, False]) if progressive == 'grouped_random' else progressive == 'on'
|
||||||
progressive &= world.goal == 'icerodhunt'
|
progressive &= world.goal == 'icerodhunt'
|
||||||
new_items = ["Bomb Upgrade (+5)"] * 6
|
new_items = ["Bomb Upgrade (+5)"] * 6
|
||||||
new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
|
new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
|
||||||
@@ -421,7 +451,8 @@ def shuffle_shops(world, items, player: int):
|
|||||||
if not new_items:
|
if not new_items:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Not all upgrades put into Player{player}' item pool. Putting remaining items in Capacity Upgrade shop instead.")
|
logging.warning(
|
||||||
|
f"Not all upgrades put into Player{player}' item pool. Putting remaining items in Capacity Upgrade shop instead.")
|
||||||
bombupgrades = sum(1 for item in new_items if 'Bomb Upgrade' in item)
|
bombupgrades = sum(1 for item in new_items if 'Bomb Upgrade' in item)
|
||||||
arrowupgrades = sum(1 for item in new_items if 'Arrow Upgrade' in item)
|
arrowupgrades = sum(1 for item in new_items if 'Arrow Upgrade' in item)
|
||||||
if bombupgrades:
|
if bombupgrades:
|
||||||
@@ -432,7 +463,7 @@ def shuffle_shops(world, items, player: int):
|
|||||||
for item in new_items:
|
for item in new_items:
|
||||||
world.push_precollected(ItemFactory(item, player))
|
world.push_precollected(ItemFactory(item, player))
|
||||||
|
|
||||||
if 'p' in option or 'i' in option:
|
if any(setting in option for setting in 'ipP'):
|
||||||
shops = []
|
shops = []
|
||||||
upgrade_shops = []
|
upgrade_shops = []
|
||||||
total_inventory = []
|
total_inventory = []
|
||||||
@@ -461,6 +492,13 @@ def shuffle_shops(world, items, player: int):
|
|||||||
for item in shop.inventory:
|
for item in shop.inventory:
|
||||||
adjust_item(item)
|
adjust_item(item)
|
||||||
|
|
||||||
|
if 'P' in option:
|
||||||
|
for item in total_inventory:
|
||||||
|
price_to_funny_price(item, world, player)
|
||||||
|
# Don't apply to upgrade shops
|
||||||
|
# Upgrade shop is only one place, and will generally be too easy to
|
||||||
|
# replenish hearts and bombs
|
||||||
|
|
||||||
if 'i' in option:
|
if 'i' in option:
|
||||||
world.random.shuffle(total_inventory)
|
world.random.shuffle(total_inventory)
|
||||||
|
|
||||||
@@ -469,3 +507,82 @@ def shuffle_shops(world, items, player: int):
|
|||||||
slots = shop.slots
|
slots = shop.slots
|
||||||
shop.inventory = total_inventory[i:i + slots]
|
shop.inventory = total_inventory[i:i + slots]
|
||||||
i += slots
|
i += slots
|
||||||
|
|
||||||
|
|
||||||
|
price_blacklist = {
|
||||||
|
ShopPriceType.Rupees: {'Rupees'},
|
||||||
|
ShopPriceType.Hearts: {'Small Heart', 'Apple'},
|
||||||
|
ShopPriceType.Magic: {'Magic Jar'},
|
||||||
|
ShopPriceType.Bombs: {'Bombs', 'Single Bomb'},
|
||||||
|
ShopPriceType.Arrows: {'Arrows', 'Single Arrow'},
|
||||||
|
ShopPriceType.HeartContainer: {},
|
||||||
|
ShopPriceType.BombUpgrade: {"Bomb Upgrade"},
|
||||||
|
ShopPriceType.ArrowUpgrade: {"Arrow Upgrade"},
|
||||||
|
ShopPriceType.Keys: {"Small Key"},
|
||||||
|
ShopPriceType.Potion: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
price_chart = {
|
||||||
|
ShopPriceType.Rupees: lambda p: p,
|
||||||
|
ShopPriceType.Hearts: lambda p: min(5, p // 5) * 8, # Each heart is 0x8 in memory, Max of 5 hearts (20 total??)
|
||||||
|
ShopPriceType.Magic: lambda p: min(15, p // 5) * 8, # Each pip is 0x8 in memory, Max of 15 pips (16 total...)
|
||||||
|
ShopPriceType.Bombs: lambda p: max(1, min(10, p // 5)), # 10 Bombs max
|
||||||
|
ShopPriceType.Arrows: lambda p: max(1, min(30, p // 5)), # 30 Arrows Max
|
||||||
|
ShopPriceType.HeartContainer: lambda p: 0x8,
|
||||||
|
ShopPriceType.BombUpgrade: lambda p: 0x1,
|
||||||
|
ShopPriceType.ArrowUpgrade: lambda p: 0x1,
|
||||||
|
ShopPriceType.Keys: lambda p: min(3, (p // 100) + 1), # Max of 3 keys for a price
|
||||||
|
ShopPriceType.Potion: lambda p: (p // 5) % 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
price_type_display_name = {
|
||||||
|
ShopPriceType.Rupees: "Rupees",
|
||||||
|
ShopPriceType.Hearts: "Hearts",
|
||||||
|
ShopPriceType.Bombs: "Bombs",
|
||||||
|
ShopPriceType.Arrows: "Arrows",
|
||||||
|
ShopPriceType.Keys: "Keys",
|
||||||
|
}
|
||||||
|
|
||||||
|
# price division
|
||||||
|
price_rate_display = {
|
||||||
|
ShopPriceType.Hearts: 8,
|
||||||
|
ShopPriceType.Magic: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
# prices with no? logic requirements
|
||||||
|
simple_price_types = [
|
||||||
|
ShopPriceType.Rupees,
|
||||||
|
ShopPriceType.Hearts,
|
||||||
|
ShopPriceType.Bombs,
|
||||||
|
ShopPriceType.Arrows,
|
||||||
|
ShopPriceType.Keys
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def price_to_funny_price(item: dict, world, player: int):
|
||||||
|
"""
|
||||||
|
Converts a raw Rupee price into a special price type
|
||||||
|
"""
|
||||||
|
if item:
|
||||||
|
my_price_types = simple_price_types.copy()
|
||||||
|
world.random.shuffle(my_price_types)
|
||||||
|
for p_type in my_price_types:
|
||||||
|
# Ignore rupee prices, logic-based prices or Keys (if we're not on universal keys)
|
||||||
|
if p_type in [ShopPriceType.Rupees, ShopPriceType.BombUpgrade, ShopPriceType.ArrowUpgrade]:
|
||||||
|
return
|
||||||
|
# If we're using keys...
|
||||||
|
# Check if we're in universal, check if our replacement isn't a Small Key
|
||||||
|
# Check if price isn't super small... (this will ideally be handled in a future table)
|
||||||
|
if p_type in [ShopPriceType.Keys]:
|
||||||
|
if world.smallkey_shuffle[player] != smallkey_shuffle.option_universal:
|
||||||
|
continue
|
||||||
|
elif item['replacement'] and 'Small Key' in item['replacement']:
|
||||||
|
continue
|
||||||
|
if item['price'] < 50:
|
||||||
|
continue
|
||||||
|
if any(x in item['item'] for x in price_blacklist[p_type]):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
item['price'] = min(price_chart[p_type](item['price']) , 255)
|
||||||
|
item['price_type'] = p_type
|
||||||
|
break
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ class ALttPItem(Item):
|
|||||||
game: str = "A Link to the Past"
|
game: str = "A Link to the Past"
|
||||||
dungeon = None
|
dungeon = None
|
||||||
|
|
||||||
def __init__(self, name, player, advancement=False, type=None, item_code=None, pedestal_hint=None, pedestal_credit=None,
|
def __init__(self, name, player, advancement=False, type=None, item_code=None, pedestal_hint=None,
|
||||||
sick_kid_credit=None, zora_credit=None, witch_credit=None, flute_boy_credit=None, hint_text=None):
|
pedestal_credit=None, sick_kid_credit=None, zora_credit=None, witch_credit=None,
|
||||||
|
flute_boy_credit=None, hint_text=None, trap=False):
|
||||||
super(ALttPItem, self).__init__(name, advancement, item_code, player)
|
super(ALttPItem, self).__init__(name, advancement, item_code, player)
|
||||||
self.type = type
|
self.type = type
|
||||||
self._pedestal_hint_text = pedestal_hint
|
self._pedestal_hint_text = pedestal_hint
|
||||||
@@ -31,6 +32,8 @@ class ALttPItem(Item):
|
|||||||
self.magicshop_credit_text = witch_credit
|
self.magicshop_credit_text = witch_credit
|
||||||
self.fluteboy_credit_text = flute_boy_credit
|
self.fluteboy_credit_text = flute_boy_credit
|
||||||
self._hint_text = hint_text
|
self._hint_text = hint_text
|
||||||
|
if trap:
|
||||||
|
self.trap = trap
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def crystal(self) -> bool:
|
def crystal(self) -> bool:
|
||||||
|
|||||||
@@ -60,20 +60,20 @@ class ALTTPWorld(World):
|
|||||||
world = self.world
|
world = self.world
|
||||||
|
|
||||||
# system for sharing ER layouts
|
# system for sharing ER layouts
|
||||||
world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
|
self.er_seed = str(world.random.randint(0, 2 ** 64))
|
||||||
|
|
||||||
if "-" in world.shuffle[player]:
|
if "-" in world.shuffle[player]:
|
||||||
shuffle, seed = world.shuffle[player].split("-", 1)
|
shuffle, seed = world.shuffle[player].split("-", 1)
|
||||||
world.shuffle[player] = shuffle
|
world.shuffle[player] = shuffle
|
||||||
if shuffle == "vanilla":
|
if shuffle == "vanilla":
|
||||||
world.er_seeds[player] = "vanilla"
|
self.er_seed = "vanilla"
|
||||||
elif seed.startswith("group-") or world.is_race:
|
elif seed.startswith("group-") or world.is_race:
|
||||||
world.er_seeds[player] = get_same_seed(world, (
|
self.er_seed = get_same_seed(world, (
|
||||||
shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
|
shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
|
||||||
else: # not a race or group seed, use set seed as is.
|
else: # not a race or group seed, use set seed as is.
|
||||||
world.er_seeds[player] = seed
|
self.er_seed = seed
|
||||||
elif world.shuffle[player] == "vanilla":
|
elif world.shuffle[player] == "vanilla":
|
||||||
world.er_seeds[player] = "vanilla"
|
self.er_seed = "vanilla"
|
||||||
for dungeon_item in ["smallkey_shuffle", "bigkey_shuffle", "compass_shuffle", "map_shuffle"]:
|
for dungeon_item in ["smallkey_shuffle", "bigkey_shuffle", "compass_shuffle", "map_shuffle"]:
|
||||||
option = getattr(world, dungeon_item)[player]
|
option = getattr(world, dungeon_item)[player]
|
||||||
if option == "own_world":
|
if option == "own_world":
|
||||||
@@ -118,7 +118,7 @@ class ALTTPWorld(World):
|
|||||||
|
|
||||||
# seeded entrance shuffle
|
# seeded entrance shuffle
|
||||||
old_random = world.random
|
old_random = world.random
|
||||||
world.random = random.Random(world.er_seeds[player])
|
world.random = random.Random(self.er_seed)
|
||||||
|
|
||||||
if world.mode[player] != 'inverted':
|
if world.mode[player] != 'inverted':
|
||||||
link_entrances(world, player)
|
link_entrances(world, player)
|
||||||
|
|||||||
@@ -100,6 +100,9 @@ def generate_mod(world, output_directory: str):
|
|||||||
for factorio_option in Options.factorio_options:
|
for factorio_option in Options.factorio_options:
|
||||||
template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value
|
template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value
|
||||||
|
|
||||||
|
if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe:
|
||||||
|
template_data["free_sample_blacklist"]["rocket-silo"] = 1
|
||||||
|
|
||||||
control_code = control_template.render(**template_data)
|
control_code = control_template.render(**template_data)
|
||||||
data_template_code = data_template.render(**template_data)
|
data_template_code = data_template.render(**template_data)
|
||||||
data_final_fixes_code = data_final_template.render(**template_data)
|
data_final_fixes_code = data_final_template.render(**template_data)
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ class Progressive(Choice):
|
|||||||
alias_false = 0
|
alias_false = 0
|
||||||
alias_true = 2
|
alias_true = 2
|
||||||
default = 2
|
default = 2
|
||||||
alias_random = 1
|
|
||||||
|
|
||||||
def want_progressives(self, random):
|
def want_progressives(self, random):
|
||||||
return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)
|
return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)
|
||||||
|
|||||||
@@ -84,11 +84,17 @@ class CustomTechnology(Technology):
|
|||||||
|
|
||||||
def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int):
|
def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int):
|
||||||
ingredients = origin.ingredients & allowed_packs
|
ingredients = origin.ingredients & allowed_packs
|
||||||
|
military_allowed = "military-science-pack" in allowed_packs \
|
||||||
|
and (ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"})
|
||||||
self.player = player
|
self.player = player
|
||||||
if origin.name not in world.worlds[player].static_nodes:
|
if origin.name not in world.worlds[player].static_nodes:
|
||||||
|
if military_allowed:
|
||||||
|
ingredients.add("military-science-pack")
|
||||||
ingredients = list(ingredients)
|
ingredients = list(ingredients)
|
||||||
ingredients.sort() # deterministic sample
|
ingredients.sort() # deterministic sample
|
||||||
ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients)))
|
ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients)))
|
||||||
|
elif origin.name == "rocket-silo" and military_allowed:
|
||||||
|
ingredients.add("military-science-pack")
|
||||||
super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id)
|
super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
|
|||||||
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies
|
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies
|
||||||
from .Shapes import get_shapes
|
from .Shapes import get_shapes
|
||||||
from .Mod import generate_mod
|
from .Mod import generate_mod
|
||||||
from .Options import factorio_options, Silo
|
from .Options import factorio_options, Silo, TechTreeInformation
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -66,6 +66,9 @@ class Factorio(World):
|
|||||||
if map_basic_settings.get("seed", None) is None: # allow seed 0
|
if map_basic_settings.get("seed", None) is None: # allow seed 0
|
||||||
map_basic_settings["seed"] = self.world.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint
|
map_basic_settings["seed"] = self.world.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint
|
||||||
|
|
||||||
|
self.sending_visible = self.world.tech_tree_information[player] == TechTreeInformation.option_full
|
||||||
|
|
||||||
|
|
||||||
generate_output = generate_mod
|
generate_output = generate_mod
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
@@ -272,7 +275,7 @@ class Factorio(World):
|
|||||||
|
|
||||||
if self.world.silo[self.player].value == Silo.option_randomize_recipe:
|
if self.world.silo[self.player].value == Silo.option_randomize_recipe:
|
||||||
valid_pool = []
|
valid_pool = []
|
||||||
for pack in self.world.max_science_pack[self.player].get_allowed_packs():
|
for pack in sorted(self.world.max_science_pack[self.player].get_allowed_packs()):
|
||||||
valid_pool += sorted(science_pack_pools[pack])
|
valid_pool += sorted(science_pack_pools[pack])
|
||||||
new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool,
|
new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool,
|
||||||
factor=(self.world.max_science_pack[self.player].value + 1) / 7)
|
factor=(self.world.max_science_pack[self.player].value + 1) / 7)
|
||||||
@@ -292,5 +295,8 @@ class Factorio(World):
|
|||||||
return FactorioItem(name, name in advancement_technologies or
|
return FactorioItem(name, name in advancement_technologies or
|
||||||
name in self.additional_advancement_technologies,
|
name in self.additional_advancement_technologies,
|
||||||
tech_table[name], self.player)
|
tech_table[name], self.player)
|
||||||
elif name in all_items:
|
|
||||||
return FactorioItem(name, False, all_items[name], self.player)
|
item = FactorioItem(name, False, all_items[name], self.player)
|
||||||
|
if "Trap" in name:
|
||||||
|
item.trap = True
|
||||||
|
return item
|
||||||
|
|||||||
@@ -9,6 +9,24 @@ function filter_ingredients(ingredients, ingredient_filter)
|
|||||||
return new_ingredient_list
|
return new_ingredient_list
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function add_ingredients(ingredients, added_ingredients)
|
||||||
|
local new_ingredient_list = table.deepcopy(ingredients)
|
||||||
|
for new_ingredient, count in pairs(added_ingredients) do
|
||||||
|
local found = false
|
||||||
|
for _, old_ingredient in pairs(ingredients) do
|
||||||
|
if old_ingredient[1] == new_ingredient then
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not found then
|
||||||
|
table.insert(new_ingredient_list, {new_ingredient, count})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return new_ingredient_list
|
||||||
|
end
|
||||||
|
|
||||||
function get_any_stack_size(name)
|
function get_any_stack_size(name)
|
||||||
local item = game.item_prototypes[name]
|
local item = game.item_prototypes[name]
|
||||||
if item ~= nil then
|
if item ~= nil then
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ function prep_copy(new_copy, old_tech)
|
|||||||
local ingredient_filter = allowed_ingredients[old_tech.name]
|
local ingredient_filter = allowed_ingredients[old_tech.name]
|
||||||
if ingredient_filter ~= nil then
|
if ingredient_filter ~= nil then
|
||||||
new_copy.unit.ingredients = filter_ingredients(new_copy.unit.ingredients, ingredient_filter)
|
new_copy.unit.ingredients = filter_ingredients(new_copy.unit.ingredients, ingredient_filter)
|
||||||
|
new_copy.unit.ingredients = add_ingredients(new_copy.unit.ingredients, ingredient_filter)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -127,5 +128,6 @@ adjust_energy("{{ recipe_name }}", {{ flop_random(*recipe_time_scale) }})
|
|||||||
|
|
||||||
{%- if silo==2 %}
|
{%- if silo==2 %}
|
||||||
-- disable silo research for pre-placed silo
|
-- disable silo research for pre-placed silo
|
||||||
technologies["rocket-silo"].hidden = true
|
technologies["rocket-silo"].enabled = false
|
||||||
|
technologies["rocket-silo"].visible_when_disabled = false
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
def locality_rules(world, player):
|
import typing
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
import BaseClasses
|
||||||
|
|
||||||
|
CollectionRule = typing.Callable[[BaseClasses.CollectionState], bool]
|
||||||
|
ItemRule = typing.Callable[[BaseClasses.Item], bool]
|
||||||
|
else:
|
||||||
|
CollectionRule = typing.Callable[[object], bool]
|
||||||
|
ItemRule = typing.Callable[[object], bool]
|
||||||
|
|
||||||
|
|
||||||
|
def locality_rules(world, player: int):
|
||||||
if world.local_items[player].value:
|
if world.local_items[player].value:
|
||||||
for location in world.get_locations():
|
for location in world.get_locations():
|
||||||
if location.player != player:
|
if location.player != player:
|
||||||
@@ -9,18 +21,22 @@ def locality_rules(world, player):
|
|||||||
forbid_items_for_player(location, world.non_local_items[player].value, player)
|
forbid_items_for_player(location, world.non_local_items[player].value, player)
|
||||||
|
|
||||||
|
|
||||||
def exclusion_rules(world, player: int, exclude_locations: set):
|
def exclusion_rules(world, player: int, exclude_locations: typing.Set[str]):
|
||||||
for loc_name in exclude_locations:
|
for loc_name in exclude_locations:
|
||||||
location = world.get_location(loc_name, player)
|
try:
|
||||||
add_item_rule(location, lambda i: not (i.advancement or i.never_exclude))
|
location = world.get_location(loc_name, player)
|
||||||
location.excluded = True
|
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
|
||||||
|
if loc_name not in world.worlds[player].location_name_to_id:
|
||||||
|
raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e
|
||||||
|
else:
|
||||||
|
add_item_rule(location, lambda i: not (i.advancement or i.never_exclude))
|
||||||
|
location.excluded = True
|
||||||
|
|
||||||
|
def set_rule(spot, rule: CollectionRule):
|
||||||
def set_rule(spot, rule):
|
|
||||||
spot.access_rule = rule
|
spot.access_rule = rule
|
||||||
|
|
||||||
|
|
||||||
def add_rule(spot, rule, combine='and'):
|
def add_rule(spot, rule: CollectionRule, combine='and'):
|
||||||
old_rule = spot.access_rule
|
old_rule = spot.access_rule
|
||||||
if combine == 'or':
|
if combine == 'or':
|
||||||
spot.access_rule = lambda state: rule(state) or old_rule(state)
|
spot.access_rule = lambda state: rule(state) or old_rule(state)
|
||||||
@@ -28,36 +44,36 @@ def add_rule(spot, rule, combine='and'):
|
|||||||
spot.access_rule = lambda state: rule(state) and old_rule(state)
|
spot.access_rule = lambda state: rule(state) and old_rule(state)
|
||||||
|
|
||||||
|
|
||||||
def forbid_item(location, item, player: int):
|
def forbid_item(location, item: str, player: int):
|
||||||
old_rule = location.item_rule
|
old_rule = location.item_rule
|
||||||
location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i)
|
location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i)
|
||||||
|
|
||||||
|
|
||||||
def forbid_items_for_player(location, items: set, player: int):
|
def forbid_items_for_player(location, items: typing.Set[str], player: int):
|
||||||
old_rule = location.item_rule
|
old_rule = location.item_rule
|
||||||
location.item_rule = lambda i: (i.player != player or i.name not in items) and old_rule(i)
|
location.item_rule = lambda i: (i.player != player or i.name not in items) and old_rule(i)
|
||||||
|
|
||||||
|
|
||||||
def forbid_items(location, items: set):
|
def forbid_items(location, items: typing.Set[str]):
|
||||||
"""unused, but kept as a debugging tool."""
|
"""unused, but kept as a debugging tool."""
|
||||||
old_rule = location.item_rule
|
old_rule = location.item_rule
|
||||||
location.item_rule = lambda i: i.name not in items and old_rule(i)
|
location.item_rule = lambda i: i.name not in items and old_rule(i)
|
||||||
|
|
||||||
|
|
||||||
def add_item_rule(location, rule):
|
def add_item_rule(location, rule: ItemRule):
|
||||||
old_rule = location.item_rule
|
old_rule = location.item_rule
|
||||||
location.item_rule = lambda item: rule(item) and old_rule(item)
|
location.item_rule = lambda item: rule(item) and old_rule(item)
|
||||||
|
|
||||||
|
|
||||||
def item_in_locations(state, item, player, locations):
|
def item_in_locations(state, item: str, player: int, locations: typing.Sequence):
|
||||||
for location in locations:
|
for location in locations:
|
||||||
if item_name(state, location[0], location[1]) == (item, player):
|
if item_name(state, location[0], location[1]) == (item, player):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def item_name(state, location, player):
|
def item_name(state, location: str, player: int) -> typing.Optional[typing.Tuple[str, int]]:
|
||||||
location = state.world.get_location(location, player)
|
location = state.world.get_location(location, player)
|
||||||
if location.item is None:
|
if location.item is None:
|
||||||
return None
|
return None
|
||||||
return (location.item.name, location.item.player)
|
return location.item.name, location.item.player
|
||||||
|
|||||||
@@ -128,6 +128,14 @@ class LogicalChus(Toggle):
|
|||||||
displayname = "Bombchus Considered in Logic"
|
displayname = "Bombchus Considered in Logic"
|
||||||
|
|
||||||
|
|
||||||
|
class MQDungeons(Range):
|
||||||
|
"""Number of MQ dungeons. The dungeons to replace are randomly selected."""
|
||||||
|
displayname = "Number of MQ Dungeons"
|
||||||
|
range_start = 0
|
||||||
|
range_end = 12
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
world_options: typing.Dict[str, type(Option)] = {
|
world_options: typing.Dict[str, type(Option)] = {
|
||||||
"starting_age": StartingAge,
|
"starting_age": StartingAge,
|
||||||
# "shuffle_interior_entrances": InteriorEntrances,
|
# "shuffle_interior_entrances": InteriorEntrances,
|
||||||
@@ -141,7 +149,7 @@ world_options: typing.Dict[str, type(Option)] = {
|
|||||||
"triforce_goal": TriforceGoal,
|
"triforce_goal": TriforceGoal,
|
||||||
"extra_triforce_percentage": ExtraTriforces,
|
"extra_triforce_percentage": ExtraTriforces,
|
||||||
"bombchus_in_logic": LogicalChus,
|
"bombchus_in_logic": LogicalChus,
|
||||||
# "mq_dungeons": make_range(0, 12),
|
"mq_dungeons": MQDungeons,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ location_id_offset = 67000
|
|||||||
|
|
||||||
# OoT's generate_output doesn't benefit from more than 2 threads, instead it uses a lot of memory.
|
# OoT's generate_output doesn't benefit from more than 2 threads, instead it uses a lot of memory.
|
||||||
i_o_limiter = threading.Semaphore(2)
|
i_o_limiter = threading.Semaphore(2)
|
||||||
hint_data_available = threading.Event()
|
|
||||||
|
|
||||||
|
|
||||||
class OOTWorld(World):
|
class OOTWorld(World):
|
||||||
@@ -88,6 +87,10 @@ class OOTWorld(World):
|
|||||||
|
|
||||||
return super().__new__(cls)
|
return super().__new__(cls)
|
||||||
|
|
||||||
|
def __init__(self, world, player):
|
||||||
|
self.hint_data_available = threading.Event()
|
||||||
|
super(OOTWorld, self).__init__(world, player)
|
||||||
|
|
||||||
def generate_early(self):
|
def generate_early(self):
|
||||||
# Player name MUST be at most 16 bytes ascii-encoded, otherwise won't write to ROM correctly
|
# Player name MUST be at most 16 bytes ascii-encoded, otherwise won't write to ROM correctly
|
||||||
if len(bytes(self.world.get_player_name(self.player), 'ascii')) > 16:
|
if len(bytes(self.world.get_player_name(self.player), 'ascii')) > 16:
|
||||||
@@ -155,7 +158,6 @@ class OOTWorld(World):
|
|||||||
|
|
||||||
# Determine which dungeons are MQ
|
# Determine which dungeons are MQ
|
||||||
# Possible future plan: allow user to pick which dungeons are MQ
|
# Possible future plan: allow user to pick which dungeons are MQ
|
||||||
self.mq_dungeons = 0 # temporary disable for client-side issues
|
|
||||||
mq_dungeons = self.world.random.sample(dungeon_table, self.mq_dungeons)
|
mq_dungeons = self.world.random.sample(dungeon_table, self.mq_dungeons)
|
||||||
self.dungeon_mq = {item['name']: (item in mq_dungeons) for item in dungeon_table}
|
self.dungeon_mq = {item['name']: (item in mq_dungeons) for item in dungeon_table}
|
||||||
|
|
||||||
@@ -457,9 +459,7 @@ class OOTWorld(World):
|
|||||||
junk_pool = get_junk_pool(self)
|
junk_pool = get_junk_pool(self)
|
||||||
removed_items = []
|
removed_items = []
|
||||||
# Determine starting items
|
# Determine starting items
|
||||||
for item in self.world.precollected_items:
|
for item in self.world.precollected_items[self.player]:
|
||||||
if item.player != self.player:
|
|
||||||
continue
|
|
||||||
if item.name in self.remove_from_start_inventory:
|
if item.name in self.remove_from_start_inventory:
|
||||||
self.remove_from_start_inventory.remove(item.name)
|
self.remove_from_start_inventory.remove(item.name)
|
||||||
removed_items.append(item.name)
|
removed_items.append(item.name)
|
||||||
@@ -587,14 +587,20 @@ class OOTWorld(World):
|
|||||||
fill_restrictive(self.world, self.world.get_all_state(False), any_dungeon_locations,
|
fill_restrictive(self.world, self.world.get_all_state(False), any_dungeon_locations,
|
||||||
itempools['any_dungeon'], True, True)
|
itempools['any_dungeon'], True, True)
|
||||||
|
|
||||||
# If anything is overworld-only, enforce them as local and not in the remaining dungeon locations
|
# If anything is overworld-only, fill into local non-dungeon locations
|
||||||
if itempools['overworld'] or self.shuffle_fortresskeys == 'overworld':
|
if self.shuffle_fortresskeys == 'overworld':
|
||||||
from worlds.generic.Rules import forbid_items_for_player
|
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.world.itempool)
|
||||||
fortresskeys = {'Small Key (Gerudo Fortress)'} if self.shuffle_fortresskeys == 'overworld' else set()
|
itempools['overworld'].extend(fortresskeys)
|
||||||
local_overworld_items = set(map(lambda item: item.name, itempools['overworld'])).union(fortresskeys)
|
if itempools['overworld']:
|
||||||
for location in self.world.get_locations():
|
for item in itempools['overworld']:
|
||||||
if location.player != self.player or location in any_dungeon_locations:
|
self.world.itempool.remove(item)
|
||||||
forbid_items_for_player(location, local_overworld_items, self.player)
|
itempools['overworld'].sort(key=lambda item:
|
||||||
|
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0))
|
||||||
|
non_dungeon_locations = [loc for loc in self.get_locations() if not loc.item and loc not in any_dungeon_locations
|
||||||
|
and loc.type != 'Shop' and (loc.type != 'Song' or self.shuffle_song_items != 'song')]
|
||||||
|
self.world.random.shuffle(non_dungeon_locations)
|
||||||
|
fill_restrictive(self.world, self.world.get_all_state(False), non_dungeon_locations,
|
||||||
|
itempools['overworld'], True, True)
|
||||||
|
|
||||||
# Place songs
|
# Place songs
|
||||||
# 5 built-in retries because this section can fail sometimes
|
# 5 built-in retries because this section can fail sometimes
|
||||||
@@ -641,7 +647,11 @@ class OOTWorld(World):
|
|||||||
shop_locations = list(
|
shop_locations = list(
|
||||||
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
|
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
|
||||||
self.world.get_unfilled_locations(player=self.player)))
|
self.world.get_unfilled_locations(player=self.player)))
|
||||||
shop_items.sort(key=lambda item: 1 if item.name in {"Buy Goron Tunic", "Buy Zora Tunic"} else 0)
|
shop_items.sort(key=lambda item: {
|
||||||
|
'Buy Deku Shield': 3*int(self.open_forest == 'closed'),
|
||||||
|
'Buy Goron Tunic': 2,
|
||||||
|
'Buy Zora Tunic': 2
|
||||||
|
}.get(item.name, int(item.advancement))) # place Deku Shields if needed, then tunics, then other advancement, then junk
|
||||||
self.world.random.shuffle(shop_locations)
|
self.world.random.shuffle(shop_locations)
|
||||||
for item in shop_items:
|
for item in shop_items:
|
||||||
self.world.itempool.remove(item)
|
self.world.itempool.remove(item)
|
||||||
@@ -694,7 +704,7 @@ class OOTWorld(World):
|
|||||||
|
|
||||||
def generate_output(self, output_directory: str):
|
def generate_output(self, output_directory: str):
|
||||||
if self.hints != 'none':
|
if self.hints != 'none':
|
||||||
hint_data_available.wait()
|
self.hint_data_available.wait()
|
||||||
|
|
||||||
with i_o_limiter:
|
with i_o_limiter:
|
||||||
# Make ice traps appear as other random items
|
# Make ice traps appear as other random items
|
||||||
@@ -773,7 +783,8 @@ class OOTWorld(World):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
finally:
|
finally:
|
||||||
hint_data_available.set()
|
for autoworld in world.get_game_worlds("Ocarina of Time"):
|
||||||
|
autoworld.hint_data_available.set()
|
||||||
|
|
||||||
def modify_multidata(self, multidata: dict):
|
def modify_multidata(self, multidata: dict):
|
||||||
for item_name in self.remove_from_start_inventory:
|
for item_name in self.remove_from_start_inventory:
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,5 @@
|
|||||||
from BaseClasses import Item
|
from BaseClasses import Item
|
||||||
import typing
|
import typing
|
||||||
from random import randint
|
|
||||||
|
|
||||||
class RiskOfRainItem(Item):
|
class RiskOfRainItem(Item):
|
||||||
game: str = "Risk of Rain 2"
|
game: str = "Risk of Rain 2"
|
||||||
@@ -48,12 +47,12 @@ new_weights = {
|
|||||||
"Uncommon Item": 40,
|
"Uncommon Item": 40,
|
||||||
"Legendary Item": 10,
|
"Legendary Item": 10,
|
||||||
"Boss Item": 5,
|
"Boss Item": 5,
|
||||||
"Lunar Item": 15,
|
"Lunar Item": 10,
|
||||||
"Equipment": 25
|
"Equipment": 20
|
||||||
}
|
}
|
||||||
|
|
||||||
uncommon_weights = {
|
uncommon_weights = {
|
||||||
"Item Scrap, Green": 15,
|
"Item Scrap, Green": 45,
|
||||||
"Item Scrap, Red": 5,
|
"Item Scrap, Red": 5,
|
||||||
"Item Scrap, Yellow": 1,
|
"Item Scrap, Yellow": 1,
|
||||||
"Item Scrap, White": 30,
|
"Item Scrap, White": 30,
|
||||||
@@ -62,7 +61,7 @@ uncommon_weights = {
|
|||||||
"Legendary Item": 10,
|
"Legendary Item": 10,
|
||||||
"Boss Item": 5,
|
"Boss Item": 5,
|
||||||
"Lunar Item": 15,
|
"Lunar Item": 15,
|
||||||
"Equipment": 25
|
"Equipment": 20
|
||||||
}
|
}
|
||||||
|
|
||||||
legendary_weights = {
|
legendary_weights = {
|
||||||
@@ -75,7 +74,7 @@ legendary_weights = {
|
|||||||
"Legendary Item": 100,
|
"Legendary Item": 100,
|
||||||
"Boss Item": 5,
|
"Boss Item": 5,
|
||||||
"Lunar Item": 15,
|
"Lunar Item": 15,
|
||||||
"Equipment": 25
|
"Equipment": 20
|
||||||
}
|
}
|
||||||
|
|
||||||
lunartic_weights = {
|
lunartic_weights = {
|
||||||
@@ -96,8 +95,8 @@ no_scraps_weights = {
|
|||||||
"Item Scrap, Red": 0,
|
"Item Scrap, Red": 0,
|
||||||
"Item Scrap, Yellow": 0,
|
"Item Scrap, Yellow": 0,
|
||||||
"Item Scrap, White": 0,
|
"Item Scrap, White": 0,
|
||||||
"Common Item": 80,
|
"Common Item": 100,
|
||||||
"Uncommon Item": 30,
|
"Uncommon Item": 40,
|
||||||
"Legendary Item": 15,
|
"Legendary Item": 15,
|
||||||
"Boss Item": 5,
|
"Boss Item": 5,
|
||||||
"Lunar Item": 10,
|
"Lunar Item": 10,
|
||||||
@@ -117,11 +116,11 @@ even_weights = {
|
|||||||
"Equipment": 1
|
"Equipment": 1
|
||||||
}
|
}
|
||||||
|
|
||||||
scraps_only_weights = {
|
scraps_only = {
|
||||||
"Item Scrap, Green": 80,
|
"Item Scrap, Green": 70,
|
||||||
"Item Scrap, Red": 40,
|
|
||||||
"Item Scrap, Yellow": 10,
|
|
||||||
"Item Scrap, White": 100,
|
"Item Scrap, White": 100,
|
||||||
|
"Item Scrap, Red": 30,
|
||||||
|
"Item Scrap, Yellow": 5,
|
||||||
"Common Item": 0,
|
"Common Item": 0,
|
||||||
"Uncommon Item": 0,
|
"Uncommon Item": 0,
|
||||||
"Legendary Item": 0,
|
"Legendary Item": 0,
|
||||||
@@ -138,7 +137,7 @@ item_pool_weights: typing.Dict[int, typing.Dict[str, int]] = {
|
|||||||
4: lunartic_weights,
|
4: lunartic_weights,
|
||||||
6: no_scraps_weights,
|
6: no_scraps_weights,
|
||||||
7: even_weights,
|
7: even_weights,
|
||||||
8: scraps_only_weights
|
8: scraps_only
|
||||||
}
|
}
|
||||||
|
|
||||||
lookup_id_to_name: typing.Dict[int, str] = {id: name for name, id in item_table.items() if id}
|
lookup_id_to_name: typing.Dict[int, str] = {id: name for name, id in item_table.items() if id}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ base_location_table = {
|
|||||||
"Level Four": None,
|
"Level Four": None,
|
||||||
"Level Five": None
|
"Level Five": None
|
||||||
}
|
}
|
||||||
|
# 37006 - 37106
|
||||||
item_pickups = {
|
item_pickups = {
|
||||||
f"ItemPickup{i}": 37005+i for i in range(1, 51)
|
f"ItemPickup{i}": 37005+i for i in range(1, 101)
|
||||||
}
|
}
|
||||||
|
|
||||||
location_table = {**base_location_table, **item_pickups}
|
location_table = {**base_location_table, **item_pickups}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class TotalLocations(Range):
|
|||||||
"""Number of location checks which are added to the Risk of Rain playthrough."""
|
"""Number of location checks which are added to the Risk of Rain playthrough."""
|
||||||
displayname = "Total Locations"
|
displayname = "Total Locations"
|
||||||
range_start = 10
|
range_start = 10
|
||||||
range_end = 50
|
range_end = 100
|
||||||
default = 20
|
default = 20
|
||||||
|
|
||||||
|
|
||||||
@@ -123,15 +123,15 @@ class ItemPoolPresetToggle(DefaultOnToggle):
|
|||||||
displayname = "Item Weight Presets"
|
displayname = "Item Weight Presets"
|
||||||
|
|
||||||
class ItemWeights(Choice):
|
class ItemWeights(Choice):
|
||||||
"""Preset choices for determining the weights of the item pool.
|
"""Preset choices for determining the weights of the item pool.<br>
|
||||||
New is a test for a potential adjustment to the default weights.
|
New is a test for a potential adjustment to the default weights.<br>
|
||||||
Uncommon puts a large number of uncommon items in the pool.
|
Uncommon puts a large number of uncommon items in the pool.<br>
|
||||||
Legendary puts a large number of legendary items in the pool.
|
Legendary puts a large number of legendary items in the pool.<br>
|
||||||
lunartic makes everything a lunar item.
|
Lunartic makes everything a lunar item.<br>
|
||||||
chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being too easy.
|
Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being too easy.<br>
|
||||||
no_scraps removes all scrap items from the item pool.
|
No Scraps removes all scrap items from the item pool.<br>
|
||||||
even generates the item pool with every item having an even weight.
|
Even generates the item pool with every item having an even weight.<br>
|
||||||
scraps_only removes all non scrap items from the item pool."""
|
Scraps Only will be only scrap items in the item pool."""
|
||||||
displayname = "Item Weights"
|
displayname = "Item Weights"
|
||||||
option_default = 0
|
option_default = 0
|
||||||
option_new = 1
|
option_new = 1
|
||||||
|
|||||||
@@ -7,27 +7,39 @@ class RiskOfRainLogic(LogicMixin):
|
|||||||
def _ror_has_items(self, player: int, amount: int) -> bool:
|
def _ror_has_items(self, player: int, amount: int) -> bool:
|
||||||
count: int = self.item_count("Common Item", player) + self.item_count("Uncommon Item", player) + \
|
count: int = self.item_count("Common Item", player) + self.item_count("Uncommon Item", player) + \
|
||||||
self.item_count("Legendary Item", player) + self.item_count("Boss Item", player) + \
|
self.item_count("Legendary Item", player) + self.item_count("Boss Item", player) + \
|
||||||
self.item_count("Lunar Item", player) + self.item_count("Equipment", player)
|
self.item_count("Lunar Item", player) + self.item_count("Equipment", player) + \
|
||||||
|
self.item_count("Dio's Best Friend", player) + self.item_count("Item Scrap, White", player) + \
|
||||||
|
self.item_count("Item Scrap, Green", player) + self.item_count("Item Scrap, Red", player) + \
|
||||||
|
self.item_count("Item Scrap, Yellow", player)
|
||||||
return count >= amount
|
return count >= amount
|
||||||
|
|
||||||
|
|
||||||
def set_rules(world: MultiWorld, player: int):
|
def set_rules(world: MultiWorld, player: int):
|
||||||
total_checks = world.total_locations[player]
|
|
||||||
# divide by 5 since 5 levels (then commencement)
|
# divide by 5 since 5 levels (then commencement)
|
||||||
items_per_level = total_checks / 5
|
items_per_level = max(int(world.total_locations[player] / 5 / (world.item_pickup_step[player]+1)), 1)
|
||||||
leftover = total_checks % 5
|
|
||||||
|
|
||||||
set_rule(world.get_location("Level One", player),
|
# lock item pickup access based on level completion
|
||||||
lambda state: state._ror_has_items(player, items_per_level + leftover))
|
for i in range(1, items_per_level):
|
||||||
|
set_rule(world.get_location(f"ItemPickup{i}", player), lambda state: True)
|
||||||
|
for i in range(items_per_level, 2*items_per_level):
|
||||||
|
set_rule(world.get_location(f"ItemPickup{i}", player), lambda state: state.has("Beat Level One", player))
|
||||||
|
for i in range(2*items_per_level, 3*items_per_level):
|
||||||
|
set_rule(world.get_location(f"ItemPickup{i}", player), lambda state: state.has("Beat Level Two", player))
|
||||||
|
for i in range(3*items_per_level, 4*items_per_level):
|
||||||
|
set_rule(world.get_location(f"ItemPickup{i}", player), lambda state: state.has("Beat Level Three", player))
|
||||||
|
for i in range(4*items_per_level, world.total_locations[player] + 1):
|
||||||
|
set_rule(world.get_location(f"ItemPickup{i}", player), lambda state: state.has("Beat Level Four", player))
|
||||||
|
|
||||||
|
# require items to beat each stage
|
||||||
set_rule(world.get_location("Level Two", player),
|
set_rule(world.get_location("Level Two", player),
|
||||||
lambda state: state._ror_has_items(player, items_per_level) and state.has("Beat Level One", player))
|
lambda state: state.has("Beat Level One", player) and state._ror_has_items(player, items_per_level))
|
||||||
set_rule(world.get_location("Level Three", player),
|
set_rule(world.get_location("Level Three", player),
|
||||||
lambda state: state._ror_has_items(player, items_per_level) and state.has("Beat Level Two", player))
|
lambda state: state._ror_has_items(player, 2 * items_per_level) and state.has("Beat Level Two", player))
|
||||||
set_rule(world.get_location("Level Four", player),
|
set_rule(world.get_location("Level Four", player),
|
||||||
lambda state: state._ror_has_items(player, items_per_level) and state.has("Beat Level Three", player))
|
lambda state: state._ror_has_items(player, 3 * items_per_level) and state.has("Beat Level Three", player))
|
||||||
set_rule(world.get_location("Level Five", player),
|
set_rule(world.get_location("Level Five", player),
|
||||||
lambda state: state._ror_has_items(player, items_per_level) and state.has("Beat Level Four", player))
|
lambda state: state._ror_has_items(player, 4 * items_per_level) and state.has("Beat Level Four", player))
|
||||||
set_rule(world.get_location("Victory", player),
|
set_rule(world.get_location("Victory", player),
|
||||||
lambda state: state._ror_has_items(player, items_per_level) and state.has("Beat Level Five", player))
|
lambda state: state._ror_has_items(player, 5 * items_per_level) and state.has("Beat Level Five", player))
|
||||||
|
|
||||||
world.completion_condition[player] = lambda state: state.has("Victory", player)
|
world.completion_condition[player] = lambda state: state.has("Victory", player)
|
||||||
@@ -23,7 +23,7 @@ class RiskOfRainWorld(World):
|
|||||||
item_name_to_id = item_table
|
item_name_to_id = item_table
|
||||||
location_name_to_id = location_table
|
location_name_to_id = location_table
|
||||||
|
|
||||||
data_version = 1
|
data_version = 2
|
||||||
forced_auto_forfeit = True
|
forced_auto_forfeit = True
|
||||||
|
|
||||||
def generate_basic(self):
|
def generate_basic(self):
|
||||||
@@ -32,21 +32,21 @@ class RiskOfRainWorld(World):
|
|||||||
self.world.push_precollected(self.world.create_item("Dio's Best Friend", self.player))
|
self.world.push_precollected(self.world.create_item("Dio's Best Friend", self.player))
|
||||||
|
|
||||||
# if presets are enabled generate junk_pool from the selected preset
|
# if presets are enabled generate junk_pool from the selected preset
|
||||||
|
pool_option = self.world.item_weights[self.player].value
|
||||||
if self.world.item_pool_presets[self.player].value:
|
if self.world.item_pool_presets[self.player].value:
|
||||||
pool_option = self.world.item_weights[self.player].value
|
|
||||||
# generate chaos weights if the preset is chosen
|
# generate chaos weights if the preset is chosen
|
||||||
if pool_option == 5:
|
if pool_option == 5:
|
||||||
junk_pool = {
|
junk_pool = {
|
||||||
"Item Scrap, Green": self.world.random.randint(0, 100),
|
"Item Scrap, Green": self.world.random.randint(0, 80),
|
||||||
"Item Scrap, Red": self.world.random.randint(0, 100),
|
"Item Scrap, Red": self.world.random.randint(0, 45),
|
||||||
"Item Scrap, Yellow": self.world.random.randint(0, 100),
|
"Item Scrap, Yellow": self.world.random.randint(0, 30),
|
||||||
"Item Scrap, White": self.world.random.randint(0, 100),
|
"Item Scrap, White": self.world.random.randint(0, 100),
|
||||||
"Common Item": self.world.random.randint(0, 100),
|
"Common Item": self.world.random.randint(0, 100),
|
||||||
"Uncommon Item": self.world.random.randint(0, 70),
|
"Uncommon Item": self.world.random.randint(0, 70),
|
||||||
"Legendary Item": self.world.random.randint(0, 45),
|
"Legendary Item": self.world.random.randint(0, 30),
|
||||||
"Boss Item": self.world.random.randint(0, 30),
|
"Boss Item": self.world.random.randint(0, 20),
|
||||||
"Lunar Item": self.world.random.randint(0, 60),
|
"Lunar Item": self.world.random.randint(0, 60),
|
||||||
"Equipment": self.world.random.randint(0, 50)
|
"Equipment": self.world.random.randint(0, 40)
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
junk_pool = item_pool_weights[pool_option].copy()
|
junk_pool = item_pool_weights[pool_option].copy()
|
||||||
@@ -64,15 +64,17 @@ class RiskOfRainWorld(World):
|
|||||||
"Equipment": self.world.equipment[self.player].value
|
"Equipment": self.world.equipment[self.player].value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled
|
||||||
|
if not self.world.enable_lunar[self.player]:
|
||||||
|
if not pool_option == 4:
|
||||||
|
junk_pool.pop("Lunar Item")
|
||||||
|
|
||||||
# Generate item pool
|
# Generate item pool
|
||||||
itempool = []
|
itempool = []
|
||||||
|
|
||||||
# Add revive items for the player
|
# Add revive items for the player
|
||||||
itempool += ["Dio's Best Friend"] * self.world.total_revivals[self.player]
|
itempool += ["Dio's Best Friend"] * self.world.total_revivals[self.player]
|
||||||
|
|
||||||
if not self.world.enable_lunar[self.player]:
|
|
||||||
junk_pool.pop("Lunar Item")
|
|
||||||
|
|
||||||
# Fill remaining items with randomly generated junk
|
# Fill remaining items with randomly generated junk
|
||||||
itempool += self.world.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()),
|
itempool += self.world.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()),
|
||||||
k=self.world.total_locations[self.player] -
|
k=self.world.total_locations[self.player] -
|
||||||
@@ -103,13 +105,13 @@ class RiskOfRainWorld(World):
|
|||||||
item = RiskOfRainItem(name, True, item_id, self.player)
|
item = RiskOfRainItem(name, True, item_id, self.player)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
# generate locations based on player setting
|
||||||
def create_regions(world, player: int):
|
def create_regions(world, player: int):
|
||||||
world.regions += [
|
world.regions += [
|
||||||
create_region(world, player, 'Menu', None, ['Lobby']),
|
create_region(world, player, 'Menu', None, ['Lobby']),
|
||||||
create_region(world, player, 'Petrichor V',
|
create_region(world, player, 'Petrichor V',
|
||||||
[location for location in base_location_table] +
|
[location for location in base_location_table] +
|
||||||
[f"ItemPickup{i}" for i in range(1, 1+world.total_locations[player])])
|
[f"ItemPickup{i}" for i in range(1, 1 + world.total_locations[player])])
|
||||||
]
|
]
|
||||||
|
|
||||||
world.get_entrance("Lobby", player).connect(world.get_region("Petrichor V", player))
|
world.get_entrance("Lobby", player).connect(world.get_region("Petrichor V", player))
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ def has_cyclops_shield(state, player):
|
|||||||
# Fins are not used when using seaglide
|
# Fins are not used when using seaglide
|
||||||
#
|
#
|
||||||
def get_max_swim_depth(state, player):
|
def get_max_swim_depth(state, player):
|
||||||
#TODO, Make this a difficulty setting.
|
# TODO, Make this a difficulty setting.
|
||||||
# Only go up to 200m without any submarines for now.
|
# Only go up to 200m without any submarines for now.
|
||||||
return 200
|
return 200
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ def get_seamoth_max_depth(state, player):
|
|||||||
if has_seamoth(state, player):
|
if has_seamoth(state, player):
|
||||||
if has_seamoth_depth_module_mk3(state, player):
|
if has_seamoth_depth_module_mk3(state, player):
|
||||||
return 900
|
return 900
|
||||||
elif has_seamoth_depth_module_mk2(state, player): # Will never be the case, 3 is craftable
|
elif has_seamoth_depth_module_mk2(state, player): # Will never be the case, 3 is craftable
|
||||||
return 500
|
return 500
|
||||||
elif has_seamoth_depth_module_mk1(state, player):
|
elif has_seamoth_depth_module_mk1(state, player):
|
||||||
return 300
|
return 300
|
||||||
@@ -165,7 +165,7 @@ def get_cyclops_max_depth(state, player):
|
|||||||
if has_cyclops(state, player):
|
if has_cyclops(state, player):
|
||||||
if has_cyclops_depth_module_mk3(state, player):
|
if has_cyclops_depth_module_mk3(state, player):
|
||||||
return 1700
|
return 1700
|
||||||
elif has_cyclops_depth_module_mk2(state, player): # Will never be the case, 3 is craftable
|
elif has_cyclops_depth_module_mk2(state, player): # Will never be the case, 3 is craftable
|
||||||
return 1300
|
return 1300
|
||||||
elif has_cyclops_depth_module_mk1(state, player):
|
elif has_cyclops_depth_module_mk1(state, player):
|
||||||
return 900
|
return 900
|
||||||
@@ -188,12 +188,12 @@ def get_prawn_max_depth(state, player):
|
|||||||
|
|
||||||
|
|
||||||
def get_max_depth(state, player):
|
def get_max_depth(state, player):
|
||||||
#TODO, Difficulty option, we can add vehicle depth + swim depth
|
# TODO, Difficulty option, we can add vehicle depth + swim depth
|
||||||
# But at this point, we have to consider traver distance in caves, not
|
# But at this point, we have to consider traver distance in caves, not
|
||||||
# just depth
|
# just depth
|
||||||
return max(get_max_swim_depth(state, player), \
|
return max(get_max_swim_depth(state, player),
|
||||||
get_seamoth_max_depth(state, player), \
|
get_seamoth_max_depth(state, player),
|
||||||
get_cyclops_max_depth(state, player), \
|
get_cyclops_max_depth(state, player),
|
||||||
get_prawn_max_depth(state, player))
|
get_prawn_max_depth(state, player))
|
||||||
|
|
||||||
|
|
||||||
@@ -201,9 +201,9 @@ def can_access_location(state, player, loc):
|
|||||||
pos_x = loc.get("position").get("x")
|
pos_x = loc.get("position").get("x")
|
||||||
pos_y = loc.get("position").get("y")
|
pos_y = loc.get("position").get("y")
|
||||||
pos_z = loc.get("position").get("z")
|
pos_z = loc.get("position").get("z")
|
||||||
depth = -pos_y # y-up
|
depth = -pos_y # y-up
|
||||||
map_center_dist = math.sqrt(pos_x**2 + pos_z**2)
|
map_center_dist = math.sqrt(pos_x ** 2 + pos_z ** 2)
|
||||||
aurora_dist = math.sqrt((pos_x - 1038.0)**2 + (pos_y - -3.4)**2 + (pos_z - -163.1)**2)
|
aurora_dist = math.sqrt((pos_x - 1038.0) ** 2 + (pos_y - -3.4) ** 2 + (pos_z - -163.1) ** 2)
|
||||||
|
|
||||||
need_radiation_suit = aurora_dist < 950
|
need_radiation_suit = aurora_dist < 950
|
||||||
need_laser_cutter = loc.get("need_laser_cutter", False)
|
need_laser_cutter = loc.get("need_laser_cutter", False)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import typing
|
||||||
|
|
||||||
logger = logging.getLogger("Subnautica")
|
logger = logging.getLogger("Subnautica")
|
||||||
|
|
||||||
@@ -26,6 +27,8 @@ class SubnauticaWorld(World):
|
|||||||
location_name_to_id = locations_lookup_name_to_id
|
location_name_to_id = locations_lookup_name_to_id
|
||||||
options = options
|
options = options
|
||||||
|
|
||||||
|
data_version = 2
|
||||||
|
|
||||||
def generate_basic(self):
|
def generate_basic(self):
|
||||||
# Link regions
|
# Link regions
|
||||||
self.world.get_entrance('Lifepod 5', self.player).connect(self.world.get_region('Planet 4546B', self.player))
|
self.world.get_entrance('Lifepod 5', self.player).connect(self.world.get_region('Planet 4546B', self.player))
|
||||||
@@ -34,19 +37,23 @@ class SubnauticaWorld(World):
|
|||||||
pool = []
|
pool = []
|
||||||
neptune_launch_platform = None
|
neptune_launch_platform = None
|
||||||
extras = 0
|
extras = 0
|
||||||
|
valuable = self.world.item_pool[self.player] == "valuable"
|
||||||
for item in item_table:
|
for item in item_table:
|
||||||
for i in range(item["count"]):
|
for i in range(item["count"]):
|
||||||
subnautica_item = self.create_item(item["name"])
|
subnautica_item = self.create_item(item["name"])
|
||||||
if item["name"] == "Neptune Launch Platform":
|
if item["name"] == "Neptune Launch Platform":
|
||||||
neptune_launch_platform = subnautica_item
|
neptune_launch_platform = subnautica_item
|
||||||
elif not item["progression"] and self.world.item_pool[self.player] == "valuable":
|
elif valuable and not item["progression"]:
|
||||||
self.world.push_precollected(subnautica_item)
|
self.world.push_precollected(subnautica_item)
|
||||||
extras += 1
|
extras += 1
|
||||||
else:
|
else:
|
||||||
pool.append(subnautica_item)
|
pool.append(subnautica_item)
|
||||||
|
|
||||||
for item_name in self.world.random.choices(sorted(advancement_item_names - {"Neptune Launch Platform"}),
|
for item_name in self.world.random.choices(sorted(advancement_item_names - {"Neptune Launch Platform"}),
|
||||||
k=extras):
|
k=extras):
|
||||||
pool.append(self.create_item(item_name))
|
item = self.create_item(item_name)
|
||||||
|
item.advancement = False # as it's an extra, just fast-fill it somewhere
|
||||||
|
pool.append(item)
|
||||||
|
|
||||||
self.world.itempool += pool
|
self.world.itempool += pool
|
||||||
|
|
||||||
@@ -70,6 +77,9 @@ class SubnauticaWorld(World):
|
|||||||
item = lookup_name_to_item[name]
|
item = lookup_name_to_item[name]
|
||||||
return SubnauticaItem(name, item["progression"], item["id"], player=self.player)
|
return SubnauticaItem(name, item["progression"], item["id"], player=self.player)
|
||||||
|
|
||||||
|
def get_required_client_version(self) -> typing.Tuple[int, int, int]:
|
||||||
|
return max((0, 1, 9), super(SubnauticaWorld, self).get_required_client_version())
|
||||||
|
|
||||||
|
|
||||||
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||||
ret = Region(name, None, name, player)
|
ret = Region(name, None, name, player)
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
[
|
[
|
||||||
{ "id": 35000, "count": 1, "progression": true, "tech_type": "Compass", "name": "Subnautica Compass" },
|
{ "id": 35000, "count": 1, "progression": false, "tech_type": "Compass", "name": "Compass" },
|
||||||
{ "id": 35001, "count": 1, "progression": true, "tech_type": "PlasteelTank", "name": "Lightweight High Capacity Tank" },
|
{ "id": 35001, "count": 1, "progression": true, "tech_type": "PlasteelTank", "name": "Lightweight High Capacity Tank" },
|
||||||
{ "id": 35002, "count": 1, "progression": true, "tech_type": "BaseUpgradeConsole", "name": "Vehicle Upgrade Console" },
|
{ "id": 35002, "count": 1, "progression": true, "tech_type": "BaseUpgradeConsole", "name": "Vehicle Upgrade Console" },
|
||||||
{ "id": 35003, "count": 1, "progression": true, "tech_type": "UltraGlideFins", "name": "Ultra Glide Fins" },
|
{ "id": 35003, "count": 1, "progression": true, "tech_type": "UltraGlideFins", "name": "Ultra Glide Fins" },
|
||||||
{ "id": 35004, "count": 1, "progression": true, "tech_type": "CyclopsSonarModule", "name": "Cyclops Sonar Upgrade" },
|
{ "id": 35004, "count": 1, "progression": false, "tech_type": "CyclopsSonarModule", "name": "Cyclops Sonar Upgrade" },
|
||||||
{ "id": 35005, "count": 1, "progression": true, "tech_type": "ReinforcedDiveSuit", "name": "Reinforced Dive Suit" },
|
{ "id": 35005, "count": 1, "progression": false, "tech_type": "ReinforcedDiveSuit", "name": "Reinforced Dive Suit" },
|
||||||
{ "id": 35006, "count": 1, "progression": true, "tech_type": "CyclopsThermalReactorModule", "name": "Cyclops Thermal Reactor Module" },
|
{ "id": 35006, "count": 1, "progression": false, "tech_type": "CyclopsThermalReactorModule", "name": "Cyclops Thermal Reactor Module" },
|
||||||
{ "id": 35007, "count": 1, "progression": true, "tech_type": "Stillsuit", "name": "Stillsuit" },
|
{ "id": 35007, "count": 1, "progression": false, "tech_type": "Stillsuit", "name": "Stillsuit" },
|
||||||
{ "id": 35008, "count": 2, "progression": false, "tech_type": "BaseWaterParkFragment", "name": "Alien Containment Fragment" },
|
{ "id": 35008, "count": 2, "progression": false, "tech_type": "BaseWaterParkFragment", "name": "Alien Containment Fragment" },
|
||||||
{ "id": 35009, "count": 1, "progression": true, "tech_type": "CyclopsDecoy", "name": "Creature Decoy" },
|
{ "id": 35009, "count": 1, "progression": false, "tech_type": "CyclopsDecoy", "name": "Creature Decoy" },
|
||||||
{ "id": 35010, "count": 1, "progression": true, "tech_type": "CyclopsFireSuppressionModule", "name": "Cyclops Fire Suppression System" },
|
{ "id": 35010, "count": 1, "progression": false, "tech_type": "CyclopsFireSuppressionModule", "name": "Cyclops Fire Suppression System" },
|
||||||
{ "id": 35011, "count": 1, "progression": true, "tech_type": "SwimChargeFins", "name": "Swim Charge Fins" },
|
{ "id": 35011, "count": 1, "progression": false, "tech_type": "SwimChargeFins", "name": "Swim Charge Fins" },
|
||||||
{ "id": 35012, "count": 1, "progression": true, "tech_type": "RepulsionCannon", "name": "Repulsion Cannon" },
|
{ "id": 35012, "count": 1, "progression": false, "tech_type": "RepulsionCannon", "name": "Repulsion Cannon" },
|
||||||
{ "id": 35013, "count": 1, "progression": true, "tech_type": "CyclopsDecoyModule", "name": "Cyclops Decoy Tube Upgrade" },
|
{ "id": 35013, "count": 1, "progression": false, "tech_type": "CyclopsDecoyModule", "name": "Cyclops Decoy Tube Upgrade" },
|
||||||
{ "id": 35014, "count": 1, "progression": true, "tech_type": "CyclopsShieldModule", "name": "Cyclops Shield Generator" },
|
{ "id": 35014, "count": 1, "progression": true, "tech_type": "CyclopsShieldModule", "name": "Cyclops Shield Generator" },
|
||||||
{ "id": 35015, "count": 1, "progression": true, "tech_type": "CyclopsHullModule1", "name": "Cyclops Depth Module MK1" },
|
{ "id": 35015, "count": 1, "progression": true, "tech_type": "CyclopsHullModule1", "name": "Cyclops Depth Module MK1" },
|
||||||
{ "id": 35016, "count": 1, "progression": true, "tech_type": "CyclopsSeamothRepairModule", "name": "Cyclops Docking Bay Repair Module" },
|
{ "id": 35016, "count": 1, "progression": false, "tech_type": "CyclopsSeamothRepairModule", "name": "Cyclops Docking Bay Repair Module" },
|
||||||
{ "id": 35017, "count": 2, "progression": true, "tech_type": "BatteryChargerFragment", "name": "Battery Charger fragment" },
|
{ "id": 35017, "count": 2, "progression": false, "tech_type": "BatteryChargerFragment", "name": "Battery Charger fragment" },
|
||||||
{ "id": 35018, "count": 2, "progression": true, "tech_type": "BeaconFragment", "name": "Beacon Fragment" },
|
{ "id": 35018, "count": 2, "progression": false, "tech_type": "BeaconFragment", "name": "Beacon Fragment" },
|
||||||
{ "id": 35019, "count": 2, "progression": true, "tech_type": "BaseBioReactorFragment", "name": "Bioreactor Fragment" },
|
{ "id": 35019, "count": 2, "progression": false, "tech_type": "BaseBioReactorFragment", "name": "Bioreactor Fragment" },
|
||||||
{ "id": 35020, "count": 3, "progression": true, "tech_type": "CyclopsBridgeFragment", "name": "Cyclops Bridge Fragment" },
|
{ "id": 35020, "count": 3, "progression": true, "tech_type": "CyclopsBridgeFragment", "name": "Cyclops Bridge Fragment" },
|
||||||
{ "id": 35021, "count": 3, "progression": true, "tech_type": "CyclopsEngineFragment", "name": "Cyclops Engine Fragment" },
|
{ "id": 35021, "count": 3, "progression": true, "tech_type": "CyclopsEngineFragment", "name": "Cyclops Engine Fragment" },
|
||||||
{ "id": 35022, "count": 3, "progression": true, "tech_type": "CyclopsHullFragment", "name": "Cyclops Hull Fragment" },
|
{ "id": 35022, "count": 3, "progression": true, "tech_type": "CyclopsHullFragment", "name": "Cyclops Hull Fragment" },
|
||||||
{ "id": 35023, "count": 2, "progression": true, "tech_type": "GravSphereFragment", "name": "Grav Trap Fragment" },
|
{ "id": 35023, "count": 2, "progression": false, "tech_type": "GravSphereFragment", "name": "Grav Trap Fragment" },
|
||||||
{ "id": 35024, "count": 3, "progression": true, "tech_type": "LaserCutterFragment", "name": "Laser Cutter Fragment" },
|
{ "id": 35024, "count": 3, "progression": true, "tech_type": "LaserCutterFragment", "name": "Laser Cutter Fragment" },
|
||||||
{ "id": 35025, "count": 1, "progression": false, "tech_type": "TechlightFragment", "name": "Light Stick Fragment" },
|
{ "id": 35025, "count": 1, "progression": false, "tech_type": "TechlightFragment", "name": "Light Stick Fragment" },
|
||||||
{ "id": 35026, "count": 3, "progression": true, "tech_type": "ConstructorFragment", "name": "Mobile Vehicle Bay Fragment" },
|
{ "id": 35026, "count": 3, "progression": true, "tech_type": "ConstructorFragment", "name": "Mobile Vehicle Bay Fragment" },
|
||||||
{ "id": 35027, "count": 3, "progression": true, "tech_type": "WorkbenchFragment", "name": "Modification Station Fragment" },
|
{ "id": 35027, "count": 3, "progression": true, "tech_type": "WorkbenchFragment", "name": "Modification Station Fragment" },
|
||||||
{ "id": 35028, "count": 2, "progression": true, "tech_type": "MoonpoolFragment", "name": "Moonpool Fragment" },
|
{ "id": 35028, "count": 2, "progression": true, "tech_type": "MoonpoolFragment", "name": "Moonpool Fragment" },
|
||||||
{ "id": 35029, "count": 3, "progression": true, "tech_type": "BaseNuclearReactorFragment", "name": "Nuclear Reactor Fragment" },
|
{ "id": 35029, "count": 3, "progression": false, "tech_type": "BaseNuclearReactorFragment", "name": "Nuclear Reactor Fragment" },
|
||||||
{ "id": 35030, "count": 2, "progression": true, "tech_type": "PowerCellChargerFragment", "name": "Power Cell Charger Fragment" },
|
{ "id": 35030, "count": 2, "progression": false, "tech_type": "PowerCellChargerFragment", "name": "Power Cell Charger Fragment" },
|
||||||
{ "id": 35031, "count": 1, "progression": true, "tech_type": "PowerTransmitterFragment", "name": "Power Transmitter Fragment" },
|
{ "id": 35031, "count": 1, "progression": false, "tech_type": "PowerTransmitterFragment", "name": "Power Transmitter Fragment" },
|
||||||
{ "id": 35032, "count": 4, "progression": true, "tech_type": "ExosuitFragment", "name": "Prawn Suit Fragment" },
|
{ "id": 35032, "count": 4, "progression": true, "tech_type": "ExosuitFragment", "name": "Prawn Suit Fragment" },
|
||||||
{ "id": 35033, "count": 2, "progression": true, "tech_type": "ExosuitDrillArmFragment", "name": "Prawn Suit Drill Arm Fragment" },
|
{ "id": 35033, "count": 2, "progression": false, "tech_type": "ExosuitDrillArmFragment", "name": "Prawn Suit Drill Arm Fragment" },
|
||||||
{ "id": 35034, "count": 2, "progression": true, "tech_type": "ExosuitGrapplingArmFragment", "name": "Prawn Suit Grappling Arm Fragment" },
|
{ "id": 35034, "count": 2, "progression": false, "tech_type": "ExosuitGrapplingArmFragment", "name": "Prawn Suit Grappling Arm Fragment" },
|
||||||
{ "id": 35035, "count": 2, "progression": true, "tech_type": "ExosuitPropulsionArmFragment", "name": "Prawn Suit Propulsion Cannon Fragment" },
|
{ "id": 35035, "count": 2, "progression": false, "tech_type": "ExosuitPropulsionArmFragment", "name": "Prawn Suit Propulsion Cannon Fragment" },
|
||||||
{ "id": 35036, "count": 2, "progression": true, "tech_type": "ExosuitTorpedoArmFragment", "name": "Prawn Suit Torpedo Arm Fragment" },
|
{ "id": 35036, "count": 2, "progression": false, "tech_type": "ExosuitTorpedoArmFragment", "name": "Prawn Suit Torpedo Arm Fragment" },
|
||||||
{ "id": 35037, "count": 3, "progression": true, "tech_type": "BaseMapRoomFragment", "name": "Scanner Room Fragment" },
|
{ "id": 35037, "count": 3, "progression": false, "tech_type": "BaseMapRoomFragment", "name": "Scanner Room Fragment" },
|
||||||
{ "id": 35038, "count": 5, "progression": true, "tech_type": "SeamothFragment", "name": "Seamoth Fragment" },
|
{ "id": 35038, "count": 5, "progression": true, "tech_type": "SeamothFragment", "name": "Seamoth Fragment" },
|
||||||
{ "id": 35039, "count": 2, "progression": true, "tech_type": "StasisRifleFragment", "name": "Stasis Rifle Fragment" },
|
{ "id": 35039, "count": 2, "progression": false, "tech_type": "StasisRifleFragment", "name": "Stasis Rifle Fragment" },
|
||||||
{ "id": 35040, "count": 2, "progression": true, "tech_type": "ThermalPlantFragment", "name": "Thermal Plant Fragment" },
|
{ "id": 35040, "count": 2, "progression": false, "tech_type": "ThermalPlantFragment", "name": "Thermal Plant Fragment" },
|
||||||
{ "id": 35041, "count": 4, "progression": true, "tech_type": "SeaglideFragment", "name": "Seaglide Fragment" },
|
{ "id": 35041, "count": 4, "progression": true, "tech_type": "SeaglideFragment", "name": "Seaglide Fragment" },
|
||||||
{ "id": 35042, "count": 1, "progression": true, "tech_type": "RadiationSuit", "name": "Radiation Suit" },
|
{ "id": 35042, "count": 1, "progression": true, "tech_type": "RadiationSuit", "name": "Radiation Suit" },
|
||||||
{ "id": 35043, "count": 2, "progression": true, "tech_type": "PropulsionCannonFragment", "name": "Propulsion Cannon Fragment" },
|
{ "id": 35043, "count": 2, "progression": true, "tech_type": "PropulsionCannonFragment", "name": "Propulsion Cannon Fragment" },
|
||||||
{ "id": 35044, "count": 1, "progression": true, "tech_type": "RocketBase", "name": "Neptune Launch Platform" },
|
{ "id": 35044, "count": 1, "progression": true, "tech_type": "RocketBase", "name": "Neptune Launch Platform" },
|
||||||
{ "id": 35045, "count": 1, "progression": true, "tech_type": "PrecursorIonPowerCell", "name": "Ion Power Cell" },
|
{ "id": 35045, "count": 1, "progression": true, "tech_type": "PrecursorIonPowerCell", "name": "Ion Power Cell" },
|
||||||
{ "id": 35046, "count": 2, "progression": true, "tech_type": "FarmingTrayFragment", "name": "Exterior Growbed Fragment" },
|
{ "id": 35046, "count": 2, "progression": false, "tech_type": "FarmingTrayFragment", "name": "Exterior Growbed Fragment" },
|
||||||
{ "id": 35047, "count": 1, "progression": false, "tech_type": "PictureFrameFragment", "name": "Picture Frame" },
|
{ "id": 35047, "count": 1, "progression": false, "tech_type": "PictureFrameFragment", "name": "Picture Frame" },
|
||||||
{ "id": 35048, "count": 2, "progression": false, "tech_type": "BenchFragment", "name": "Bench Fragment" },
|
{ "id": 35048, "count": 2, "progression": false, "tech_type": "BenchFragment", "name": "Bench Fragment" },
|
||||||
{ "id": 35049, "count": 1, "progression": true, "tech_type": "PlanterPotFragment", "name": "Basic Plant Pot" },
|
{ "id": 35049, "count": 1, "progression": false, "tech_type": "PlanterPotFragment", "name": "Basic Plant Pot" },
|
||||||
{ "id": 35050, "count": 1, "progression": true, "tech_type": "PlanterBoxFragment", "name": "Interior Growbed" },
|
{ "id": 35050, "count": 1, "progression": false, "tech_type": "PlanterBoxFragment", "name": "Interior Growbed" },
|
||||||
{ "id": 35051, "count": 1, "progression": true, "tech_type": "PlanterShelfFragment", "name": "Plant Shelf" },
|
{ "id": 35051, "count": 1, "progression": false, "tech_type": "PlanterShelfFragment", "name": "Plant Shelf" },
|
||||||
{ "id": 35052, "count": 2, "progression": false, "tech_type": "BaseObservatoryFragment", "name": "Observatory Fragment" },
|
{ "id": 35052, "count": 2, "progression": false, "tech_type": "BaseObservatoryFragment", "name": "Observatory Fragment" },
|
||||||
{ "id": 35053, "count": 2, "progression": true, "tech_type": "BaseRoomFragment", "name": "Multipurpose Room Fragment" },
|
{ "id": 35053, "count": 2, "progression": false, "tech_type": "BaseRoomFragment", "name": "Multipurpose Room Fragment" },
|
||||||
{ "id": 35054, "count": 2, "progression": false, "tech_type": "BaseBulkheadFragment", "name": "Bulkhead Fragment" },
|
{ "id": 35054, "count": 2, "progression": false, "tech_type": "BaseBulkheadFragment", "name": "Bulkhead Fragment" },
|
||||||
{ "id": 35055, "count": 1, "progression": true, "tech_type": "Spotlight", "name": "Spotlight" },
|
{ "id": 35055, "count": 1, "progression": false, "tech_type": "Spotlight", "name": "Spotlight" },
|
||||||
{ "id": 35056, "count": 2, "progression": false, "tech_type": "StarshipDesk", "name": "Desk" },
|
{ "id": 35056, "count": 2, "progression": false, "tech_type": "StarshipDesk", "name": "Desk" },
|
||||||
{ "id": 35057, "count": 1, "progression": false, "tech_type": "StarshipChair", "name": "Swivel Chair" },
|
{ "id": 35057, "count": 1, "progression": false, "tech_type": "StarshipChair", "name": "Swivel Chair" },
|
||||||
{ "id": 35058, "count": 1, "progression": false, "tech_type": "StarshipChair2", "name": "Office Chair" },
|
{ "id": 35058, "count": 1, "progression": false, "tech_type": "StarshipChair2", "name": "Office Chair" },
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
{ "id": 35068, "count": 1, "progression": false, "tech_type": "VendingMachine", "name": "Vending Machine" },
|
{ "id": 35068, "count": 1, "progression": false, "tech_type": "VendingMachine", "name": "Vending Machine" },
|
||||||
{ "id": 35069, "count": 1, "progression": false, "tech_type": "SingleWallShelf", "name": "Single Wall Shelf" },
|
{ "id": 35069, "count": 1, "progression": false, "tech_type": "SingleWallShelf", "name": "Single Wall Shelf" },
|
||||||
{ "id": 35070, "count": 1, "progression": false, "tech_type": "WallShelves", "name": "Wall Shelves" },
|
{ "id": 35070, "count": 1, "progression": false, "tech_type": "WallShelves", "name": "Wall Shelves" },
|
||||||
{ "id": 35071, "count": 1, "progression": true, "tech_type": "PlanterPot2", "name": "Round Plant Pot" },
|
{ "id": 35071, "count": 1, "progression": false, "tech_type": "PlanterPot2", "name": "Round Plant Pot" },
|
||||||
{ "id": 35072, "count": 1, "progression": true, "tech_type": "PlanterPot3", "name": "Chic Plant Pot" },
|
{ "id": 35072, "count": 1, "progression": false, "tech_type": "PlanterPot3", "name": "Chic Plant Pot" },
|
||||||
{ "id": 35073, "count": 1, "progression": false, "tech_type": "LabTrashcan", "name": "Nuclear Waste Disposal" },
|
{ "id": 35073, "count": 1, "progression": false, "tech_type": "LabTrashcan", "name": "Nuclear Waste Disposal" },
|
||||||
{ "id": 35074, "count": 1, "progression": false, "tech_type": "BasePlanter", "name": "Wall Planter" },
|
{ "id": 35074, "count": 1, "progression": false, "tech_type": "BasePlanter", "name": "Wall Planter" },
|
||||||
{ "id": 35075, "count": 1, "progression": true, "tech_type": "PrecursorIonBattery", "name": "Ion Battery" },
|
{ "id": 35075, "count": 1, "progression": true, "tech_type": "PrecursorIonBattery", "name": "Ion Battery" },
|
||||||
@@ -79,5 +79,5 @@
|
|||||||
{ "id": 35077, "count": 1, "progression": true, "tech_type": "RocketStage1", "name": "Neptune Boosters" },
|
{ "id": 35077, "count": 1, "progression": true, "tech_type": "RocketStage1", "name": "Neptune Boosters" },
|
||||||
{ "id": 35078, "count": 1, "progression": true, "tech_type": "RocketStage2", "name": "Neptune Fuel Reserve" },
|
{ "id": 35078, "count": 1, "progression": true, "tech_type": "RocketStage2", "name": "Neptune Fuel Reserve" },
|
||||||
{ "id": 35079, "count": 1, "progression": true, "tech_type": "RocketStage3", "name": "Neptune Cockpit" },
|
{ "id": 35079, "count": 1, "progression": true, "tech_type": "RocketStage3", "name": "Neptune Cockpit" },
|
||||||
{ "id": 35080, "count": 1, "progression": true, "tech_type": "BaseFiltrationMachine", "name": "Water Filtration Machine" }
|
{ "id": 35080, "count": 1, "progression": false, "tech_type": "BaseFiltrationMachine", "name": "Water Filtration Machine" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
{ "id": 33010, "position": { "x": -1396.3, "y": -330.8, "z": 730.0},
|
{ "id": 33010, "position": { "x": -1396.3, "y": -330.8, "z": 730.0},
|
||||||
"need_laser_cutter": false, "can_slip_through": false,
|
"need_laser_cutter": false, "can_slip_through": false,
|
||||||
"name": "Dunes Wreck - PDA" },
|
"name": "Dunes North Wreck - PDA" },
|
||||||
|
|
||||||
{ "id": 33011, "position": { "x": -1409.8, "y": -332.4, "z": 706.9},
|
{ "id": 33011, "position": { "x": -1409.8, "y": -332.4, "z": 706.9},
|
||||||
"need_laser_cutter": true, "can_slip_through": false,
|
"need_laser_cutter": true, "can_slip_through": false,
|
||||||
@@ -380,7 +380,7 @@
|
|||||||
"name": "Aurora Drive Room - Upgrade Console" },
|
"name": "Aurora Drive Room - Upgrade Console" },
|
||||||
|
|
||||||
{ "id": 33095, "position": { "x": 991.6, "y": 3.2, "z": -31.0},
|
{ "id": 33095, "position": { "x": 991.6, "y": 3.2, "z": -31.0},
|
||||||
"need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true,
|
"need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true,
|
||||||
"name": "Aurora Prawn Suit Bay - Upgrade Console" },
|
"name": "Aurora Prawn Suit Bay - Upgrade Console" },
|
||||||
|
|
||||||
{ "id": 33096, "position": { "x": 952.1, "y": 41.2, "z": 113.9},
|
{ "id": 33096, "position": { "x": 952.1, "y": 41.2, "z": 113.9},
|
||||||
@@ -497,7 +497,7 @@
|
|||||||
|
|
||||||
{ "id": 33124, "position": { "x": -30.4, "y": -1220.3, "z": 111.8},
|
{ "id": 33124, "position": { "x": -30.4, "y": -1220.3, "z": 111.8},
|
||||||
"need_laser_cutter": false, "can_slip_through": false,
|
"need_laser_cutter": false, "can_slip_through": false,
|
||||||
"name": "Alien Thermal Plant - Yelow Alien Data Terminal" },
|
"name": "Alien Thermal Plant - Yellow Alien Data Terminal" },
|
||||||
|
|
||||||
{ "id": 33125, "position": { "x": 245.8, "y": -1430.6, "z": -311.5},
|
{ "id": 33125, "position": { "x": 245.8, "y": -1430.6, "z": -311.5},
|
||||||
"need_laser_cutter": false, "can_slip_through": false,
|
"need_laser_cutter": false, "can_slip_through": false,
|
||||||
|
|||||||
@@ -1,62 +1,63 @@
|
|||||||
from typing import Dict, Tuple, NamedTuple
|
from typing import Dict, Set, Tuple, NamedTuple
|
||||||
|
|
||||||
class ItemData(NamedTuple):
|
class ItemData(NamedTuple):
|
||||||
category: str
|
category: str
|
||||||
code: int
|
code: int
|
||||||
count: int = 1
|
count: int = 1
|
||||||
progression: bool = False
|
progression: bool = False
|
||||||
|
never_exclude: bool = False
|
||||||
|
|
||||||
# A lot of items arent normally dropped by the randomizer as they are mostly enemy drops, but they can be enabled if desired
|
# A lot of items arent normally dropped by the randomizer as they are mostly enemy drops, but they can be enabled if desired
|
||||||
item_table: Dict[str, ItemData] = {
|
item_table: Dict[str, ItemData] = {
|
||||||
'Eternal Crown': ItemData('Equipment', 1337000),
|
'Eternal Crown': ItemData('Equipment', 1337000, never_exclude=True),
|
||||||
#'Security Visor': ItemData('Equipment', 1337001),
|
'Security Visor': ItemData('Equipment', 1337001, 0),
|
||||||
#'Engineer Goggles': ItemData('Equipment', 1337002),
|
'Engineer Goggles': ItemData('Equipment', 1337002, 0),
|
||||||
#'Leather Helmet': ItemData('Equipment', 1337003),
|
'Leather Helmet': ItemData('Equipment', 1337003, 0),
|
||||||
#'Copper Helmet': ItemData('Equipment', 1337004),
|
'Copper Helmet': ItemData('Equipment', 1337004, 0),
|
||||||
'Pointy Hat': ItemData('Equipment', 1337005),
|
'Pointy Hat': ItemData('Equipment', 1337005),
|
||||||
#'Dragoon Helmet': ItemData('Equipment', 1337006),
|
'Dragoon Helmet': ItemData('Equipment', 1337006, 0),
|
||||||
'Buckle Hat': ItemData('Equipment', 1337007),
|
'Buckle Hat': ItemData('Equipment', 1337007),
|
||||||
#'Advisor Hat': ItemData('Equipment', 1337008),
|
'Advisor Hat': ItemData('Equipment', 1337008, 0),
|
||||||
'Librarian Hat': ItemData('Equipment', 1337009),
|
'Librarian Hat': ItemData('Equipment', 1337009),
|
||||||
#'Combat Helmet': ItemData('Equipment', 1337010),
|
'Combat Helmet': ItemData('Equipment', 1337010, 0),
|
||||||
'Captain\'s Cap': ItemData('Equipment', 1337011),
|
'Captain\'s Cap': ItemData('Equipment', 1337011),
|
||||||
'Lab Glasses': ItemData('Equipment', 1337012),
|
'Lab Glasses': ItemData('Equipment', 1337012),
|
||||||
'Empire Crown': ItemData('Equipment', 1337013),
|
'Empire Crown': ItemData('Equipment', 1337013),
|
||||||
'Viletian Crown': ItemData('Equipment', 1337014),
|
'Viletian Crown': ItemData('Equipment', 1337014),
|
||||||
#'Sunglasses': ItemData('Equipment', 1337015),
|
'Sunglasses': ItemData('Equipment', 1337015, 0),
|
||||||
'Old Coat': ItemData('Equipment', 1337016),
|
'Old Coat': ItemData('Equipment', 1337016),
|
||||||
#'Trendy Jacket': ItemData('Equipment', 1337017),
|
'Trendy Jacket': ItemData('Equipment', 1337017, 0),
|
||||||
#'Security Vest': ItemData('Equipment', 1337018),
|
'Security Vest': ItemData('Equipment', 1337018, 0),
|
||||||
#'Leather Jerkin': ItemData('Equipment', 1337019),
|
'Leather Jerkin': ItemData('Equipment', 1337019, 0),
|
||||||
#'Copper Breastplate': ItemData('Equipment', 1337020),
|
'Copper Breastplate': ItemData('Equipment', 1337020, 0),
|
||||||
'Traveler\'s Cloak': ItemData('Equipment', 1337021),
|
'Traveler\'s Cloak': ItemData('Equipment', 1337021),
|
||||||
#'Dragoon Armor': ItemData('Equipment', 1337022),
|
'Dragoon Armor': ItemData('Equipment', 1337022, 0),
|
||||||
'Midnight Cloak': ItemData('Equipment', 1337023),
|
'Midnight Cloak': ItemData('Equipment', 1337023),
|
||||||
#'Advisor Robe': ItemData('Equipment', 1337024),
|
'Advisor Robe': ItemData('Equipment', 1337024, 0),
|
||||||
'Librarian Robe': ItemData('Equipment', 1337025),
|
'Librarian Robe': ItemData('Equipment', 1337025),
|
||||||
#'Military Armor': ItemData('Equipment', 1337026),
|
'Military Armor': ItemData('Equipment', 1337026, 0),
|
||||||
'Captain\'s Uniform': ItemData('Equipment', 1337027),
|
'Captain\'s Uniform': ItemData('Equipment', 1337027),
|
||||||
'Lab Coat': ItemData('Equipment', 1337028),
|
'Lab Coat': ItemData('Equipment', 1337028),
|
||||||
'Empress Robe': ItemData('Equipment', 1337029),
|
'Empress Robe': ItemData('Equipment', 1337029),
|
||||||
'Princess Dress': ItemData('Equipment', 1337030),
|
'Princess Dress': ItemData('Equipment', 1337030),
|
||||||
'Eternal Coat': ItemData('Equipment', 1337031),
|
'Eternal Coat': ItemData('Equipment', 1337031, never_exclude=True),
|
||||||
#'Synthetic Plume': ItemData('Equipment', 1337032),
|
'Synthetic Plume': ItemData('Equipment', 1337032, 0),
|
||||||
#'Cheveur Plume': ItemData('Equipment', 1337033),
|
'Cheveur Plume': ItemData('Equipment', 1337033, 0),
|
||||||
'Metal Wristband': ItemData('Equipment', 1337034),
|
'Metal Wristband': ItemData('Equipment', 1337034),
|
||||||
#'Nymph Hairband': ItemData('Equipment', 1337035),
|
'Nymph Hairband': ItemData('Equipment', 1337035, 0),
|
||||||
#'Mother o\' Pearl': ItemData('Equipment', 1337036),
|
'Mother o\' Pearl': ItemData('Equipment', 1337036, 0),
|
||||||
'Bird Statue': ItemData('Equipment', 1337037),
|
'Bird Statue': ItemData('Equipment', 1337037, never_exclude=True),
|
||||||
#'Chaos Stole': ItemData('Equipment', 1337038),
|
'Chaos Stole': ItemData('Equipment', 1337038, 0),
|
||||||
'Pendulum': ItemData('Equipment', 1337039),
|
'Pendulum': ItemData('Equipment', 1337039, never_exclude=True),
|
||||||
#'Chaos Horn': ItemData('Equipment', 1337040),
|
'Chaos Horn': ItemData('Equipment', 1337040, 0),
|
||||||
'Filigree Clasp': ItemData('Equipment', 1337041),
|
'Filigree Clasp': ItemData('Equipment', 1337041),
|
||||||
#'Azure Stole': ItemData('Equipment', 1337042),
|
'Azure Stole': ItemData('Equipment', 1337042, 0),
|
||||||
'Ancient Coin': ItemData('Equipment', 1337043),
|
'Ancient Coin': ItemData('Equipment', 1337043),
|
||||||
#'Shiny Rock': ItemData('Equipment', 1337044),
|
'Shiny Rock': ItemData('Equipment', 1337044, 0),
|
||||||
'Galaxy Earrings': ItemData('Equipment', 1337045),
|
'Galaxy Earrings': ItemData('Equipment', 1337045, never_exclude=True),
|
||||||
'Selen\'s Bangle': ItemData('Equipment', 1337046),
|
'Selen\'s Bangle': ItemData('Equipment', 1337046, never_exclude=True),
|
||||||
'Glass Pumpkin': ItemData('Equipment', 1337047),
|
'Glass Pumpkin': ItemData('Equipment', 1337047, never_exclude=True),
|
||||||
'Gilded Egg': ItemData('Equipment', 1337048),
|
'Gilded Egg': ItemData('Equipment', 1337048, never_exclude=True),
|
||||||
'Meyef': ItemData('Familiar', 1337049),
|
'Meyef': ItemData('Familiar', 1337049),
|
||||||
'Griffin': ItemData('Familiar', 1337050),
|
'Griffin': ItemData('Familiar', 1337050),
|
||||||
'Merchant Crow': ItemData('Familiar', 1337051),
|
'Merchant Crow': ItemData('Familiar', 1337051),
|
||||||
@@ -76,45 +77,45 @@ item_table: Dict[str, ItemData] = {
|
|||||||
'Antidote': ItemData('UseItem', 1337065, 0),
|
'Antidote': ItemData('UseItem', 1337065, 0),
|
||||||
'Chaos Rose': ItemData('UseItem', 1337066, 0),
|
'Chaos Rose': ItemData('UseItem', 1337066, 0),
|
||||||
'Warp Shard': ItemData('UseItem', 1337067),
|
'Warp Shard': ItemData('UseItem', 1337067),
|
||||||
#'Dream Wisp': ItemData('UseItem', 1337068),
|
'Dream Wisp': ItemData('UseItem', 1337068, 0),
|
||||||
#'PlaceHolderItem1': ItemData('UseItem', 1337069),
|
'PlaceHolderItem1': ItemData('UseItem', 1337069, 0),
|
||||||
#'Lachiemi Sun': ItemData('UseItem', 1337070),
|
'Lachiemi Sun': ItemData('UseItem', 1337070, 0),
|
||||||
'Jerky': ItemData('UseItem', 1337071),
|
'Jerky': ItemData('UseItem', 1337071),
|
||||||
#'Biscuit': ItemData('UseItem', 1337072),
|
'Biscuit': ItemData('UseItem', 1337072, 0),
|
||||||
#'Fried Cheveur': ItemData('UseItem', 1337073),
|
'Fried Cheveur': ItemData('UseItem', 1337073, 0),
|
||||||
#'Sautéed Wyvern Tail': ItemData('UseItem', 1337074),
|
'Sautéed Wyvern Tail': ItemData('UseItem', 1337074, 0),
|
||||||
#'Unagi Roll': ItemData('UseItem', 1337075),
|
'Unagi Roll': ItemData('UseItem', 1337075, 0),
|
||||||
#'Cheveur au Vin': ItemData('UseItem', 1337076),
|
'Cheveur au Vin': ItemData('UseItem', 1337076, 0),
|
||||||
#'Royal Casserole': ItemData('UseItem', 1337077),
|
'Royal Casserole': ItemData('UseItem', 1337077, 0),
|
||||||
'Spaghetti': ItemData('UseItem', 1337078),
|
'Spaghetti': ItemData('UseItem', 1337078),
|
||||||
#'Plump Maggot': ItemData('UseItem', 1337079),
|
'Plump Maggot': ItemData('UseItem', 1337079, 0),
|
||||||
#'Orange Juice': ItemData('UseItem', 1337080),
|
'Orange Juice': ItemData('UseItem', 1337080, 0),
|
||||||
'Filigree Tea': ItemData('UseItem', 1337081),
|
'Filigree Tea': ItemData('UseItem', 1337081),
|
||||||
#'Empress Cake': ItemData('UseItem', 1337082),
|
'Empress Cake': ItemData('UseItem', 1337082, 0),
|
||||||
#'Rotten Tail': ItemData('UseItem', 1337083),
|
'Rotten Tail': ItemData('UseItem', 1337083, 0),
|
||||||
#'Alchemy Tools': ItemData('UseItem', 1337084),
|
'Alchemy Tools': ItemData('UseItem', 1337084, 0),
|
||||||
'Galaxy Stone': ItemData('UseItem', 1337085),
|
'Galaxy Stone': ItemData('UseItem', 1337085),
|
||||||
#1337086 Used interally
|
# 1337086 Used interally
|
||||||
#'Essence Crystal': ItemData('UseItem', 1337087),
|
'Essence Crystal': ItemData('UseItem', 1337087, 0),
|
||||||
#'Gold Ring': ItemData('UseItem', 1337088),
|
'Gold Ring': ItemData('UseItem', 1337088, 0),
|
||||||
#'Gold Necklace': ItemData('UseItem', 1337089),
|
'Gold Necklace': ItemData('UseItem', 1337089, 0),
|
||||||
'Herb': ItemData('UseItem', 1337090),
|
'Herb': ItemData('UseItem', 1337090),
|
||||||
#'Mushroom': ItemData('UseItem', 1337091),
|
'Mushroom': ItemData('UseItem', 1337091, 0),
|
||||||
#'Plasma Crystal': ItemData('UseItem', 1337092),
|
'Plasma Crystal': ItemData('UseItem', 1337092, 0),
|
||||||
'Plasma IV Bag': ItemData('UseItem', 1337093),
|
'Plasma IV Bag': ItemData('UseItem', 1337093),
|
||||||
#'Cheveur Drumstick': ItemData('UseItem', 1337094),
|
'Cheveur Drumstick': ItemData('UseItem', 1337094, 0),
|
||||||
#'Wyvern Tail': ItemData('UseItem', 1337095),
|
'Wyvern Tail': ItemData('UseItem', 1337095, 0),
|
||||||
#'Eel Meat': ItemData('UseItem', 1337096),
|
'Eel Meat': ItemData('UseItem', 1337096, 0),
|
||||||
#'Cheveux Breast': ItemData('UseItem', 1337097),
|
'Cheveux Breast': ItemData('UseItem', 1337097, 0),
|
||||||
'Food Synthesizer': ItemData('UseItem', 1337098),
|
'Food Synthesizer': ItemData('UseItem', 1337098),
|
||||||
#'Cheveux Feather': ItemData('UseItem', 1337099),
|
'Cheveux Feather': ItemData('UseItem', 1337099, 0),
|
||||||
#'Siren Ink': ItemData('UseItem', 1337100),
|
'Siren Ink': ItemData('UseItem', 1337100, 0),
|
||||||
#'Plasma Core': ItemData('UseItem', 1337101),
|
'Plasma Core': ItemData('UseItem', 1337101, 0),
|
||||||
#'Silver Ore': ItemData('UseItem', 1337102),
|
'Silver Ore': ItemData('UseItem', 1337102, 0),
|
||||||
#'Historical Documents': ItemData('UseItem', 1337103),
|
'Historical Documents': ItemData('UseItem', 1337103, 0),
|
||||||
#'MapReveal 0': ItemData('UseItem', 1337104),
|
'MapReveal 0': ItemData('UseItem', 1337104, 0),
|
||||||
#'MapReveal 1': ItemData('UseItem', 1337105),
|
'MapReveal 1': ItemData('UseItem', 1337105, 0),
|
||||||
#'MapReveal 2': ItemData('UseItem', 1337106),
|
'MapReveal 2': ItemData('UseItem', 1337106, 0),
|
||||||
'Timespinner Wheel': ItemData('Relic', 1337107, progression=True),
|
'Timespinner Wheel': ItemData('Relic', 1337107, progression=True),
|
||||||
'Timespinner Spindle': ItemData('Relic', 1337108, progression=True),
|
'Timespinner Spindle': ItemData('Relic', 1337108, progression=True),
|
||||||
'Timespinner Gear 1': ItemData('Relic', 1337109, progression=True),
|
'Timespinner Gear 1': ItemData('Relic', 1337109, progression=True),
|
||||||
@@ -134,7 +135,7 @@ item_table: Dict[str, ItemData] = {
|
|||||||
'Library Keycard V': ItemData('Relic', 1337123, progression=True),
|
'Library Keycard V': ItemData('Relic', 1337123, progression=True),
|
||||||
'Tablet': ItemData('Relic', 1337124, progression=True),
|
'Tablet': ItemData('Relic', 1337124, progression=True),
|
||||||
'Elevator Keycard': ItemData('Relic', 1337125, progression=True),
|
'Elevator Keycard': ItemData('Relic', 1337125, progression=True),
|
||||||
'Jewelry Box': ItemData('Relic', 1337126),
|
'Jewelry Box': ItemData('Relic', 1337126, never_exclude=True),
|
||||||
'Goddess Brooch': ItemData('Relic', 1337127),
|
'Goddess Brooch': ItemData('Relic', 1337127),
|
||||||
'Wyrm Brooch': ItemData('Relic', 1337128),
|
'Wyrm Brooch': ItemData('Relic', 1337128),
|
||||||
'Greed Brooch': ItemData('Relic', 1337129),
|
'Greed Brooch': ItemData('Relic', 1337129),
|
||||||
@@ -171,7 +172,7 @@ item_table: Dict[str, ItemData] = {
|
|||||||
'Bombardment': ItemData('Orb Spell', 1337160),
|
'Bombardment': ItemData('Orb Spell', 1337160),
|
||||||
'Corruption': ItemData('Orb Spell', 1337161),
|
'Corruption': ItemData('Orb Spell', 1337161),
|
||||||
'Lightwall': ItemData('Orb Spell', 1337162, progression=True),
|
'Lightwall': ItemData('Orb Spell', 1337162, progression=True),
|
||||||
'Bleak Ring': ItemData('Orb Passive', 1337163),
|
'Bleak Ring': ItemData('Orb Passive', 1337163, never_exclude=True),
|
||||||
'Scythe Ring': ItemData('Orb Passive', 1337164),
|
'Scythe Ring': ItemData('Orb Passive', 1337164),
|
||||||
'Pyro Ring': ItemData('Orb Passive', 1337165, progression=True),
|
'Pyro Ring': ItemData('Orb Passive', 1337165, progression=True),
|
||||||
'Royal Ring': ItemData('Orb Passive', 1337166, progression=True),
|
'Royal Ring': ItemData('Orb Passive', 1337166, progression=True),
|
||||||
@@ -180,12 +181,12 @@ item_table: Dict[str, ItemData] = {
|
|||||||
'Tailwind Ring': ItemData('Orb Passive', 1337169),
|
'Tailwind Ring': ItemData('Orb Passive', 1337169),
|
||||||
'Economizer Ring': ItemData('Orb Passive', 1337170),
|
'Economizer Ring': ItemData('Orb Passive', 1337170),
|
||||||
'Dusk Ring': ItemData('Orb Passive', 1337171),
|
'Dusk Ring': ItemData('Orb Passive', 1337171),
|
||||||
'Star of Lachiem': ItemData('Orb Passive', 1337172),
|
'Star of Lachiem': ItemData('Orb Passive', 1337172, never_exclude=True),
|
||||||
'Oculus Ring': ItemData('Orb Passive', 1337173, progression=True),
|
'Oculus Ring': ItemData('Orb Passive', 1337173, progression=True),
|
||||||
'Sanguine Ring': ItemData('Orb Passive', 1337174),
|
'Sanguine Ring': ItemData('Orb Passive', 1337174),
|
||||||
'Sun Ring': ItemData('Orb Passive', 1337175),
|
'Sun Ring': ItemData('Orb Passive', 1337175),
|
||||||
'Silence Ring': ItemData('Orb Passive', 1337176),
|
'Silence Ring': ItemData('Orb Passive', 1337176),
|
||||||
'Shadow Seal': ItemData('Orb Passive', 1337177),
|
'Shadow Seal': ItemData('Orb Passive', 1337177, never_exclude=True),
|
||||||
'Hope Ring': ItemData('Orb Passive', 1337178),
|
'Hope Ring': ItemData('Orb Passive', 1337178),
|
||||||
'Max HP': ItemData('Stat', 1337179, 12),
|
'Max HP': ItemData('Stat', 1337179, 12),
|
||||||
'Max Aura': ItemData('Stat', 1337180, 13),
|
'Max Aura': ItemData('Stat', 1337180, 13),
|
||||||
@@ -193,7 +194,7 @@ item_table: Dict[str, ItemData] = {
|
|||||||
'Max Sand': ItemData('Stat', 1337249, 14)
|
'Max Sand': ItemData('Stat', 1337249, 14)
|
||||||
}
|
}
|
||||||
|
|
||||||
starter_melee_weapons: Tuple[str] = (
|
starter_melee_weapons: Tuple[str, ...] = (
|
||||||
'Blue Orb',
|
'Blue Orb',
|
||||||
'Blade Orb',
|
'Blade Orb',
|
||||||
'Fire Orb',
|
'Fire Orb',
|
||||||
@@ -211,7 +212,7 @@ starter_melee_weapons: Tuple[str] = (
|
|||||||
'Radiant Orb'
|
'Radiant Orb'
|
||||||
)
|
)
|
||||||
|
|
||||||
starter_spells: Tuple[str] = (
|
starter_spells: Tuple[str, ...] = (
|
||||||
'Colossal Blade',
|
'Colossal Blade',
|
||||||
'Infernal Flames',
|
'Infernal Flames',
|
||||||
'Plasma Geyser',
|
'Plasma Geyser',
|
||||||
@@ -229,7 +230,7 @@ starter_spells: Tuple[str] = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# weighted
|
# weighted
|
||||||
starter_progression_items: Tuple[str] = (
|
starter_progression_items: Tuple[str, ...] = (
|
||||||
'Talaria Attachment',
|
'Talaria Attachment',
|
||||||
'Talaria Attachment',
|
'Talaria Attachment',
|
||||||
'Succubus Hairpin',
|
'Succubus Hairpin',
|
||||||
@@ -241,7 +242,7 @@ starter_progression_items: Tuple[str] = (
|
|||||||
'Lightwall'
|
'Lightwall'
|
||||||
)
|
)
|
||||||
|
|
||||||
filler_items: Tuple[str] = (
|
filler_items: Tuple[str, ...] = (
|
||||||
'Potion',
|
'Potion',
|
||||||
'Ether',
|
'Ether',
|
||||||
'Hi-Potion',
|
'Hi-Potion',
|
||||||
@@ -255,3 +256,11 @@ filler_items: Tuple[str] = (
|
|||||||
'Antidote',
|
'Antidote',
|
||||||
'Chaos Rose'
|
'Chaos Rose'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_item_names_per_category() -> Dict[str, Set[str]]:
|
||||||
|
categories: Dict[str, Set[str]] = {}
|
||||||
|
|
||||||
|
for name, data in item_table.items():
|
||||||
|
categories.setdefault(data.category, set()).add(name)
|
||||||
|
|
||||||
|
return categories
|
||||||
@@ -10,8 +10,8 @@ class LocationData(NamedTuple):
|
|||||||
code: Optional[int]
|
code: Optional[int]
|
||||||
rule: Callable = lambda state: True
|
rule: Callable = lambda state: True
|
||||||
|
|
||||||
def get_locations(world: MultiWorld, player: int):
|
def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]:
|
||||||
location_table: Tuple[LocationData] = (
|
location_table: Tuple[LocationData, ...] = (
|
||||||
# PresentItemLocations
|
# PresentItemLocations
|
||||||
LocationData('Tutorial', 'Yo Momma 1', 1337000),
|
LocationData('Tutorial', 'Yo Momma 1', 1337000),
|
||||||
LocationData('Tutorial', 'Yo Momma 2', 1337001),
|
LocationData('Tutorial', 'Yo Momma 2', 1337001),
|
||||||
@@ -194,10 +194,11 @@ def get_locations(world: MultiWorld, player: int):
|
|||||||
LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId)
|
LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId)
|
||||||
)
|
)
|
||||||
|
|
||||||
downloadable_items: Tuple[LocationData] = (
|
downloadable_items: Tuple[LocationData, ...] = (
|
||||||
# DownloadTerminals
|
# DownloadTerminals
|
||||||
LocationData('Libary', 'Library terminal 1', 1337157, lambda state: state.has('Tablet', player)),
|
LocationData('Libary', 'Library terminal 1', 1337157, lambda state: state.has('Tablet', player)),
|
||||||
LocationData('Libary', 'Library terminal 2', 1337156, lambda state: state.has('Tablet', player)),
|
LocationData('Libary', 'Library terminal 2', 1337156, lambda state: state.has('Tablet', player)),
|
||||||
|
# 1337158 Is Lost in time
|
||||||
LocationData('Libary', 'Library terminal 3', 1337159, lambda state: state.has('Tablet', player)),
|
LocationData('Libary', 'Library terminal 3', 1337159, lambda state: state.has('Tablet', player)),
|
||||||
LocationData('Libary', 'V terminal 1', 1337160, lambda state: state.has_all(['Tablet', 'Library Keycard V'], player)),
|
LocationData('Libary', 'V terminal 1', 1337160, lambda state: state.has_all(['Tablet', 'Library Keycard V'], player)),
|
||||||
LocationData('Libary', 'V terminal 2', 1337161, lambda state: state.has_all(['Tablet', 'Library Keycard V'], player)),
|
LocationData('Libary', 'V terminal 2', 1337161, lambda state: state.has_all(['Tablet', 'Library Keycard V'], player)),
|
||||||
@@ -217,7 +218,8 @@ def get_locations(world: MultiWorld, player: int):
|
|||||||
else:
|
else:
|
||||||
return location_table
|
return location_table
|
||||||
|
|
||||||
starter_progression_locations: Tuple[str] = (
|
|
||||||
|
starter_progression_locations: Tuple[str, ...] = (
|
||||||
'Starter chest 2',
|
'Starter chest 2',
|
||||||
'Starter chest 3',
|
'Starter chest 3',
|
||||||
'Starter chest 1',
|
'Starter chest 1',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class DownloadableItems(Toggle):
|
|||||||
display_name = "Downloadable items"
|
display_name = "Downloadable items"
|
||||||
|
|
||||||
class FacebookMode(Toggle):
|
class FacebookMode(Toggle):
|
||||||
"With the tablet you will be able to download items at terminals"
|
"Requires Oculus Rift(ng) to spot the weakspots in walls and floors"
|
||||||
display_name = "Facebook mode"
|
display_name = "Facebook mode"
|
||||||
|
|
||||||
class StartWithMeyef(Toggle):
|
class StartWithMeyef(Toggle):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from BaseClasses import MultiWorld
|
|||||||
from .Options import is_option_enabled
|
from .Options import is_option_enabled
|
||||||
|
|
||||||
def get_pyramid_keys_unlock(world: MultiWorld, player: int) -> str:
|
def get_pyramid_keys_unlock(world: MultiWorld, player: int) -> str:
|
||||||
present_teleportation_gates: Tuple[str] = (
|
present_teleportation_gates: Tuple[str, ...] = (
|
||||||
"GateKittyBoss",
|
"GateKittyBoss",
|
||||||
"GateLeftLibrary",
|
"GateLeftLibrary",
|
||||||
"GateMilitairyGate",
|
"GateMilitairyGate",
|
||||||
@@ -12,7 +12,7 @@ def get_pyramid_keys_unlock(world: MultiWorld, player: int) -> str:
|
|||||||
"GateLakeDesolation"
|
"GateLakeDesolation"
|
||||||
)
|
)
|
||||||
|
|
||||||
past_teleportation_gates: Tuple[str] = (
|
past_teleportation_gates: Tuple[str, ...] = (
|
||||||
"GateLakeSirineRight",
|
"GateLakeSirineRight",
|
||||||
"GateAccessToPast",
|
"GateAccessToPast",
|
||||||
"GateCastleRamparts",
|
"GateCastleRamparts",
|
||||||
|
|||||||
@@ -3,51 +3,51 @@ from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType
|
|||||||
from .Options import is_option_enabled
|
from .Options import is_option_enabled
|
||||||
from .Locations import LocationData
|
from .Locations import LocationData
|
||||||
|
|
||||||
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData], pyramid_keys_unlock: str):
|
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location], pyramid_keys_unlock: str):
|
||||||
locations_per_region = get_locations_per_region(locations)
|
locations_per_region = get_locations_per_region(locations)
|
||||||
|
|
||||||
world.regions += [
|
world.regions += [
|
||||||
create_region(world, player, locations_per_region, 'Menu'),
|
create_region(world, player, locations_per_region, location_cache, 'Menu'),
|
||||||
create_region(world, player, locations_per_region, 'Tutorial'),
|
create_region(world, player, locations_per_region, location_cache, 'Tutorial'),
|
||||||
create_region(world, player, locations_per_region, 'Lake desolation'),
|
create_region(world, player, locations_per_region, location_cache, 'Lake desolation'),
|
||||||
create_region(world, player, locations_per_region, 'Upper lake desolation'),
|
create_region(world, player, locations_per_region, location_cache, 'Upper lake desolation'),
|
||||||
create_region(world, player, locations_per_region, 'Lower lake desolation'),
|
create_region(world, player, locations_per_region, location_cache, 'Lower lake desolation'),
|
||||||
create_region(world, player, locations_per_region, 'Libary'),
|
create_region(world, player, locations_per_region, location_cache, 'Libary'),
|
||||||
create_region(world, player, locations_per_region, 'Libary top'),
|
create_region(world, player, locations_per_region, location_cache, 'Libary top'),
|
||||||
create_region(world, player, locations_per_region, 'Varndagroth tower left'),
|
create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower left'),
|
||||||
create_region(world, player, locations_per_region, 'Varndagroth tower right (upper)'),
|
create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (upper)'),
|
||||||
create_region(world, player, locations_per_region, 'Varndagroth tower right (lower)'),
|
create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (lower)'),
|
||||||
create_region(world, player, locations_per_region, 'Varndagroth tower right (elevator)'),
|
create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (elevator)'),
|
||||||
create_region(world, player, locations_per_region, 'Sealed Caves (Sirens)'),
|
create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (Sirens)'),
|
||||||
create_region(world, player, locations_per_region, 'Militairy Fortress'),
|
create_region(world, player, locations_per_region, location_cache, 'Militairy Fortress'),
|
||||||
create_region(world, player, locations_per_region, 'The lab'),
|
create_region(world, player, locations_per_region, location_cache, 'The lab'),
|
||||||
create_region(world, player, locations_per_region, 'The lab (power off)'),
|
create_region(world, player, locations_per_region, location_cache, 'The lab (power off)'),
|
||||||
create_region(world, player, locations_per_region, 'The lab (upper)'),
|
create_region(world, player, locations_per_region, location_cache, 'The lab (upper)'),
|
||||||
create_region(world, player, locations_per_region, 'Emperors tower'),
|
create_region(world, player, locations_per_region, location_cache, 'Emperors tower'),
|
||||||
create_region(world, player, locations_per_region, 'Skeleton Shaft'),
|
create_region(world, player, locations_per_region, location_cache, 'Skeleton Shaft'),
|
||||||
create_region(world, player, locations_per_region, 'Sealed Caves (upper)'),
|
create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (upper)'),
|
||||||
create_region(world, player, locations_per_region, 'Sealed Caves (Xarion)'),
|
create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (Xarion)'),
|
||||||
create_region(world, player, locations_per_region, 'Refugee Camp'),
|
create_region(world, player, locations_per_region, location_cache, 'Refugee Camp'),
|
||||||
create_region(world, player, locations_per_region, 'Forest'),
|
create_region(world, player, locations_per_region, location_cache, 'Forest'),
|
||||||
create_region(world, player, locations_per_region, 'Left Side forest Caves'),
|
create_region(world, player, locations_per_region, location_cache, 'Left Side forest Caves'),
|
||||||
create_region(world, player, locations_per_region, 'Upper Lake Sirine'),
|
create_region(world, player, locations_per_region, location_cache, 'Upper Lake Sirine'),
|
||||||
create_region(world, player, locations_per_region, 'Lower Lake Sirine'),
|
create_region(world, player, locations_per_region, location_cache, 'Lower Lake Sirine'),
|
||||||
create_region(world, player, locations_per_region, 'Caves of Banishment (upper)'),
|
create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (upper)'),
|
||||||
create_region(world, player, locations_per_region, 'Caves of Banishment (Maw)'),
|
create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (Maw)'),
|
||||||
create_region(world, player, locations_per_region, 'Caves of Banishment (Sirens)'),
|
create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (Sirens)'),
|
||||||
create_region(world, player, locations_per_region, 'Caste Ramparts'),
|
create_region(world, player, locations_per_region, location_cache, 'Caste Ramparts'),
|
||||||
create_region(world, player, locations_per_region, 'Caste Keep'),
|
create_region(world, player, locations_per_region, location_cache, 'Caste Keep'),
|
||||||
create_region(world, player, locations_per_region, 'Royal towers (lower)'),
|
create_region(world, player, locations_per_region, location_cache, 'Royal towers (lower)'),
|
||||||
create_region(world, player, locations_per_region, 'Royal towers'),
|
create_region(world, player, locations_per_region, location_cache, 'Royal towers'),
|
||||||
create_region(world, player, locations_per_region, 'Royal towers (upper)'),
|
create_region(world, player, locations_per_region, location_cache, 'Royal towers (upper)'),
|
||||||
create_region(world, player, locations_per_region, 'Ancient Pyramid (left)'),
|
create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (left)'),
|
||||||
create_region(world, player, locations_per_region, 'Ancient Pyramid (right)'),
|
create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (right)'),
|
||||||
create_region(world, player, locations_per_region, 'Space time continuum')
|
create_region(world, player, locations_per_region, location_cache, 'Space time continuum')
|
||||||
]
|
]
|
||||||
|
|
||||||
connectStartingRegion(world, player)
|
connectStartingRegion(world, player)
|
||||||
|
|
||||||
names = {}
|
names: Dict[str, int] = {}
|
||||||
|
|
||||||
connect(world, player, names, 'Lake desolation', 'Lower lake desolation', lambda state: state._timespinner_has_timestop(world, player or state.has('Talaria Attachment', player)))
|
connect(world, player, names, 'Lake desolation', 'Lower lake desolation', lambda state: state._timespinner_has_timestop(world, player or state.has('Talaria Attachment', player)))
|
||||||
connect(world, player, names, 'Lake desolation', 'Upper lake desolation', lambda state: state._timespinner_has_fire(world, player) and state.can_reach('Upper Lake Sirine', 'Region', player))
|
connect(world, player, names, 'Lake desolation', 'Upper lake desolation', lambda state: state._timespinner_has_fire(world, player) and state.can_reach('Upper Lake Sirine', 'Region', player))
|
||||||
@@ -60,7 +60,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
|||||||
connect(world, player, names, 'Lower lake desolation', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
connect(world, player, names, 'Lower lake desolation', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
||||||
connect(world, player, names, 'Libary', 'Lower lake desolation')
|
connect(world, player, names, 'Libary', 'Lower lake desolation')
|
||||||
connect(world, player, names, 'Libary', 'Libary top', lambda state: state._timespinner_has_doublejump(world, player) or state.has('Talaria Attachment', player))
|
connect(world, player, names, 'Libary', 'Libary top', lambda state: state._timespinner_has_doublejump(world, player) or state.has('Talaria Attachment', player))
|
||||||
connect(world, player, names, 'Libary', 'Varndagroth tower left', lambda state: state._timespinner_has_keycard_C(world, player))
|
connect(world, player, names, 'Libary', 'Varndagroth tower left', lambda state: state._timespinner_has_keycard_D(world, player))
|
||||||
connect(world, player, names, 'Libary', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
connect(world, player, names, 'Libary', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
||||||
connect(world, player, names, 'Libary top', 'Libary')
|
connect(world, player, names, 'Libary top', 'Libary')
|
||||||
connect(world, player, names, 'Varndagroth tower left', 'Libary')
|
connect(world, player, names, 'Varndagroth tower left', 'Libary')
|
||||||
@@ -98,7 +98,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
|||||||
connect(world, player, names, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', lambda state: state._timespinner_has_forwarddash_doublejump(world, player))
|
connect(world, player, names, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', lambda state: state._timespinner_has_forwarddash_doublejump(world, player))
|
||||||
connect(world, player, names, 'Sealed Caves (Xarion)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
connect(world, player, names, 'Sealed Caves (Xarion)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
||||||
connect(world, player, names, 'Refugee Camp', 'Forest')
|
connect(world, player, names, 'Refugee Camp', 'Forest')
|
||||||
connect(world, player, names, 'Refugee Camp', 'Libary', lambda state: is_option_enabled(world, player, "Inverted"))
|
connect(world, player, names, 'Refugee Camp', 'Libary', lambda state: not is_option_enabled(world, player, "Inverted"))
|
||||||
connect(world, player, names, 'Refugee Camp', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
connect(world, player, names, 'Refugee Camp', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
||||||
connect(world, player, names, 'Forest', 'Refugee Camp')
|
connect(world, player, names, 'Forest', 'Refugee Camp')
|
||||||
connect(world, player, names, 'Forest', 'Left Side forest Caves', lambda state: state.has('Talaria Attachment', player) or state._timespinner_has_timestop(world, player))
|
connect(world, player, names, 'Forest', 'Left Side forest Caves', lambda state: state.has('Talaria Attachment', player) or state._timespinner_has_timestop(world, player))
|
||||||
@@ -149,27 +149,32 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
|||||||
connect(world, player, names, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: pyramid_keys_unlock == "GateMaw")
|
connect(world, player, names, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: pyramid_keys_unlock == "GateMaw")
|
||||||
connect(world, player, names, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: pyramid_keys_unlock == "GateCavesOfBanishment")
|
connect(world, player, names, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: pyramid_keys_unlock == "GateCavesOfBanishment")
|
||||||
|
|
||||||
def create_location(player: int, name: str, id: Optional[int], region: Region, rule: Callable) -> Location:
|
|
||||||
location = Location(player, name, id, region)
|
def create_location(player: int, location_data: LocationData, region: Region, location_cache: List[Location]) -> Location:
|
||||||
location.access_rule = rule
|
location = Location(player, location_data.name, location_data.code, region)
|
||||||
|
location.access_rule = location_data.rule
|
||||||
|
|
||||||
if id is None:
|
if id is None:
|
||||||
location.event = True
|
location.event = True
|
||||||
location.locked = True
|
location.locked = True
|
||||||
|
|
||||||
|
location_cache.append(location)
|
||||||
|
|
||||||
return location
|
return location
|
||||||
|
|
||||||
def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]], name: str) -> Region:
|
|
||||||
|
def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]], location_cache: List[Location], name: str) -> Region:
|
||||||
region = Region(name, RegionType.Generic, name, player)
|
region = Region(name, RegionType.Generic, name, player)
|
||||||
region.world = world
|
region.world = world
|
||||||
|
|
||||||
if name in locations_per_region:
|
if name in locations_per_region:
|
||||||
for location_data in locations_per_region[name]:
|
for location_data in locations_per_region[name]:
|
||||||
location = create_location(player, location_data.name, location_data.code, region, location_data.rule)
|
location = create_location(player, location_data, region, location_cache)
|
||||||
region.locations.append(location)
|
region.locations.append(location)
|
||||||
|
|
||||||
return region
|
return region
|
||||||
|
|
||||||
|
|
||||||
def connectStartingRegion(world: MultiWorld, player: int):
|
def connectStartingRegion(world: MultiWorld, player: int):
|
||||||
menu = world.get_region('Menu', player)
|
menu = world.get_region('Menu', player)
|
||||||
tutorial = world.get_region('Tutorial', player)
|
tutorial = world.get_region('Tutorial', player)
|
||||||
@@ -192,7 +197,8 @@ def connectStartingRegion(world: MultiWorld, player: int):
|
|||||||
teleport_back_to_start.connect(starting_region)
|
teleport_back_to_start.connect(starting_region)
|
||||||
space_time_continuum.exits.append(teleport_back_to_start)
|
space_time_continuum.exits.append(teleport_back_to_start)
|
||||||
|
|
||||||
def connect(world: MultiWorld, player: int, used_names : Dict[str, int], source: str, target: str, rule: Optional[Callable] = None):
|
|
||||||
|
def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: str, target: str, rule: Optional[Callable] = None):
|
||||||
sourceRegion = world.get_region(source, player)
|
sourceRegion = world.get_region(source, player)
|
||||||
targetRegion = world.get_region(target, player)
|
targetRegion = world.get_region(target, player)
|
||||||
|
|
||||||
@@ -211,10 +217,11 @@ def connect(world: MultiWorld, player: int, used_names : Dict[str, int], source:
|
|||||||
sourceRegion.exits.append(connection)
|
sourceRegion.exits.append(connection)
|
||||||
connection.connect(targetRegion)
|
connection.connect(targetRegion)
|
||||||
|
|
||||||
def get_locations_per_region(locations: Tuple[LocationData]) -> Dict[str, List[LocationData]]:
|
|
||||||
|
def get_locations_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]:
|
||||||
per_region: Dict[str, List[LocationData]] = {}
|
per_region: Dict[str, List[LocationData]] = {}
|
||||||
|
|
||||||
for location in locations:
|
for location in locations:
|
||||||
per_region[location.region] = [ location ] if location.region not in per_region else per_region[location.region] + [ location ]
|
per_region.setdefault(location.region, []).append(location)
|
||||||
|
|
||||||
return per_region
|
return per_region
|
||||||
@@ -1,96 +1,106 @@
|
|||||||
from typing import Dict, List
|
from typing import Dict, List, Set
|
||||||
from BaseClasses import Item, MultiWorld
|
from BaseClasses import Item, MultiWorld, Location
|
||||||
from ..AutoWorld import World
|
from ..AutoWorld import World
|
||||||
from .LogicMixin import TimespinnerLogic
|
from .LogicMixin import TimespinnerLogic
|
||||||
from .Items import item_table, starter_melee_weapons, starter_spells, starter_progression_items, filler_items
|
from .Items import get_item_names_per_category, item_table, starter_melee_weapons, starter_spells, starter_progression_items, filler_items
|
||||||
from .Locations import get_locations, starter_progression_locations, EventId
|
from .Locations import get_locations, starter_progression_locations, EventId
|
||||||
from .Regions import create_regions
|
from .Regions import create_regions
|
||||||
from .Options import is_option_enabled, timespinner_options
|
from .Options import is_option_enabled, timespinner_options
|
||||||
from .PyramidKeys import get_pyramid_keys_unlock
|
from .PyramidKeys import get_pyramid_keys_unlock
|
||||||
|
|
||||||
class TimespinnerWorld(World):
|
class TimespinnerWorld(World):
|
||||||
|
"""
|
||||||
|
Timespinner is a beautiful metroidvania inspired by classic 90s action-platformers.
|
||||||
|
Travel back in time to change fate itself. Join timekeeper Lunais on her quest for revenge against the empire that killed her family.
|
||||||
|
"""
|
||||||
|
|
||||||
options = timespinner_options
|
options = timespinner_options
|
||||||
game = "Timespinner"
|
game = "Timespinner"
|
||||||
topology_present = True
|
topology_present = True
|
||||||
data_version = 1
|
remote_items = False
|
||||||
hidden = True
|
data_version = 2
|
||||||
|
|
||||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||||
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
|
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
|
||||||
|
item_name_groups = get_item_names_per_category()
|
||||||
|
|
||||||
locked_locations: Dict[int, List[str]] = {}
|
locked_locations: Dict[int, List[str]] = {}
|
||||||
pyramid_keys_unlock: Dict[int, str] = {}
|
pyramid_keys_unlock: Dict[int, str] = {}
|
||||||
|
location_cache: Dict[int, List[Location]] = {}
|
||||||
|
|
||||||
def generate_early(self):
|
def generate_early(self):
|
||||||
self.locked_locations[self.player] = []
|
self.locked_locations[self.player] = []
|
||||||
|
self.location_cache[self.player] = []
|
||||||
self.pyramid_keys_unlock[self.player] = get_pyramid_keys_unlock(self.world, self.player)
|
self.pyramid_keys_unlock[self.player] = get_pyramid_keys_unlock(self.world, self.player)
|
||||||
|
|
||||||
self.item_name_groups = get_item_name_groups()
|
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
create_regions(self.world, self.player, get_locations(self.world, self.player), self.pyramid_keys_unlock[self.player])
|
create_regions(self.world, self.player, get_locations(self.world, self.player),
|
||||||
|
self.location_cache[self.player], self.pyramid_keys_unlock[self.player])
|
||||||
|
|
||||||
|
|
||||||
def create_item(self, name: str) -> Item:
|
def create_item(self, name: str) -> Item:
|
||||||
return create_item(name, self.player)
|
return create_item_with_correct_settings(self.world, self.player, name)
|
||||||
|
|
||||||
|
|
||||||
def set_rules(self):
|
def set_rules(self):
|
||||||
setup_events(self.world, self.player, self.locked_locations[self.player])
|
setup_events(self.world, self.player, self.locked_locations[self.player], self.location_cache[self.player])
|
||||||
|
|
||||||
self.world.completion_condition[self.player] = lambda state: state.has('Killed Nightmare', self.player)
|
self.world.completion_condition[self.player] = lambda state: state.has('Killed Nightmare', self.player)
|
||||||
|
|
||||||
|
|
||||||
def generate_basic(self):
|
def generate_basic(self):
|
||||||
excluded_items = get_excluded_items_based_on_options(self.world, self.player)
|
excluded_items = get_excluded_items_based_on_options(self.world, self.player)
|
||||||
|
|
||||||
assign_starter_items(self.world, self.player, excluded_items, self.locked_locations[self.player])
|
assign_starter_items(self.world, self.player, excluded_items, self.locked_locations[self.player])
|
||||||
|
|
||||||
if not is_option_enabled(self.world, self.player, "QuickSeed") or not is_option_enabled(self.world, self.player, "Inverted"):
|
if not is_option_enabled(self.world, self.player, "QuickSeed") and not is_option_enabled(self.world, self.player, "Inverted"):
|
||||||
place_first_progression_item(self.world, self.player, excluded_items, self.locked_locations[self.player])
|
place_first_progression_item(self.world, self.player, excluded_items, self.locked_locations[self.player])
|
||||||
|
|
||||||
pool = get_item_pool(self.world, self.player, excluded_items)
|
pool = get_item_pool(self.world, self.player, excluded_items)
|
||||||
|
|
||||||
fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations[self.player], pool)
|
fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations[self.player], self.location_cache[self.player], pool)
|
||||||
|
|
||||||
self.world.itempool += pool
|
self.world.itempool += pool
|
||||||
|
|
||||||
def fill_slot_data(self) -> Dict:
|
|
||||||
slot_data = {}
|
def fill_slot_data(self) -> Dict[str, object]:
|
||||||
|
slot_data: Dict[str, object] = {}
|
||||||
|
|
||||||
for option_name in timespinner_options:
|
for option_name in timespinner_options:
|
||||||
option = getattr(self.world, option_name)[self.player]
|
slot_data[option_name] = is_option_enabled(self.world, self.player, option_name)
|
||||||
slot_data[option_name] = int(option.value)
|
|
||||||
|
|
||||||
slot_data["StinkyMaw"] = 1
|
slot_data["StinkyMaw"] = True
|
||||||
slot_data["ProgressiveVerticalMovement"] = 0
|
slot_data["ProgressiveVerticalMovement"] = False
|
||||||
slot_data["ProgressiveKeycards"] = 0
|
slot_data["ProgressiveKeycards"] = False
|
||||||
slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock[self.player]
|
slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock[self.player]
|
||||||
|
slot_data["PersonalItems"] = get_personal_items(self.player, self.location_cache[self.player])
|
||||||
|
|
||||||
return slot_data
|
return slot_data
|
||||||
|
|
||||||
def create_item(name: str, player: int) -> Item:
|
|
||||||
data = item_table[name]
|
|
||||||
return Item(name, data.progression, data.code, player)
|
|
||||||
|
|
||||||
def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> List[str]:
|
def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> Set[str]:
|
||||||
excluded_items = []
|
excluded_items: Set[str] = set()
|
||||||
|
|
||||||
if is_option_enabled(world, player, "StartWithJewelryBox"):
|
if is_option_enabled(world, player, "StartWithJewelryBox"):
|
||||||
excluded_items.append('Jewelry Box')
|
excluded_items.add('Jewelry Box')
|
||||||
if is_option_enabled(world, player, "StartWithMeyef"):
|
if is_option_enabled(world, player, "StartWithMeyef"):
|
||||||
excluded_items.append('Meyef')
|
excluded_items.add('Meyef')
|
||||||
if is_option_enabled(world, player, "QuickSeed"):
|
if is_option_enabled(world, player, "QuickSeed"):
|
||||||
excluded_items.append('Talaria Attachment')
|
excluded_items.add('Talaria Attachment')
|
||||||
|
|
||||||
return excluded_items
|
return excluded_items
|
||||||
|
|
||||||
def assign_starter_items(world: MultiWorld, player: int, excluded_items: List[str], locked_locations: List[str]):
|
|
||||||
|
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
|
||||||
melee_weapon = world.random.choice(starter_melee_weapons)
|
melee_weapon = world.random.choice(starter_melee_weapons)
|
||||||
spell = world.random.choice(starter_spells)
|
spell = world.random.choice(starter_spells)
|
||||||
|
|
||||||
excluded_items.append(melee_weapon)
|
excluded_items.add(melee_weapon)
|
||||||
excluded_items.append(spell)
|
excluded_items.add(spell)
|
||||||
|
|
||||||
melee_weapon_item = create_item(melee_weapon, player)
|
melee_weapon_item = create_item_with_correct_settings(world, player, melee_weapon)
|
||||||
spell_item = create_item(spell, player)
|
spell_item = create_item_with_correct_settings(world, player, spell)
|
||||||
|
|
||||||
world.get_location('Yo Momma 1', player).place_locked_item(melee_weapon_item)
|
world.get_location('Yo Momma 1', player).place_locked_item(melee_weapon_item)
|
||||||
world.get_location('Yo Momma 2', player).place_locked_item(spell_item)
|
world.get_location('Yo Momma 2', player).place_locked_item(spell_item)
|
||||||
@@ -98,58 +108,70 @@ def assign_starter_items(world: MultiWorld, player: int, excluded_items: List[st
|
|||||||
locked_locations.append('Yo Momma 1')
|
locked_locations.append('Yo Momma 1')
|
||||||
locked_locations.append('Yo Momma 2')
|
locked_locations.append('Yo Momma 2')
|
||||||
|
|
||||||
def get_item_pool(world: MultiWorld, player: int, excluded_items: List[str]) -> List[Item]:
|
|
||||||
pool = []
|
def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]:
|
||||||
|
pool: List[Item] = []
|
||||||
|
|
||||||
for name, data in item_table.items():
|
for name, data in item_table.items():
|
||||||
if not name in excluded_items:
|
if not name in excluded_items:
|
||||||
for _ in range(data.count):
|
for _ in range(data.count):
|
||||||
item = update_progressive_state_based_flags(world, player, name, create_item(name, player))
|
item = create_item_with_correct_settings(world, player, name)
|
||||||
pool.append(item)
|
pool.append(item)
|
||||||
|
|
||||||
return pool
|
return pool
|
||||||
|
|
||||||
def fill_item_pool_with_dummy_items(world: MultiWorld, player: int, locked_locations: List[str], pool: List[Item]):
|
|
||||||
for _ in range(len(get_locations(world, player)) - len(locked_locations) - len(pool)):
|
def fill_item_pool_with_dummy_items(world: MultiWorld, player: int, locked_locations: List[str],
|
||||||
item = create_item(world.random.choice(filler_items), player)
|
location_cache: List[Location], pool: List[Item]):
|
||||||
|
for _ in range(len(location_cache) - len(locked_locations) - len(pool)):
|
||||||
|
item = create_item_with_correct_settings(world, player, world.random.choice(filler_items))
|
||||||
pool.append(item)
|
pool.append(item)
|
||||||
|
|
||||||
def place_first_progression_item(world: MultiWorld, player: int, excluded_items: List[str], locked_locations: List[str]):
|
|
||||||
progression_item = world.random.choice(starter_progression_items)
|
|
||||||
location = world.random.choice(starter_progression_locations)
|
|
||||||
|
|
||||||
excluded_items.append(progression_item)
|
def place_first_progression_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
|
||||||
|
progression_item = world.random.choice(starter_progression_items)
|
||||||
|
location = world.random.choice(starter_progression_locations)
|
||||||
|
|
||||||
|
excluded_items.add(progression_item)
|
||||||
locked_locations.append(location)
|
locked_locations.append(location)
|
||||||
|
|
||||||
item = create_item(progression_item, player)
|
item = create_item_with_correct_settings(world, player, progression_item)
|
||||||
|
|
||||||
world.get_location(location, player).place_locked_item(item)
|
world.get_location(location, player).place_locked_item(item)
|
||||||
|
|
||||||
def update_progressive_state_based_flags(world: MultiWorld, player: int, name: str, data: Item) -> Item:
|
|
||||||
if not data.advancement:
|
def create_item_with_correct_settings(world: MultiWorld, player: int, name: str) -> Item:
|
||||||
return data
|
data = item_table[name]
|
||||||
|
|
||||||
|
item = Item(name, data.progression, data.code, player)
|
||||||
|
item.never_exclude = data.never_exclude
|
||||||
|
|
||||||
|
if not item.advancement:
|
||||||
|
return item
|
||||||
|
|
||||||
if (name == 'Tablet' or name == 'Library Keycard V') and not is_option_enabled(world, player, "DownloadableItems"):
|
if (name == 'Tablet' or name == 'Library Keycard V') and not is_option_enabled(world, player, "DownloadableItems"):
|
||||||
data.advancement = False
|
item.advancement = False
|
||||||
if name == 'Oculus Ring' and not is_option_enabled(world, player, "FacebookMode"):
|
if name == 'Oculus Ring' and not is_option_enabled(world, player, "FacebookMode"):
|
||||||
data.advancement = False
|
item.advancement = False
|
||||||
|
|
||||||
return data
|
return item
|
||||||
|
|
||||||
def setup_events(world: MultiWorld, player: int, locked_locations: List[str]):
|
|
||||||
for location in get_locations(world, player):
|
def setup_events(world: MultiWorld, player: int, locked_locations: List[str], location_cache: List[Location]):
|
||||||
if location.code == EventId:
|
for location in location_cache:
|
||||||
location = world.get_location(location.name, player)
|
if location.address == EventId:
|
||||||
item = Item(location.name, True, EventId, player)
|
item = Item(location.name, True, EventId, player)
|
||||||
|
|
||||||
locked_locations.append(location.name)
|
locked_locations.append(location.name)
|
||||||
|
|
||||||
location.place_locked_item(item)
|
location.place_locked_item(item)
|
||||||
|
|
||||||
def get_item_name_groups() -> Dict[str, List[str]]:
|
|
||||||
groups: Dict[str, List[str]] = {}
|
|
||||||
|
|
||||||
for name, data in item_table.items():
|
def get_personal_items(player: int, locations: List[Location]) -> Dict[int, int]:
|
||||||
groups[data.category] = [ name ] if data.category not in groups else groups[data.category] + [ name ]
|
personal_items: Dict[int, int] = {}
|
||||||
|
|
||||||
return groups
|
for location in locations:
|
||||||
|
if location.address and location.item and location.item.code and location.item.player == player:
|
||||||
|
personal_items[location.address] = location.item.code
|
||||||
|
|
||||||
|
return personal_items
|
||||||
Reference in New Issue
Block a user