mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-25 04:13:26 -07:00
Compare commits
1 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29ae9cd91e |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
|||||||
worlds/blasphemous/region_data.py linguist-generated=true
|
|
||||||
7
.github/workflows/unittests.yml
vendored
7
.github/workflows/unittests.yml
vendored
@@ -37,13 +37,12 @@ jobs:
|
|||||||
- {version: '3.9'}
|
- {version: '3.9'}
|
||||||
- {version: '3.10'}
|
- {version: '3.10'}
|
||||||
- {version: '3.11'}
|
- {version: '3.11'}
|
||||||
- {version: '3.12'}
|
|
||||||
include:
|
include:
|
||||||
- python: {version: '3.8'} # win7 compat
|
- python: {version: '3.8'} # win7 compat
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
- python: {version: '3.12'} # current
|
- python: {version: '3.11'} # current
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
- python: {version: '3.12'} # current
|
- python: {version: '3.11'} # current
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -71,7 +70,7 @@ jobs:
|
|||||||
os:
|
os:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
python:
|
python:
|
||||||
- {version: '3.12'} # current
|
- {version: '3.11'} # current
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
199
BaseClasses.py
199
BaseClasses.py
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
import copy
|
||||||
import itertools
|
import itertools
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
@@ -11,10 +12,8 @@ from argparse import Namespace
|
|||||||
from collections import Counter, deque
|
from collections import Counter, deque
|
||||||
from collections.abc import Collection, MutableSequence
|
from collections.abc import Collection, MutableSequence
|
||||||
from enum import IntEnum, IntFlag
|
from enum import IntEnum, IntFlag
|
||||||
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
|
from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \
|
||||||
Optional, Protocol, Set, Tuple, Union, Type)
|
TypedDict, Union, Type, ClassVar
|
||||||
|
|
||||||
from typing_extensions import NotRequired, TypedDict
|
|
||||||
|
|
||||||
import NetUtils
|
import NetUtils
|
||||||
import Options
|
import Options
|
||||||
@@ -24,16 +23,16 @@ if typing.TYPE_CHECKING:
|
|||||||
from worlds import AutoWorld
|
from worlds import AutoWorld
|
||||||
|
|
||||||
|
|
||||||
class Group(TypedDict):
|
class Group(TypedDict, total=False):
|
||||||
name: str
|
name: str
|
||||||
game: str
|
game: str
|
||||||
world: "AutoWorld.World"
|
world: "AutoWorld.World"
|
||||||
players: AbstractSet[int]
|
players: Set[int]
|
||||||
item_pool: NotRequired[Set[str]]
|
item_pool: Set[str]
|
||||||
replacement_items: NotRequired[Dict[int, Optional[str]]]
|
replacement_items: Dict[int, Optional[str]]
|
||||||
local_items: NotRequired[Set[str]]
|
local_items: Set[str]
|
||||||
non_local_items: NotRequired[Set[str]]
|
non_local_items: Set[str]
|
||||||
link_replacement: NotRequired[bool]
|
link_replacement: bool
|
||||||
|
|
||||||
|
|
||||||
class ThreadBarrierProxy:
|
class ThreadBarrierProxy:
|
||||||
@@ -50,11 +49,6 @@ class ThreadBarrierProxy:
|
|||||||
"Please use multiworld.per_slot_randoms[player] or randomize ahead of output.")
|
"Please use multiworld.per_slot_randoms[player] or randomize ahead of output.")
|
||||||
|
|
||||||
|
|
||||||
class HasNameAndPlayer(Protocol):
|
|
||||||
name: str
|
|
||||||
player: int
|
|
||||||
|
|
||||||
|
|
||||||
class MultiWorld():
|
class MultiWorld():
|
||||||
debug_types = False
|
debug_types = False
|
||||||
player_name: Dict[int, str]
|
player_name: Dict[int, str]
|
||||||
@@ -163,7 +157,7 @@ class MultiWorld():
|
|||||||
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
|
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
|
||||||
|
|
||||||
for player in range(1, players + 1):
|
for player in range(1, players + 1):
|
||||||
def set_player_attr(attr: str, val) -> None:
|
def set_player_attr(attr, val):
|
||||||
self.__dict__.setdefault(attr, {})[player] = val
|
self.__dict__.setdefault(attr, {})[player] = val
|
||||||
set_player_attr('plando_items', [])
|
set_player_attr('plando_items', [])
|
||||||
set_player_attr('plando_texts', {})
|
set_player_attr('plando_texts', {})
|
||||||
@@ -172,13 +166,13 @@ class MultiWorld():
|
|||||||
set_player_attr('completion_condition', lambda state: True)
|
set_player_attr('completion_condition', lambda state: True)
|
||||||
self.worlds = {}
|
self.worlds = {}
|
||||||
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
|
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
|
||||||
"world's random object instead (usually self.random)")
|
"world's random object instead (usually self.random)")
|
||||||
self.plando_options = PlandoOptions.none
|
self.plando_options = PlandoOptions.none
|
||||||
|
|
||||||
def get_all_ids(self) -> Tuple[int, ...]:
|
def get_all_ids(self) -> Tuple[int, ...]:
|
||||||
return self.player_ids + tuple(self.groups)
|
return self.player_ids + tuple(self.groups)
|
||||||
|
|
||||||
def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset()) -> Tuple[int, Group]:
|
def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]:
|
||||||
"""Create a group with name and return the assigned player ID and group.
|
"""Create a group with name and return the assigned player ID and group.
|
||||||
If a group of this name already exists, the set of players is extended instead of creating a new one."""
|
If a group of this name already exists, the set of players is extended instead of creating a new one."""
|
||||||
from worlds import AutoWorld
|
from worlds import AutoWorld
|
||||||
@@ -202,7 +196,7 @@ class MultiWorld():
|
|||||||
|
|
||||||
return new_id, new_group
|
return new_id, new_group
|
||||||
|
|
||||||
def get_player_groups(self, player: int) -> Set[int]:
|
def get_player_groups(self, player) -> Set[int]:
|
||||||
return {group_id for group_id, group in self.groups.items() if player in group["players"]}
|
return {group_id for group_id, group in self.groups.items() if player in group["players"]}
|
||||||
|
|
||||||
def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
|
def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
|
||||||
@@ -265,7 +259,7 @@ class MultiWorld():
|
|||||||
"link_replacement": replacement_prio.index(item_link["link_replacement"]),
|
"link_replacement": replacement_prio.index(item_link["link_replacement"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _name, item_link in item_links.items():
|
for name, item_link in item_links.items():
|
||||||
current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups
|
current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups
|
||||||
pool = set()
|
pool = set()
|
||||||
local_items = set()
|
local_items = set()
|
||||||
@@ -342,8 +336,6 @@ class MultiWorld():
|
|||||||
region = Region("Menu", group_id, self, "ItemLink")
|
region = Region("Menu", group_id, self, "ItemLink")
|
||||||
self.regions.append(region)
|
self.regions.append(region)
|
||||||
locations = region.locations
|
locations = region.locations
|
||||||
# ensure that progression items are linked first, then non-progression
|
|
||||||
self.itempool.sort(key=lambda item: item.advancement)
|
|
||||||
for item in self.itempool:
|
for item in self.itempool:
|
||||||
count = common_item_count.get(item.player, {}).get(item.name, 0)
|
count = common_item_count.get(item.player, {}).get(item.name, 0)
|
||||||
if count:
|
if count:
|
||||||
@@ -397,7 +389,7 @@ class MultiWorld():
|
|||||||
return tuple(world for player, world in self.worlds.items() if
|
return tuple(world for player, world in self.worlds.items() if
|
||||||
player not in self.groups and self.game[player] == game_name)
|
player not in self.groups and self.game[player] == game_name)
|
||||||
|
|
||||||
def get_name_string_for_object(self, obj: HasNameAndPlayer) -> str:
|
def get_name_string_for_object(self, obj) -> str:
|
||||||
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
|
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
|
||||||
|
|
||||||
def get_player_name(self, player: int) -> str:
|
def get_player_name(self, player: int) -> str:
|
||||||
@@ -439,7 +431,7 @@ class MultiWorld():
|
|||||||
subworld = self.worlds[player]
|
subworld = self.worlds[player]
|
||||||
for item in subworld.get_pre_fill_items():
|
for item in subworld.get_pre_fill_items():
|
||||||
subworld.collect(ret, item)
|
subworld.collect(ret, item)
|
||||||
ret.sweep_for_advancements()
|
ret.sweep_for_events()
|
||||||
|
|
||||||
if use_cache:
|
if use_cache:
|
||||||
self._all_state = ret
|
self._all_state = ret
|
||||||
@@ -448,7 +440,7 @@ class MultiWorld():
|
|||||||
def get_items(self) -> List[Item]:
|
def get_items(self) -> List[Item]:
|
||||||
return [loc.item for loc in self.get_filled_locations()] + self.itempool
|
return [loc.item for loc in self.get_filled_locations()] + self.itempool
|
||||||
|
|
||||||
def find_item_locations(self, item: str, player: int, resolve_group_locations: bool = False) -> List[Location]:
|
def find_item_locations(self, item, player: int, resolve_group_locations: bool = False) -> List[Location]:
|
||||||
if resolve_group_locations:
|
if resolve_group_locations:
|
||||||
player_groups = self.get_player_groups(player)
|
player_groups = self.get_player_groups(player)
|
||||||
return [location for location in self.get_locations() if
|
return [location for location in self.get_locations() if
|
||||||
@@ -457,7 +449,7 @@ class MultiWorld():
|
|||||||
return [location for location in self.get_locations() if
|
return [location for location in self.get_locations() if
|
||||||
location.item and location.item.name == item and location.item.player == player]
|
location.item and location.item.name == item and location.item.player == player]
|
||||||
|
|
||||||
def find_item(self, item: str, player: int) -> Location:
|
def find_item(self, item, player: int) -> Location:
|
||||||
return next(location for location in self.get_locations() if
|
return next(location for location in self.get_locations() if
|
||||||
location.item and location.item.name == item and location.item.player == player)
|
location.item and location.item.name == item and location.item.player == player)
|
||||||
|
|
||||||
@@ -550,9 +542,9 @@ class MultiWorld():
|
|||||||
return True
|
return True
|
||||||
state = starting_state.copy()
|
state = starting_state.copy()
|
||||||
else:
|
else:
|
||||||
state = CollectionState(self)
|
if self.has_beaten_game(self.state):
|
||||||
if self.has_beaten_game(state):
|
|
||||||
return True
|
return True
|
||||||
|
state = CollectionState(self)
|
||||||
prog_locations = {location for location in self.get_locations() if location.item
|
prog_locations = {location for location in self.get_locations() if location.item
|
||||||
and location.item.advancement and location not in state.locations_checked}
|
and location.item.advancement and location not in state.locations_checked}
|
||||||
|
|
||||||
@@ -625,7 +617,8 @@ class MultiWorld():
|
|||||||
|
|
||||||
def location_relevant(location: Location) -> bool:
|
def location_relevant(location: Location) -> bool:
|
||||||
"""Determine if this location is relevant to sweep."""
|
"""Determine if this location is relevant to sweep."""
|
||||||
return location.player in players["full"] or location.advancement
|
return location.progress_type != LocationProgressType.EXCLUDED \
|
||||||
|
and (location.player in players["full"] or location.advancement)
|
||||||
|
|
||||||
def all_done() -> bool:
|
def all_done() -> bool:
|
||||||
"""Check if all access rules are fulfilled"""
|
"""Check if all access rules are fulfilled"""
|
||||||
@@ -670,7 +663,7 @@ class CollectionState():
|
|||||||
multiworld: MultiWorld
|
multiworld: MultiWorld
|
||||||
reachable_regions: Dict[int, Set[Region]]
|
reachable_regions: Dict[int, Set[Region]]
|
||||||
blocked_connections: Dict[int, Set[Entrance]]
|
blocked_connections: Dict[int, Set[Entrance]]
|
||||||
advancements: Set[Location]
|
events: Set[Location]
|
||||||
path: Dict[Union[Region, Entrance], PathValue]
|
path: Dict[Union[Region, Entrance], PathValue]
|
||||||
locations_checked: Set[Location]
|
locations_checked: Set[Location]
|
||||||
stale: Dict[int, bool]
|
stale: Dict[int, bool]
|
||||||
@@ -682,7 +675,7 @@ class CollectionState():
|
|||||||
self.multiworld = parent
|
self.multiworld = parent
|
||||||
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
|
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
|
||||||
self.blocked_connections = {player: set() for player in parent.get_all_ids()}
|
self.blocked_connections = {player: set() for player in parent.get_all_ids()}
|
||||||
self.advancements = set()
|
self.events = set()
|
||||||
self.path = {}
|
self.path = {}
|
||||||
self.locations_checked = set()
|
self.locations_checked = set()
|
||||||
self.stale = {player: True for player in parent.get_all_ids()}
|
self.stale = {player: True for player in parent.get_all_ids()}
|
||||||
@@ -694,25 +687,17 @@ class CollectionState():
|
|||||||
|
|
||||||
def update_reachable_regions(self, player: int):
|
def update_reachable_regions(self, player: int):
|
||||||
self.stale[player] = False
|
self.stale[player] = False
|
||||||
world: AutoWorld.World = self.multiworld.worlds[player]
|
|
||||||
reachable_regions = self.reachable_regions[player]
|
reachable_regions = self.reachable_regions[player]
|
||||||
|
blocked_connections = self.blocked_connections[player]
|
||||||
queue = deque(self.blocked_connections[player])
|
queue = deque(self.blocked_connections[player])
|
||||||
start: Region = world.get_region(world.origin_region_name)
|
start = self.multiworld.get_region("Menu", player)
|
||||||
|
|
||||||
# init on first call - this can't be done on construction since the regions don't exist yet
|
# init on first call - this can't be done on construction since the regions don't exist yet
|
||||||
if start not in reachable_regions:
|
if start not in reachable_regions:
|
||||||
reachable_regions.add(start)
|
reachable_regions.add(start)
|
||||||
self.blocked_connections[player].update(start.exits)
|
blocked_connections.update(start.exits)
|
||||||
queue.extend(start.exits)
|
queue.extend(start.exits)
|
||||||
|
|
||||||
if world.explicit_indirect_conditions:
|
|
||||||
self._update_reachable_regions_explicit_indirect_conditions(player, queue)
|
|
||||||
else:
|
|
||||||
self._update_reachable_regions_auto_indirect_conditions(player, queue)
|
|
||||||
|
|
||||||
def _update_reachable_regions_explicit_indirect_conditions(self, player: int, queue: deque):
|
|
||||||
reachable_regions = self.reachable_regions[player]
|
|
||||||
blocked_connections = self.blocked_connections[player]
|
|
||||||
# run BFS on all connections, and keep track of those blocked by missing items
|
# run BFS on all connections, and keep track of those blocked by missing items
|
||||||
while queue:
|
while queue:
|
||||||
connection = queue.popleft()
|
connection = queue.popleft()
|
||||||
@@ -732,39 +717,16 @@ class CollectionState():
|
|||||||
if new_entrance in blocked_connections and new_entrance not in queue:
|
if new_entrance in blocked_connections and new_entrance not in queue:
|
||||||
queue.append(new_entrance)
|
queue.append(new_entrance)
|
||||||
|
|
||||||
def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque):
|
|
||||||
reachable_regions = self.reachable_regions[player]
|
|
||||||
blocked_connections = self.blocked_connections[player]
|
|
||||||
new_connection: bool = True
|
|
||||||
# run BFS on all connections, and keep track of those blocked by missing items
|
|
||||||
while new_connection:
|
|
||||||
new_connection = False
|
|
||||||
while queue:
|
|
||||||
connection = queue.popleft()
|
|
||||||
new_region = connection.connected_region
|
|
||||||
if new_region in reachable_regions:
|
|
||||||
blocked_connections.remove(connection)
|
|
||||||
elif connection.can_reach(self):
|
|
||||||
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
|
|
||||||
reachable_regions.add(new_region)
|
|
||||||
blocked_connections.remove(connection)
|
|
||||||
blocked_connections.update(new_region.exits)
|
|
||||||
queue.extend(new_region.exits)
|
|
||||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
|
||||||
new_connection = True
|
|
||||||
# sweep for indirect connections, mostly Entrance.can_reach(unrelated_Region)
|
|
||||||
queue.extend(blocked_connections)
|
|
||||||
|
|
||||||
def copy(self) -> CollectionState:
|
def copy(self) -> CollectionState:
|
||||||
ret = CollectionState(self.multiworld)
|
ret = CollectionState(self.multiworld)
|
||||||
ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()}
|
ret.prog_items = copy.deepcopy(self.prog_items)
|
||||||
ret.reachable_regions = {player: region_set.copy() for player, region_set in
|
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in
|
||||||
self.reachable_regions.items()}
|
self.reachable_regions}
|
||||||
ret.blocked_connections = {player: entrance_set.copy() for player, entrance_set in
|
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in
|
||||||
self.blocked_connections.items()}
|
self.blocked_connections}
|
||||||
ret.advancements = self.advancements.copy()
|
ret.events = copy.copy(self.events)
|
||||||
ret.path = self.path.copy()
|
ret.path = copy.copy(self.path)
|
||||||
ret.locations_checked = self.locations_checked.copy()
|
ret.locations_checked = copy.copy(self.locations_checked)
|
||||||
for function in self.additional_copy_functions:
|
for function in self.additional_copy_functions:
|
||||||
ret = function(self, ret)
|
ret = function(self, ret)
|
||||||
return ret
|
return ret
|
||||||
@@ -795,24 +757,19 @@ class CollectionState():
|
|||||||
return self.multiworld.get_region(spot, player).can_reach(self)
|
return self.multiworld.get_region(spot, player).can_reach(self)
|
||||||
|
|
||||||
def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None:
|
def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None:
|
||||||
Utils.deprecate("sweep_for_events has been renamed to sweep_for_advancements. The functionality is the same. "
|
|
||||||
"Please switch over to sweep_for_advancements.")
|
|
||||||
return self.sweep_for_advancements(locations)
|
|
||||||
|
|
||||||
def sweep_for_advancements(self, locations: Optional[Iterable[Location]] = None) -> None:
|
|
||||||
if locations is None:
|
if locations is None:
|
||||||
locations = self.multiworld.get_filled_locations()
|
locations = self.multiworld.get_filled_locations()
|
||||||
reachable_advancements = True
|
reachable_events = True
|
||||||
# since the loop has a good chance to run more than once, only filter the advancements once
|
# since the loop has a good chance to run more than once, only filter the events once
|
||||||
locations = {location for location in locations if location.advancement and location not in self.advancements}
|
locations = {location for location in locations if location.advancement and location not in self.events}
|
||||||
|
|
||||||
while reachable_advancements:
|
while reachable_events:
|
||||||
reachable_advancements = {location for location in locations if location.can_reach(self)}
|
reachable_events = {location for location in locations if location.can_reach(self)}
|
||||||
locations -= reachable_advancements
|
locations -= reachable_events
|
||||||
for advancement in reachable_advancements:
|
for event in reachable_events:
|
||||||
self.advancements.add(advancement)
|
self.events.add(event)
|
||||||
assert isinstance(advancement.item, Item), "tried to collect Event with no Item"
|
assert isinstance(event.item, Item), "tried to collect Event with no Item"
|
||||||
self.collect(advancement.item, True, advancement)
|
self.collect(event.item, True, event)
|
||||||
|
|
||||||
# item name related
|
# item name related
|
||||||
def has(self, item: str, player: int, count: int = 1) -> bool:
|
def has(self, item: str, player: int, count: int = 1) -> bool:
|
||||||
@@ -907,16 +864,20 @@ class CollectionState():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Item related
|
# Item related
|
||||||
def collect(self, item: Item, prevent_sweep: bool = False, location: Optional[Location] = None) -> bool:
|
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
|
||||||
if location:
|
if location:
|
||||||
self.locations_checked.add(location)
|
self.locations_checked.add(location)
|
||||||
|
|
||||||
changed = self.multiworld.worlds[item.player].collect(self, item)
|
changed = self.multiworld.worlds[item.player].collect(self, item)
|
||||||
|
|
||||||
|
if not changed and event:
|
||||||
|
self.prog_items[item.player][item.name] += 1
|
||||||
|
changed = True
|
||||||
|
|
||||||
self.stale[item.player] = True
|
self.stale[item.player] = True
|
||||||
|
|
||||||
if changed and not prevent_sweep:
|
if changed and not event:
|
||||||
self.sweep_for_advancements()
|
self.sweep_for_events()
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
@@ -940,7 +901,7 @@ class Entrance:
|
|||||||
addresses = None
|
addresses = None
|
||||||
target = None
|
target = None
|
||||||
|
|
||||||
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None:
|
def __init__(self, player: int, name: str = '', parent: Region = None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.parent_region = parent
|
self.parent_region = parent
|
||||||
self.player = player
|
self.player = player
|
||||||
@@ -960,6 +921,9 @@ class Entrance:
|
|||||||
region.entrances.append(self)
|
region.entrances.append(self)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
multiworld = self.parent_region.multiworld if self.parent_region else None
|
multiworld = self.parent_region.multiworld if self.parent_region else None
|
||||||
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
||||||
|
|
||||||
@@ -1085,7 +1049,7 @@ class Region:
|
|||||||
self.locations.append(location_type(self.player, location, address, self))
|
self.locations.append(location_type(self.player, location, address, self))
|
||||||
|
|
||||||
def connect(self, connecting_region: Region, name: Optional[str] = None,
|
def connect(self, connecting_region: Region, name: Optional[str] = None,
|
||||||
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
|
rule: Optional[Callable[[CollectionState], bool]] = None) -> entrance_type:
|
||||||
"""
|
"""
|
||||||
Connects this Region to another Region, placing the provided rule on the connection.
|
Connects this Region to another Region, placing the provided rule on the connection.
|
||||||
|
|
||||||
@@ -1125,6 +1089,9 @@ class Region:
|
|||||||
rules[connecting_region] if rules and connecting_region in rules else None)
|
rules[connecting_region] if rules and connecting_region in rules else None)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
|
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
|
||||||
|
|
||||||
|
|
||||||
@@ -1143,9 +1110,9 @@ class Location:
|
|||||||
locked: bool = False
|
locked: bool = False
|
||||||
show_in_spoiler: bool = True
|
show_in_spoiler: bool = True
|
||||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||||
always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False)
|
always_allow = staticmethod(lambda state, item: False)
|
||||||
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
||||||
item_rule: Callable[[Item], bool] = staticmethod(lambda item: True)
|
item_rule = staticmethod(lambda item: True)
|
||||||
item: Optional[Item] = None
|
item: Optional[Item] = None
|
||||||
|
|
||||||
def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None):
|
def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None):
|
||||||
@@ -1154,20 +1121,16 @@ class Location:
|
|||||||
self.address = address
|
self.address = address
|
||||||
self.parent_region = parent
|
self.parent_region = parent
|
||||||
|
|
||||||
def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool:
|
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
||||||
return ((
|
return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items)
|
||||||
self.always_allow(state, item)
|
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
|
||||||
and item.name not in state.multiworld.worlds[item.player].options.non_local_items
|
and self.item_rule(item)
|
||||||
) or (
|
and (not check_access or self.can_reach(state))))
|
||||||
(self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
|
|
||||||
and self.item_rule(item)
|
|
||||||
and (not check_access or self.can_reach(state))
|
|
||||||
))
|
|
||||||
|
|
||||||
def can_reach(self, state: CollectionState) -> bool:
|
def can_reach(self, state: CollectionState) -> bool:
|
||||||
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average
|
# self.access_rule computes faster on average, so placing it first for faster abort
|
||||||
assert self.parent_region, "Can't reach location without region"
|
assert self.parent_region, "Can't reach location without region"
|
||||||
return self.parent_region.can_reach(state) and self.access_rule(state)
|
return self.access_rule(state) and self.parent_region.can_reach(state)
|
||||||
|
|
||||||
def place_locked_item(self, item: Item):
|
def place_locked_item(self, item: Item):
|
||||||
if self.item:
|
if self.item:
|
||||||
@@ -1177,6 +1140,9 @@ class Location:
|
|||||||
self.locked = True
|
self.locked = True
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
|
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
|
||||||
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
||||||
|
|
||||||
@@ -1198,7 +1164,7 @@ class Location:
|
|||||||
@property
|
@property
|
||||||
def native_item(self) -> bool:
|
def native_item(self) -> bool:
|
||||||
"""Returns True if the item in this location matches game."""
|
"""Returns True if the item in this location matches game."""
|
||||||
return self.item is not None and self.item.game == self.game
|
return self.item and self.item.game == self.game
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hint_text(self) -> str:
|
def hint_text(self) -> str:
|
||||||
@@ -1209,7 +1175,7 @@ class ItemClassification(IntFlag):
|
|||||||
filler = 0b0000 # aka trash, as in filler items like ammo, currency etc,
|
filler = 0b0000 # aka trash, as in filler items like ammo, currency etc,
|
||||||
progression = 0b0001 # Item that is logically relevant
|
progression = 0b0001 # Item that is logically relevant
|
||||||
useful = 0b0010 # Item that is generally quite useful, but not required for anything logical
|
useful = 0b0010 # Item that is generally quite useful, but not required for anything logical
|
||||||
trap = 0b0100 # detrimental item
|
trap = 0b0100 # detrimental or entirely useless (nothing) item
|
||||||
skip_balancing = 0b1000 # should technically never occur on its own
|
skip_balancing = 0b1000 # should technically never occur on its own
|
||||||
# Item that is logically relevant, but progression balancing should not touch.
|
# Item that is logically relevant, but progression balancing should not touch.
|
||||||
# Typically currency or other counted items.
|
# Typically currency or other counted items.
|
||||||
@@ -1281,6 +1247,9 @@ class Item:
|
|||||||
return hash((self.name, self.player))
|
return hash((self.name, self.player))
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
if self.location and self.location.parent_region and self.location.parent_region.multiworld:
|
if self.location and self.location.parent_region and self.location.parent_region.multiworld:
|
||||||
return self.location.parent_region.multiworld.get_name_string_for_object(self)
|
return self.location.parent_region.multiworld.get_name_string_for_object(self)
|
||||||
return f"{self.name} (Player {self.player})"
|
return f"{self.name} (Player {self.player})"
|
||||||
@@ -1358,9 +1327,9 @@ class Spoiler:
|
|||||||
|
|
||||||
# in the second phase, we cull each sphere such that the game is still beatable,
|
# in the second phase, we cull each sphere such that the game is still beatable,
|
||||||
# reducing each range of influence to the bare minimum required inside it
|
# reducing each range of influence to the bare minimum required inside it
|
||||||
restore_later: Dict[Location, Item] = {}
|
restore_later = {}
|
||||||
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
|
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
|
||||||
to_delete: Set[Location] = set()
|
to_delete = set()
|
||||||
for location in sphere:
|
for location in sphere:
|
||||||
# we remove the item at location and check if game is still beatable
|
# we remove the item at location and check if game is still beatable
|
||||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
|
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
|
||||||
@@ -1378,7 +1347,7 @@ class Spoiler:
|
|||||||
sphere -= to_delete
|
sphere -= to_delete
|
||||||
|
|
||||||
# second phase, sphere 0
|
# second phase, sphere 0
|
||||||
removed_precollected: List[Item] = []
|
removed_precollected = []
|
||||||
for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement):
|
for item in (i for i in chain.from_iterable(multiworld.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)
|
||||||
multiworld.precollected_items[item.player].remove(item)
|
multiworld.precollected_items[item.player].remove(item)
|
||||||
@@ -1459,7 +1428,7 @@ class Spoiler:
|
|||||||
# Maybe move the big bomb over to the Event system instead?
|
# Maybe move the big bomb over to the Event system instead?
|
||||||
if any(exit_path == 'Pyramid Fairy' for path in self.paths.values()
|
if any(exit_path == 'Pyramid Fairy' for path in self.paths.values()
|
||||||
for (_, exit_path) in path):
|
for (_, exit_path) in path):
|
||||||
if multiworld.worlds[player].options.mode != 'inverted':
|
if multiworld.mode[player] != 'inverted':
|
||||||
self.paths[str(multiworld.get_region('Big Bomb Shop', player))] = \
|
self.paths[str(multiworld.get_region('Big Bomb Shop', player))] = \
|
||||||
get_path(state, multiworld.get_region('Big Bomb Shop', player))
|
get_path(state, multiworld.get_region('Big Bomb Shop', player))
|
||||||
else:
|
else:
|
||||||
@@ -1531,9 +1500,9 @@ class Spoiler:
|
|||||||
|
|
||||||
if self.paths:
|
if self.paths:
|
||||||
outfile.write('\n\nPaths:\n\n')
|
outfile.write('\n\nPaths:\n\n')
|
||||||
path_listings: List[str] = []
|
path_listings = []
|
||||||
for location, path in sorted(self.paths.items()):
|
for location, path in sorted(self.paths.items()):
|
||||||
path_lines: List[str] = []
|
path_lines = []
|
||||||
for region, exit in path:
|
for region, exit in path:
|
||||||
if exit is not None:
|
if exit is not None:
|
||||||
path_lines.append("{} -> {}".format(region, exit))
|
path_lines.append("{} -> {}".format(region, exit))
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
ModuleUpdate.update()
|
ModuleUpdate.update()
|
||||||
|
|
||||||
from worlds._bizhawk.context import launch
|
from worlds._bizhawk.context import launch
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
launch(*sys.argv[1:])
|
launch()
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ class CommonContext:
|
|||||||
starting_reconnect_delay: int = 5
|
starting_reconnect_delay: int = 5
|
||||||
current_reconnect_delay: int = starting_reconnect_delay
|
current_reconnect_delay: int = starting_reconnect_delay
|
||||||
command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor
|
command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor
|
||||||
ui: typing.Optional["kvui.GameManager"] = None
|
ui = None
|
||||||
ui_task: typing.Optional["asyncio.Task[None]"] = None
|
ui_task: typing.Optional["asyncio.Task[None]"] = None
|
||||||
input_task: typing.Optional["asyncio.Task[None]"] = None
|
input_task: typing.Optional["asyncio.Task[None]"] = None
|
||||||
keep_alive_task: typing.Optional["asyncio.Task[None]"] = None
|
keep_alive_task: typing.Optional["asyncio.Task[None]"] = None
|
||||||
@@ -662,19 +662,17 @@ class CommonContext:
|
|||||||
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
|
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
|
||||||
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
|
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
|
||||||
|
|
||||||
def make_gui(self) -> typing.Type["kvui.GameManager"]:
|
def run_gui(self):
|
||||||
"""To return the Kivy App class needed for run_gui so it can be overridden before being built"""
|
"""Import kivy UI system and start running it as self.ui_task."""
|
||||||
from kvui import GameManager
|
from kvui import GameManager
|
||||||
|
|
||||||
class TextManager(GameManager):
|
class TextManager(GameManager):
|
||||||
|
logging_pairs = [
|
||||||
|
("Client", "Archipelago")
|
||||||
|
]
|
||||||
base_title = "Archipelago Text Client"
|
base_title = "Archipelago Text Client"
|
||||||
|
|
||||||
return TextManager
|
self.ui = TextManager(self)
|
||||||
|
|
||||||
def run_gui(self):
|
|
||||||
"""Import kivy UI system from make_gui() and start running it as self.ui_task."""
|
|
||||||
ui_class = self.make_gui()
|
|
||||||
self.ui = ui_class(self)
|
|
||||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||||
|
|
||||||
def run_cli(self):
|
def run_cli(self):
|
||||||
@@ -996,7 +994,7 @@ def get_base_parser(description: typing.Optional[str] = None):
|
|||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
def run_as_textclient(*args):
|
def run_as_textclient():
|
||||||
class TextContext(CommonContext):
|
class TextContext(CommonContext):
|
||||||
# Text Mode to use !hint and such with games that have no text entry
|
# Text Mode to use !hint and such with games that have no text entry
|
||||||
tags = CommonContext.tags | {"TextOnly"}
|
tags = CommonContext.tags | {"TextOnly"}
|
||||||
@@ -1035,18 +1033,15 @@ def run_as_textclient(*args):
|
|||||||
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
|
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
|
||||||
parser.add_argument('--name', default=None, help="Slot Name to connect as.")
|
parser.add_argument('--name', default=None, help="Slot Name to connect as.")
|
||||||
parser.add_argument("url", nargs="?", help="Archipelago connection url")
|
parser.add_argument("url", nargs="?", help="Archipelago connection url")
|
||||||
args = parser.parse_args(args)
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.url:
|
if args.url:
|
||||||
url = urllib.parse.urlparse(args.url)
|
url = urllib.parse.urlparse(args.url)
|
||||||
if url.scheme == "archipelago":
|
args.connect = url.netloc
|
||||||
args.connect = url.netloc
|
if url.username:
|
||||||
if url.username:
|
args.name = urllib.parse.unquote(url.username)
|
||||||
args.name = urllib.parse.unquote(url.username)
|
if url.password:
|
||||||
if url.password:
|
args.password = urllib.parse.unquote(url.password)
|
||||||
args.password = urllib.parse.unquote(url.password)
|
|
||||||
else:
|
|
||||||
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
|
|
||||||
|
|
||||||
colorama.init()
|
colorama.init()
|
||||||
|
|
||||||
@@ -1056,4 +1051,4 @@ def run_as_textclient(*args):
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING
|
logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING
|
||||||
run_as_textclient(*sys.argv[1:]) # default value for parse_args
|
run_as_textclient()
|
||||||
|
|||||||
60
Fill.py
60
Fill.py
@@ -12,12 +12,7 @@ from worlds.generic.Rules import add_item_rule
|
|||||||
|
|
||||||
|
|
||||||
class FillError(RuntimeError):
|
class FillError(RuntimeError):
|
||||||
def __init__(self, *args: typing.Union[str, typing.Any], **kwargs) -> None:
|
pass
|
||||||
if "multiworld" in kwargs and isinstance(args[0], str):
|
|
||||||
placements = (args[0] + f"\nAll Placements:\n" +
|
|
||||||
f"{[(loc, loc.item) for loc in kwargs['multiworld'].get_filled_locations()]}")
|
|
||||||
args = (placements, *args[1:])
|
|
||||||
super().__init__(*args)
|
|
||||||
|
|
||||||
|
|
||||||
def _log_fill_progress(name: str, placed: int, total_items: int) -> None:
|
def _log_fill_progress(name: str, placed: int, total_items: int) -> None:
|
||||||
@@ -29,7 +24,7 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
|
|||||||
new_state = base_state.copy()
|
new_state = base_state.copy()
|
||||||
for item in itempool:
|
for item in itempool:
|
||||||
new_state.collect(item, True)
|
new_state.collect(item, True)
|
||||||
new_state.sweep_for_advancements(locations=locations)
|
new_state.sweep_for_events(locations=locations)
|
||||||
return new_state
|
return new_state
|
||||||
|
|
||||||
|
|
||||||
@@ -217,7 +212,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
|||||||
f"Unfilled locations:\n"
|
f"Unfilled locations:\n"
|
||||||
f"{', '.join(str(location) for location in locations)}\n"
|
f"{', '.join(str(location) for location in locations)}\n"
|
||||||
f"Already placed {len(placements)}:\n"
|
f"Already placed {len(placements)}:\n"
|
||||||
f"{', '.join(str(place) for place in placements)}", multiworld=multiworld)
|
f"{', '.join(str(place) for place in placements)}")
|
||||||
|
|
||||||
item_pool.extend(unplaced_items)
|
item_pool.extend(unplaced_items)
|
||||||
|
|
||||||
@@ -232,12 +227,15 @@ def remaining_fill(multiworld: MultiWorld,
|
|||||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
||||||
total = min(len(itempool), len(locations))
|
total = min(len(itempool), len(locations))
|
||||||
placed = 0
|
placed = 0
|
||||||
|
|
||||||
|
state = CollectionState(multiworld)
|
||||||
|
|
||||||
while locations and itempool:
|
while locations and itempool:
|
||||||
item_to_place = itempool.pop()
|
item_to_place = itempool.pop()
|
||||||
spot_to_fill: typing.Optional[Location] = None
|
spot_to_fill: typing.Optional[Location] = None
|
||||||
|
|
||||||
for i, location in enumerate(locations):
|
for i, location in enumerate(locations):
|
||||||
if location.item_rule(item_to_place):
|
if location.can_fill(state, item_to_place, check_access=False):
|
||||||
# popping by index is faster than removing by content,
|
# popping by index is faster than removing by content,
|
||||||
spot_to_fill = locations.pop(i)
|
spot_to_fill = locations.pop(i)
|
||||||
# skipping a scan for the element
|
# skipping a scan for the element
|
||||||
@@ -258,7 +256,7 @@ def remaining_fill(multiworld: MultiWorld,
|
|||||||
|
|
||||||
location.item = None
|
location.item = None
|
||||||
placed_item.location = None
|
placed_item.location = None
|
||||||
if location.item_rule(item_to_place):
|
if location.can_fill(state, item_to_place, check_access=False):
|
||||||
# Add this item to the existing placement, and
|
# Add this item to the existing placement, and
|
||||||
# add the old item to the back of the queue
|
# add the old item to the back of the queue
|
||||||
spot_to_fill = placements.pop(i)
|
spot_to_fill = placements.pop(i)
|
||||||
@@ -304,7 +302,7 @@ def remaining_fill(multiworld: MultiWorld,
|
|||||||
f"Unfilled locations:\n"
|
f"Unfilled locations:\n"
|
||||||
f"{', '.join(str(location) for location in locations)}\n"
|
f"{', '.join(str(location) for location in locations)}\n"
|
||||||
f"Already placed {len(placements)}:\n"
|
f"Already placed {len(placements)}:\n"
|
||||||
f"{', '.join(str(place) for place in placements)}", multiworld=multiworld)
|
f"{', '.join(str(place) for place in placements)}")
|
||||||
|
|
||||||
itempool.extend(unplaced_items)
|
itempool.extend(unplaced_items)
|
||||||
|
|
||||||
@@ -329,8 +327,8 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
|
|||||||
pool.append(location.item)
|
pool.append(location.item)
|
||||||
state.remove(location.item)
|
state.remove(location.item)
|
||||||
location.item = None
|
location.item = None
|
||||||
if location in state.advancements:
|
if location in state.events:
|
||||||
state.advancements.remove(location)
|
state.events.remove(location)
|
||||||
locations.append(location)
|
locations.append(location)
|
||||||
if pool and locations:
|
if pool and locations:
|
||||||
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
|
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
|
||||||
@@ -363,7 +361,7 @@ def distribute_early_items(multiworld: MultiWorld,
|
|||||||
early_priority_locations: typing.List[Location] = []
|
early_priority_locations: typing.List[Location] = []
|
||||||
loc_indexes_to_remove: typing.Set[int] = set()
|
loc_indexes_to_remove: typing.Set[int] = set()
|
||||||
base_state = multiworld.state.copy()
|
base_state = multiworld.state.copy()
|
||||||
base_state.sweep_for_advancements(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
|
base_state.sweep_for_events(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
|
||||||
for i, loc in enumerate(fill_locations):
|
for i, loc in enumerate(fill_locations):
|
||||||
if loc.can_reach(base_state):
|
if loc.can_reach(base_state):
|
||||||
if loc.progress_type == LocationProgressType.PRIORITY:
|
if loc.progress_type == LocationProgressType.PRIORITY:
|
||||||
@@ -475,26 +473,28 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
|||||||
nonlocal lock_later
|
nonlocal lock_later
|
||||||
lock_later.append(location)
|
lock_later.append(location)
|
||||||
|
|
||||||
single_player = multiworld.players == 1 and not multiworld.groups
|
|
||||||
|
|
||||||
if prioritylocations:
|
if prioritylocations:
|
||||||
# "priority fill"
|
# "priority fill"
|
||||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
|
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
|
||||||
single_player_placement=single_player, swap=False, on_place=mark_for_locking, name="Priority")
|
single_player_placement=multiworld.players == 1, swap=False, on_place=mark_for_locking,
|
||||||
|
name="Priority")
|
||||||
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
||||||
defaultlocations = prioritylocations + defaultlocations
|
defaultlocations = prioritylocations + defaultlocations
|
||||||
|
|
||||||
if progitempool:
|
if progitempool:
|
||||||
# "advancement/progression fill"
|
# "advancement/progression fill"
|
||||||
if panic_method == "swap":
|
if panic_method == "swap":
|
||||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=True,
|
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
|
||||||
name="Progression", single_player_placement=single_player)
|
swap=True,
|
||||||
|
name="Progression", single_player_placement=multiworld.players == 1)
|
||||||
elif panic_method == "raise":
|
elif panic_method == "raise":
|
||||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
|
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
|
||||||
name="Progression", single_player_placement=single_player)
|
swap=False,
|
||||||
|
name="Progression", single_player_placement=multiworld.players == 1)
|
||||||
elif panic_method == "start_inventory":
|
elif panic_method == "start_inventory":
|
||||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
|
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
|
||||||
allow_partial=True, name="Progression", single_player_placement=single_player)
|
swap=False, allow_partial=True,
|
||||||
|
name="Progression", single_player_placement=multiworld.players == 1)
|
||||||
if progitempool:
|
if progitempool:
|
||||||
for item in progitempool:
|
for item in progitempool:
|
||||||
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
|
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
|
||||||
@@ -509,8 +509,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
|||||||
if progitempool:
|
if progitempool:
|
||||||
raise FillError(
|
raise FillError(
|
||||||
f"Not enough locations for progression items. "
|
f"Not enough locations for progression items. "
|
||||||
f"There are {len(progitempool)} more progression items than there are available locations.",
|
f"There are {len(progitempool)} more progression items than there are available locations."
|
||||||
multiworld=multiworld,
|
|
||||||
)
|
)
|
||||||
accessibility_corrections(multiworld, multiworld.state, defaultlocations)
|
accessibility_corrections(multiworld, multiworld.state, defaultlocations)
|
||||||
|
|
||||||
@@ -527,8 +526,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
|||||||
if excludedlocations:
|
if excludedlocations:
|
||||||
raise FillError(
|
raise FillError(
|
||||||
f"Not enough filler items for excluded locations. "
|
f"Not enough filler items for excluded locations. "
|
||||||
f"There are {len(excludedlocations)} more excluded locations than filler or trap items.",
|
f"There are {len(excludedlocations)} more excluded locations than filler or trap items."
|
||||||
multiworld=multiworld,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
restitempool = filleritempool + usefulitempool
|
restitempool = filleritempool + usefulitempool
|
||||||
@@ -556,7 +554,7 @@ def flood_items(multiworld: MultiWorld) -> None:
|
|||||||
progress_done = False
|
progress_done = False
|
||||||
|
|
||||||
# sweep once to pick up preplaced items
|
# sweep once to pick up preplaced items
|
||||||
multiworld.state.sweep_for_advancements()
|
multiworld.state.sweep_for_events()
|
||||||
|
|
||||||
# fill multiworld from top of itempool while we can
|
# fill multiworld from top of itempool while we can
|
||||||
while not progress_done:
|
while not progress_done:
|
||||||
@@ -594,7 +592,7 @@ def flood_items(multiworld: MultiWorld) -> None:
|
|||||||
if candidate_item_to_place is not None:
|
if candidate_item_to_place is not None:
|
||||||
item_to_place = candidate_item_to_place
|
item_to_place = candidate_item_to_place
|
||||||
else:
|
else:
|
||||||
raise FillError('No more progress items left to place.', multiworld=multiworld)
|
raise FillError('No more progress items left to place.')
|
||||||
|
|
||||||
# find item to replace with progress item
|
# find item to replace with progress item
|
||||||
location_list = multiworld.get_reachable_locations()
|
location_list = multiworld.get_reachable_locations()
|
||||||
@@ -744,7 +742,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
|||||||
), items_to_test):
|
), items_to_test):
|
||||||
reducing_state.collect(location.item, True, location)
|
reducing_state.collect(location.item, True, location)
|
||||||
|
|
||||||
reducing_state.sweep_for_advancements(locations=locations_to_test)
|
reducing_state.sweep_for_events(locations=locations_to_test)
|
||||||
|
|
||||||
if multiworld.has_beaten_game(balancing_state):
|
if multiworld.has_beaten_game(balancing_state):
|
||||||
if not multiworld.has_beaten_game(reducing_state):
|
if not multiworld.has_beaten_game(reducing_state):
|
||||||
@@ -827,7 +825,7 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
|||||||
warn(warning, force)
|
warn(warning, force)
|
||||||
|
|
||||||
swept_state = multiworld.state.copy()
|
swept_state = multiworld.state.copy()
|
||||||
swept_state.sweep_for_advancements()
|
swept_state.sweep_for_events()
|
||||||
reachable = frozenset(multiworld.get_reachable_locations(swept_state))
|
reachable = frozenset(multiworld.get_reachable_locations(swept_state))
|
||||||
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
||||||
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
||||||
|
|||||||
36
Generate.py
36
Generate.py
@@ -43,10 +43,10 @@ def mystery_argparse():
|
|||||||
parser.add_argument('--race', action='store_true', default=defaults.race)
|
parser.add_argument('--race', action='store_true', default=defaults.race)
|
||||||
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
|
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
|
||||||
parser.add_argument('--log_level', default='info', help='Sets log level')
|
parser.add_argument('--log_level', default='info', help='Sets log level')
|
||||||
parser.add_argument("--csv_output", action="store_true",
|
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0),
|
||||||
help="Output rolled player options to csv (made for async multiworld).")
|
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
|
||||||
parser.add_argument("--plando", default=defaults.plando_options,
|
parser.add_argument('--plando', default=defaults.plando_options,
|
||||||
help="List of options that can be set manually. Can be combined, for example \"bosses, items\"")
|
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
|
||||||
parser.add_argument("--skip_prog_balancing", action="store_true",
|
parser.add_argument("--skip_prog_balancing", action="store_true",
|
||||||
help="Skip progression balancing step during generation.")
|
help="Skip progression balancing step during generation.")
|
||||||
parser.add_argument("--skip_output", action="store_true",
|
parser.add_argument("--skip_output", action="store_true",
|
||||||
@@ -155,8 +155,6 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
|||||||
erargs.outputpath = args.outputpath
|
erargs.outputpath = args.outputpath
|
||||||
erargs.skip_prog_balancing = args.skip_prog_balancing
|
erargs.skip_prog_balancing = args.skip_prog_balancing
|
||||||
erargs.skip_output = args.skip_output
|
erargs.skip_output = args.skip_output
|
||||||
erargs.name = {}
|
|
||||||
erargs.csv_output = args.csv_output
|
|
||||||
|
|
||||||
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
||||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
|
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
|
||||||
@@ -204,7 +202,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
|||||||
|
|
||||||
if path == args.weights_file_path: # if name came from the weights file, just use base player name
|
if path == args.weights_file_path: # if name came from the weights file, just use base player name
|
||||||
erargs.name[player] = f"Player{player}"
|
erargs.name[player] = f"Player{player}"
|
||||||
elif player not in erargs.name: # if name was not specified, generate it from filename
|
elif not erargs.name[player]: # if name was not specified, generate it from filename
|
||||||
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
||||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||||
|
|
||||||
@@ -217,6 +215,28 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
|||||||
if len(set(name.lower() for name in erargs.name.values())) != len(erargs.name):
|
if len(set(name.lower() for name in erargs.name.values())) != len(erargs.name):
|
||||||
raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in erargs.name.values())}")
|
raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in erargs.name.values())}")
|
||||||
|
|
||||||
|
if args.yaml_output:
|
||||||
|
import yaml
|
||||||
|
important = {}
|
||||||
|
for option, player_settings in vars(erargs).items():
|
||||||
|
if type(player_settings) == dict:
|
||||||
|
if all(type(value) != list for value in player_settings.values()):
|
||||||
|
if len(player_settings.values()) > 1:
|
||||||
|
important[option] = {player: value for player, value in player_settings.items() if
|
||||||
|
player <= args.yaml_output}
|
||||||
|
else:
|
||||||
|
logging.debug(f"No player settings defined for option '{option}'")
|
||||||
|
|
||||||
|
else:
|
||||||
|
if player_settings != "": # is not empty name
|
||||||
|
important[option] = player_settings
|
||||||
|
else:
|
||||||
|
logging.debug(f"No player settings defined for option '{option}'")
|
||||||
|
if args.outputpath:
|
||||||
|
os.makedirs(args.outputpath, exist_ok=True)
|
||||||
|
with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f:
|
||||||
|
yaml.dump(important, f)
|
||||||
|
|
||||||
return erargs, seed
|
return erargs, seed
|
||||||
|
|
||||||
|
|
||||||
@@ -491,7 +511,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
|||||||
continue
|
continue
|
||||||
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
|
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
|
||||||
if PlandoOptions.items in plando_options:
|
if PlandoOptions.items in plando_options:
|
||||||
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
|
ret.plando_items = game_weights.get("plando_items", [])
|
||||||
if ret.game == "A Link to the Past":
|
if ret.game == "A Link to the Past":
|
||||||
roll_alttp_settings(ret, game_weights)
|
roll_alttp_settings(ret, game_weights)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
if __name__ == '__main__':
|
|
||||||
import ModuleUpdate
|
|
||||||
ModuleUpdate.update()
|
|
||||||
|
|
||||||
import Utils
|
|
||||||
Utils.init_logging("KH1Client", exception_logger="Client")
|
|
||||||
|
|
||||||
from worlds.kh1.Client import launch
|
|
||||||
launch()
|
|
||||||
105
Launcher.py
105
Launcher.py
@@ -16,11 +16,10 @@ import multiprocessing
|
|||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import urllib.parse
|
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from typing import Callable, Optional, Sequence, Tuple, Union
|
from typing import Callable, Sequence, Union, Optional
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
import settings
|
import settings
|
||||||
@@ -108,81 +107,7 @@ components.extend([
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
def identify(path: Union[None, str]):
|
||||||
url = urllib.parse.urlparse(path)
|
|
||||||
queries = urllib.parse.parse_qs(url.query)
|
|
||||||
launch_args = (path, *launch_args)
|
|
||||||
client_component = None
|
|
||||||
text_client_component = None
|
|
||||||
if "game" in queries:
|
|
||||||
game = queries["game"][0]
|
|
||||||
else: # TODO around 0.6.0 - this is for pre this change webhost uri's
|
|
||||||
game = "Archipelago"
|
|
||||||
for component in components:
|
|
||||||
if component.supports_uri and component.game_name == game:
|
|
||||||
client_component = component
|
|
||||||
elif component.display_name == "Text Client":
|
|
||||||
text_client_component = component
|
|
||||||
|
|
||||||
from kvui import App, Button, BoxLayout, Label, Clock, Window
|
|
||||||
|
|
||||||
class Popup(App):
|
|
||||||
timer_label: Label
|
|
||||||
remaining_time: Optional[int]
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.title = "Connect to Multiworld"
|
|
||||||
self.icon = r"data/icon.png"
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def build(self):
|
|
||||||
layout = BoxLayout(orientation="vertical")
|
|
||||||
|
|
||||||
if client_component is None:
|
|
||||||
self.remaining_time = 7
|
|
||||||
label_text = (f"A game client able to parse URIs was not detected for {game}.\n"
|
|
||||||
f"Launching Text Client in 7 seconds...")
|
|
||||||
self.timer_label = Label(text=label_text)
|
|
||||||
layout.add_widget(self.timer_label)
|
|
||||||
Clock.schedule_interval(self.update_label, 1)
|
|
||||||
else:
|
|
||||||
layout.add_widget(Label(text="Select client to open and connect with."))
|
|
||||||
button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4))
|
|
||||||
|
|
||||||
text_client_button = Button(
|
|
||||||
text=text_client_component.display_name,
|
|
||||||
on_release=lambda *args: run_component(text_client_component, *launch_args)
|
|
||||||
)
|
|
||||||
button_row.add_widget(text_client_button)
|
|
||||||
|
|
||||||
game_client_button = Button(
|
|
||||||
text=client_component.display_name,
|
|
||||||
on_release=lambda *args: run_component(client_component, *launch_args)
|
|
||||||
)
|
|
||||||
button_row.add_widget(game_client_button)
|
|
||||||
|
|
||||||
layout.add_widget(button_row)
|
|
||||||
|
|
||||||
return layout
|
|
||||||
|
|
||||||
def update_label(self, dt):
|
|
||||||
if self.remaining_time > 1:
|
|
||||||
# countdown the timer and string replace the number
|
|
||||||
self.remaining_time -= 1
|
|
||||||
self.timer_label.text = self.timer_label.text.replace(
|
|
||||||
str(self.remaining_time + 1), str(self.remaining_time)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# our timer is finished so launch text client and close down
|
|
||||||
run_component(text_client_component, *launch_args)
|
|
||||||
Clock.unschedule(self.update_label)
|
|
||||||
App.get_running_app().stop()
|
|
||||||
Window.close()
|
|
||||||
|
|
||||||
Popup().run()
|
|
||||||
|
|
||||||
|
|
||||||
def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]:
|
|
||||||
if path is None:
|
if path is None:
|
||||||
return None, None
|
return None, None
|
||||||
for component in components:
|
for component in components:
|
||||||
@@ -341,7 +266,7 @@ def run_gui():
|
|||||||
if file and component:
|
if file and component:
|
||||||
run_component(component, file)
|
run_component(component, file)
|
||||||
else:
|
else:
|
||||||
logging.warning(f"unable to identify component for {file}")
|
logging.warning(f"unable to identify component for {filename}")
|
||||||
|
|
||||||
def _stop(self, *largs):
|
def _stop(self, *largs):
|
||||||
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
|
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
|
||||||
@@ -374,24 +299,20 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
|||||||
elif not args:
|
elif not args:
|
||||||
args = {}
|
args = {}
|
||||||
|
|
||||||
path = args.get("Patch|Game|Component|url", None)
|
if args.get("Patch|Game|Component", None) is not None:
|
||||||
if path is not None:
|
file, component = identify(args["Patch|Game|Component"])
|
||||||
if path.startswith("archipelago://"):
|
|
||||||
handle_uri(path, args.get("args", ()))
|
|
||||||
return
|
|
||||||
file, component = identify(path)
|
|
||||||
if file:
|
if file:
|
||||||
args['file'] = file
|
args['file'] = file
|
||||||
if component:
|
if component:
|
||||||
args['component'] = component
|
args['component'] = component
|
||||||
if not component:
|
if not component:
|
||||||
logging.warning(f"Could not identify Component responsible for {path}")
|
logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}")
|
||||||
|
|
||||||
if args["update_settings"]:
|
if args["update_settings"]:
|
||||||
update_settings()
|
update_settings()
|
||||||
if "file" in args:
|
if 'file' in args:
|
||||||
run_component(args["component"], args["file"], *args["args"])
|
run_component(args["component"], args["file"], *args["args"])
|
||||||
elif "component" in args:
|
elif 'component' in args:
|
||||||
run_component(args["component"], *args["args"])
|
run_component(args["component"], *args["args"])
|
||||||
elif not args["update_settings"]:
|
elif not args["update_settings"]:
|
||||||
run_gui()
|
run_gui()
|
||||||
@@ -401,16 +322,12 @@ if __name__ == '__main__':
|
|||||||
init_logging('Launcher')
|
init_logging('Launcher')
|
||||||
Utils.freeze_support()
|
Utils.freeze_support()
|
||||||
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
|
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(description='Archipelago Launcher')
|
||||||
description='Archipelago Launcher',
|
|
||||||
usage="[-h] [--update_settings] [Patch|Game|Component] [-- component args here]"
|
|
||||||
)
|
|
||||||
run_group = parser.add_argument_group("Run")
|
run_group = parser.add_argument_group("Run")
|
||||||
run_group.add_argument("--update_settings", action="store_true",
|
run_group.add_argument("--update_settings", action="store_true",
|
||||||
help="Update host.yaml and exit.")
|
help="Update host.yaml and exit.")
|
||||||
run_group.add_argument("Patch|Game|Component|url", type=str, nargs="?",
|
run_group.add_argument("Patch|Game|Component", type=str, nargs="?",
|
||||||
help="Pass either a patch file, a generated game, the component name to run, or a url to "
|
help="Pass either a patch file, a generated game or the name of a component to run.")
|
||||||
"connect with.")
|
|
||||||
run_group.add_argument("args", nargs="*",
|
run_group.add_argument("args", nargs="*",
|
||||||
help="Arguments to pass to component.")
|
help="Arguments to pass to component.")
|
||||||
main(parser.parse_args())
|
main(parser.parse_args())
|
||||||
|
|||||||
@@ -467,8 +467,6 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
|
|
||||||
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
|
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
|
||||||
self.client = LinksAwakeningClient()
|
self.client = LinksAwakeningClient()
|
||||||
self.slot_data = {}
|
|
||||||
|
|
||||||
if magpie:
|
if magpie:
|
||||||
self.magpie_enabled = True
|
self.magpie_enabled = True
|
||||||
self.magpie = MagpieBridge()
|
self.magpie = MagpieBridge()
|
||||||
@@ -566,8 +564,6 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
def on_package(self, cmd: str, args: dict):
|
def on_package(self, cmd: str, args: dict):
|
||||||
if cmd == "Connected":
|
if cmd == "Connected":
|
||||||
self.game = self.slot_info[self.slot].game
|
self.game = self.slot_info[self.slot].game
|
||||||
self.slot_data = args.get("slot_data", {})
|
|
||||||
|
|
||||||
# TODO - use watcher_event
|
# TODO - use watcher_event
|
||||||
if cmd == "ReceivedItems":
|
if cmd == "ReceivedItems":
|
||||||
for index, item in enumerate(args["items"], start=args["index"]):
|
for index, item in enumerate(args["items"], start=args["index"]):
|
||||||
@@ -632,7 +628,6 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
self.magpie.set_checks(self.client.tracker.all_checks)
|
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||||
await self.magpie.set_item_tracker(self.client.item_tracker)
|
await self.magpie.set_item_tracker(self.client.item_tracker)
|
||||||
await self.magpie.send_gps(self.client.gps_tracker)
|
await self.magpie.send_gps(self.client.gps_tracker)
|
||||||
self.magpie.slot_data = self.slot_data
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# Don't let magpie errors take out the client
|
# Don't let magpie errors take out the client
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import tkinter as tk
|
|||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from concurrent.futures import as_completed, ThreadPoolExecutor
|
from concurrent.futures import as_completed, ThreadPoolExecutor
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, BOTH, TOP, LabelFrame, \
|
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, TOP, LabelFrame, \
|
||||||
IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
|
IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
|
||||||
from tkinter.constants import DISABLED, NORMAL
|
from tkinter.constants import DISABLED, NORMAL
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@@ -29,8 +29,7 @@ from Utils import output_path, local_path, user_path, open_file, get_cert_none_s
|
|||||||
|
|
||||||
|
|
||||||
GAME_ALTTP = "A Link to the Past"
|
GAME_ALTTP = "A Link to the Past"
|
||||||
WINDOW_MIN_HEIGHT = 525
|
|
||||||
WINDOW_MIN_WIDTH = 425
|
|
||||||
|
|
||||||
class AdjusterWorld(object):
|
class AdjusterWorld(object):
|
||||||
def __init__(self, sprite_pool):
|
def __init__(self, sprite_pool):
|
||||||
@@ -243,17 +242,16 @@ def adjustGUI():
|
|||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from Utils import __version__ as MWVersion
|
from Utils import __version__ as MWVersion
|
||||||
adjustWindow = Tk()
|
adjustWindow = Tk()
|
||||||
adjustWindow.minsize(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT)
|
|
||||||
adjustWindow.wm_title("Archipelago %s LttP Adjuster" % MWVersion)
|
adjustWindow.wm_title("Archipelago %s LttP Adjuster" % MWVersion)
|
||||||
set_icon(adjustWindow)
|
set_icon(adjustWindow)
|
||||||
|
|
||||||
rom_options_frame, rom_vars, set_sprite = get_rom_options_frame(adjustWindow)
|
rom_options_frame, rom_vars, set_sprite = get_rom_options_frame(adjustWindow)
|
||||||
|
|
||||||
bottomFrame2 = Frame(adjustWindow, padx=8, pady=2)
|
bottomFrame2 = Frame(adjustWindow)
|
||||||
|
|
||||||
romFrame, romVar = get_rom_frame(adjustWindow)
|
romFrame, romVar = get_rom_frame(adjustWindow)
|
||||||
|
|
||||||
romDialogFrame = Frame(adjustWindow, padx=8, pady=2)
|
romDialogFrame = Frame(adjustWindow)
|
||||||
baseRomLabel2 = Label(romDialogFrame, text='Rom to adjust')
|
baseRomLabel2 = Label(romDialogFrame, text='Rom to adjust')
|
||||||
romVar2 = StringVar()
|
romVar2 = StringVar()
|
||||||
romEntry2 = Entry(romDialogFrame, textvariable=romVar2)
|
romEntry2 = Entry(romDialogFrame, textvariable=romVar2)
|
||||||
@@ -263,9 +261,9 @@ def adjustGUI():
|
|||||||
romVar2.set(rom)
|
romVar2.set(rom)
|
||||||
|
|
||||||
romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2)
|
romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2)
|
||||||
romDialogFrame.pack(side=TOP, expand=False, fill=X)
|
romDialogFrame.pack(side=TOP, expand=True, fill=X)
|
||||||
baseRomLabel2.pack(side=LEFT, expand=False, fill=X, padx=(0, 8))
|
baseRomLabel2.pack(side=LEFT)
|
||||||
romEntry2.pack(side=LEFT, expand=True, fill=BOTH, pady=1)
|
romEntry2.pack(side=LEFT, expand=True, fill=X)
|
||||||
romSelectButton2.pack(side=LEFT)
|
romSelectButton2.pack(side=LEFT)
|
||||||
|
|
||||||
def adjustRom():
|
def adjustRom():
|
||||||
@@ -333,11 +331,12 @@ def adjustGUI():
|
|||||||
messagebox.showinfo(title="Success", message="Settings saved to persistent storage")
|
messagebox.showinfo(title="Success", message="Settings saved to persistent storage")
|
||||||
|
|
||||||
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
|
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
|
||||||
rom_options_frame.pack(side=TOP, padx=8, pady=8, fill=BOTH, expand=True)
|
rom_options_frame.pack(side=TOP)
|
||||||
adjustButton.pack(side=LEFT, padx=(5,5))
|
adjustButton.pack(side=LEFT, padx=(5,5))
|
||||||
|
|
||||||
saveButton = Button(bottomFrame2, text='Save Settings', command=saveGUISettings)
|
saveButton = Button(bottomFrame2, text='Save Settings', command=saveGUISettings)
|
||||||
saveButton.pack(side=LEFT, padx=(5,5))
|
saveButton.pack(side=LEFT, padx=(5,5))
|
||||||
|
|
||||||
bottomFrame2.pack(side=TOP, pady=(5,5))
|
bottomFrame2.pack(side=TOP, pady=(5,5))
|
||||||
|
|
||||||
tkinter_center_window(adjustWindow)
|
tkinter_center_window(adjustWindow)
|
||||||
@@ -577,7 +576,7 @@ class AttachTooltip(object):
|
|||||||
def get_rom_frame(parent=None):
|
def get_rom_frame(parent=None):
|
||||||
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
|
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
|
||||||
|
|
||||||
romFrame = Frame(parent, padx=8, pady=8)
|
romFrame = Frame(parent)
|
||||||
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
|
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
|
||||||
romVar = StringVar(value=adjuster_settings.baserom)
|
romVar = StringVar(value=adjuster_settings.baserom)
|
||||||
romEntry = Entry(romFrame, textvariable=romVar)
|
romEntry = Entry(romFrame, textvariable=romVar)
|
||||||
@@ -597,19 +596,20 @@ def get_rom_frame(parent=None):
|
|||||||
romSelectButton = Button(romFrame, text='Select Rom', command=RomSelect)
|
romSelectButton = Button(romFrame, text='Select Rom', command=RomSelect)
|
||||||
|
|
||||||
baseRomLabel.pack(side=LEFT)
|
baseRomLabel.pack(side=LEFT)
|
||||||
romEntry.pack(side=LEFT, expand=True, fill=BOTH, pady=1)
|
romEntry.pack(side=LEFT, expand=True, fill=X)
|
||||||
romSelectButton.pack(side=LEFT)
|
romSelectButton.pack(side=LEFT)
|
||||||
romFrame.pack(side=TOP, fill=X)
|
romFrame.pack(side=TOP, expand=True, fill=X)
|
||||||
|
|
||||||
return romFrame, romVar
|
return romFrame, romVar
|
||||||
|
|
||||||
def get_rom_options_frame(parent=None):
|
def get_rom_options_frame(parent=None):
|
||||||
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
|
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
|
||||||
|
|
||||||
romOptionsFrame = LabelFrame(parent, text="Rom options", padx=8, pady=8)
|
romOptionsFrame = LabelFrame(parent, text="Rom options")
|
||||||
|
romOptionsFrame.columnconfigure(0, weight=1)
|
||||||
|
romOptionsFrame.columnconfigure(1, weight=1)
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
romOptionsFrame.rowconfigure(i, weight=0, pad=4)
|
romOptionsFrame.rowconfigure(i, weight=1)
|
||||||
vars = Namespace()
|
vars = Namespace()
|
||||||
|
|
||||||
vars.MusicVar = IntVar()
|
vars.MusicVar = IntVar()
|
||||||
@@ -660,7 +660,7 @@ def get_rom_options_frame(parent=None):
|
|||||||
spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect)
|
spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect)
|
||||||
|
|
||||||
baseSpriteLabel.pack(side=LEFT)
|
baseSpriteLabel.pack(side=LEFT)
|
||||||
spriteEntry.pack(side=LEFT, expand=True, fill=X)
|
spriteEntry.pack(side=LEFT)
|
||||||
spriteSelectButton.pack(side=LEFT)
|
spriteSelectButton.pack(side=LEFT)
|
||||||
|
|
||||||
oofDialogFrame = Frame(romOptionsFrame)
|
oofDialogFrame = Frame(romOptionsFrame)
|
||||||
|
|||||||
25
Main.py
25
Main.py
@@ -11,8 +11,7 @@ from typing import Dict, List, Optional, Set, Tuple, Union
|
|||||||
|
|
||||||
import worlds
|
import worlds
|
||||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
|
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
|
||||||
from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, distribute_planned, \
|
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
|
||||||
flood_items
|
|
||||||
from Options import StartInventoryPool
|
from Options import StartInventoryPool
|
||||||
from Utils import __version__, output_path, version_tuple, get_settings
|
from Utils import __version__, output_path, version_tuple, get_settings
|
||||||
from settings import get_settings
|
from settings import get_settings
|
||||||
@@ -46,9 +45,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
multiworld.sprite_pool = args.sprite_pool.copy()
|
multiworld.sprite_pool = args.sprite_pool.copy()
|
||||||
|
|
||||||
multiworld.set_options(args)
|
multiworld.set_options(args)
|
||||||
if args.csv_output:
|
|
||||||
from Options import dump_player_options
|
|
||||||
dump_player_options(multiworld)
|
|
||||||
multiworld.set_item_links()
|
multiworld.set_item_links()
|
||||||
multiworld.state = CollectionState(multiworld)
|
multiworld.state = CollectionState(multiworld)
|
||||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed)
|
logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed)
|
||||||
@@ -104,7 +100,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
multiworld.early_items[player][item_name] = max(0, early-count)
|
multiworld.early_items[player][item_name] = max(0, early-count)
|
||||||
remaining_count = count-early
|
remaining_count = count-early
|
||||||
if remaining_count > 0:
|
if remaining_count > 0:
|
||||||
local_early = multiworld.local_early_items[player].get(item_name, 0)
|
local_early = multiworld.early_local_items[player].get(item_name, 0)
|
||||||
if local_early:
|
if local_early:
|
||||||
multiworld.early_items[player][item_name] = max(0, local_early - remaining_count)
|
multiworld.early_items[player][item_name] = max(0, local_early - remaining_count)
|
||||||
del local_early
|
del local_early
|
||||||
@@ -155,7 +151,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
# Because some worlds don't actually create items during create_items this has to be as late as possible.
|
# Because some worlds don't actually create items during create_items this has to be as late as possible.
|
||||||
if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids):
|
if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids):
|
||||||
new_items: List[Item] = []
|
new_items: List[Item] = []
|
||||||
old_items: List[Item] = []
|
|
||||||
depletion_pool: Dict[int, Dict[str, int]] = {
|
depletion_pool: Dict[int, Dict[str, int]] = {
|
||||||
player: getattr(multiworld.worlds[player].options,
|
player: getattr(multiworld.worlds[player].options,
|
||||||
"start_inventory_from_pool",
|
"start_inventory_from_pool",
|
||||||
@@ -174,24 +169,20 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
depletion_pool[item.player][item.name] -= 1
|
depletion_pool[item.player][item.name] -= 1
|
||||||
# quick abort if we have found all items
|
# quick abort if we have found all items
|
||||||
if not target:
|
if not target:
|
||||||
old_items.extend(multiworld.itempool[i+1:])
|
new_items.extend(multiworld.itempool[i+1:])
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
old_items.append(item)
|
new_items.append(item)
|
||||||
|
|
||||||
# leftovers?
|
# leftovers?
|
||||||
if target:
|
if target:
|
||||||
for player, remaining_items in depletion_pool.items():
|
for player, remaining_items in depletion_pool.items():
|
||||||
remaining_items = {name: count for name, count in remaining_items.items() if count}
|
remaining_items = {name: count for name, count in remaining_items.items() if count}
|
||||||
if remaining_items:
|
if remaining_items:
|
||||||
logger.warning(f"{multiworld.get_player_name(player)}"
|
raise Exception(f"{multiworld.get_player_name(player)}"
|
||||||
f" is trying to remove items from their pool that don't exist: {remaining_items}")
|
f" is trying to remove items from their pool that don't exist: {remaining_items}")
|
||||||
# find all filler we generated for the current player and remove until it matches
|
assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change."
|
||||||
removables = [item for item in new_items if item.player == player]
|
multiworld.itempool[:] = new_items
|
||||||
for _ in range(sum(remaining_items.values())):
|
|
||||||
new_items.remove(removables.pop())
|
|
||||||
assert len(multiworld.itempool) == len(new_items + old_items), "Item Pool amounts should not change."
|
|
||||||
multiworld.itempool[:] = new_items + old_items
|
|
||||||
|
|
||||||
multiworld.link_items()
|
multiworld.link_items()
|
||||||
|
|
||||||
@@ -350,7 +341,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
output_file_futures.append(pool.submit(write_multidata))
|
output_file_futures.append(pool.submit(write_multidata))
|
||||||
if not check_accessibility_task.result():
|
if not check_accessibility_task.result():
|
||||||
if not multiworld.can_beat_game():
|
if not multiworld.can_beat_game():
|
||||||
raise FillError("Game appears as unbeatable. Aborting.", multiworld=multiworld)
|
raise Exception("Game appears as unbeatable. Aborting.")
|
||||||
else:
|
else:
|
||||||
logger.warning("Location Accessibility requirements not fulfilled.")
|
logger.warning("Location Accessibility requirements not fulfilled.")
|
||||||
|
|
||||||
|
|||||||
@@ -75,13 +75,13 @@ def update(yes: bool = False, force: bool = False) -> None:
|
|||||||
if not update_ran:
|
if not update_ran:
|
||||||
update_ran = True
|
update_ran = True
|
||||||
|
|
||||||
install_pkg_resources(yes=yes)
|
|
||||||
import pkg_resources
|
|
||||||
|
|
||||||
if force:
|
if force:
|
||||||
update_command()
|
update_command()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
install_pkg_resources(yes=yes)
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
prev = "" # if a line ends in \ we store here and merge later
|
prev = "" # if a line ends in \ we store here and merge later
|
||||||
for req_file in requirements_files:
|
for req_file in requirements_files:
|
||||||
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
|
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
|
||||||
|
|||||||
@@ -67,21 +67,6 @@ def update_dict(dictionary, entries):
|
|||||||
return dictionary
|
return dictionary
|
||||||
|
|
||||||
|
|
||||||
def queue_gc():
|
|
||||||
import gc
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
gc_thread: typing.Optional[Thread] = getattr(queue_gc, "_thread", None)
|
|
||||||
def async_collect():
|
|
||||||
time.sleep(2)
|
|
||||||
setattr(queue_gc, "_thread", None)
|
|
||||||
gc.collect()
|
|
||||||
if not gc_thread:
|
|
||||||
gc_thread = Thread(target=async_collect)
|
|
||||||
setattr(queue_gc, "_thread", gc_thread)
|
|
||||||
gc_thread.start()
|
|
||||||
|
|
||||||
|
|
||||||
# functions callable on storable data on the server by clients
|
# functions callable on storable data on the server by clients
|
||||||
modify_functions = {
|
modify_functions = {
|
||||||
# generic:
|
# generic:
|
||||||
@@ -566,9 +551,6 @@ class Context:
|
|||||||
self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
|
self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
|
||||||
else:
|
else:
|
||||||
self.save_dirty = False
|
self.save_dirty = False
|
||||||
if not atexit_save: # if atexit is used, that keeps a reference anyway
|
|
||||||
queue_gc()
|
|
||||||
|
|
||||||
self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True)
|
self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True)
|
||||||
self.auto_saver_thread.start()
|
self.auto_saver_thread.start()
|
||||||
|
|
||||||
@@ -1009,7 +991,7 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
|
|||||||
collect_player(ctx, team, group, True)
|
collect_player(ctx, team, group, True)
|
||||||
|
|
||||||
|
|
||||||
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[typing.Tuple[int, int]]:
|
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||||
return ctx.locations.get_remaining(ctx.location_checks, team, slot)
|
return ctx.locations.get_remaining(ctx.location_checks, team, slot)
|
||||||
|
|
||||||
|
|
||||||
@@ -1221,10 +1203,6 @@ class CommonCommandProcessor(CommandProcessor):
|
|||||||
timer = int(seconds, 10)
|
timer = int(seconds, 10)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
timer = 10
|
timer = 10
|
||||||
else:
|
|
||||||
if timer > 60 * 60:
|
|
||||||
raise ValueError(f"{timer} is invalid. Maximum is 1 hour.")
|
|
||||||
|
|
||||||
async_start(countdown(self.ctx, timer))
|
async_start(countdown(self.ctx, timer))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -1372,10 +1350,10 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
def _cmd_remaining(self) -> bool:
|
def _cmd_remaining(self) -> bool:
|
||||||
"""List remaining items in your game, but not their location or recipient"""
|
"""List remaining items in your game, but not their location or recipient"""
|
||||||
if self.ctx.remaining_mode == "enabled":
|
if self.ctx.remaining_mode == "enabled":
|
||||||
rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot)
|
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||||
if rest_locations:
|
if remaining_item_ids:
|
||||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id]
|
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id]
|
||||||
for slot, item_id in rest_locations))
|
for item_id in remaining_item_ids))
|
||||||
else:
|
else:
|
||||||
self.output("No remaining items found.")
|
self.output("No remaining items found.")
|
||||||
return True
|
return True
|
||||||
@@ -1385,10 +1363,10 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
return False
|
return False
|
||||||
else: # is goal
|
else: # is goal
|
||||||
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
||||||
rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot)
|
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||||
if rest_locations:
|
if remaining_item_ids:
|
||||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id]
|
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id]
|
||||||
for slot, item_id in rest_locations))
|
for item_id in remaining_item_ids))
|
||||||
else:
|
else:
|
||||||
self.output("No remaining items found.")
|
self.output("No remaining items found.")
|
||||||
return True
|
return True
|
||||||
@@ -2061,8 +2039,6 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||||||
item_name, usable, response = get_intended_text(item_name, names)
|
item_name, usable, response = get_intended_text(item_name, names)
|
||||||
if usable:
|
if usable:
|
||||||
amount: int = int(amount)
|
amount: int = int(amount)
|
||||||
if amount > 100:
|
|
||||||
raise ValueError(f"{amount} is invalid. Maximum is 100.")
|
|
||||||
new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))]
|
new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))]
|
||||||
send_items_to(self.ctx, team, slot, *new_items)
|
send_items_to(self.ctx, team, slot, *new_items)
|
||||||
|
|
||||||
|
|||||||
12
NetUtils.py
12
NetUtils.py
@@ -79,7 +79,6 @@ class NetworkItem(typing.NamedTuple):
|
|||||||
item: int
|
item: int
|
||||||
location: int
|
location: int
|
||||||
player: int
|
player: int
|
||||||
""" Sending player, except in LocationInfo (from LocationScouts), where it is the receiving player. """
|
|
||||||
flags: int = 0
|
flags: int = 0
|
||||||
|
|
||||||
|
|
||||||
@@ -273,8 +272,7 @@ class RawJSONtoTextParser(JSONtoTextParser):
|
|||||||
|
|
||||||
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
||||||
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
|
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
|
||||||
'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47,
|
'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
|
||||||
'plum': 35, 'slateblue': 34, 'salmon': 31,} # convert ui colors to terminal colors
|
|
||||||
|
|
||||||
|
|
||||||
def color_code(*args):
|
def color_code(*args):
|
||||||
@@ -399,12 +397,12 @@ class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tu
|
|||||||
location_id not in checked]
|
location_id not in checked]
|
||||||
|
|
||||||
def get_remaining(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int
|
def get_remaining(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int
|
||||||
) -> typing.List[typing.Tuple[int, int]]:
|
) -> typing.List[int]:
|
||||||
checked = state[team, slot]
|
checked = state[team, slot]
|
||||||
player_locations = self[slot]
|
player_locations = self[slot]
|
||||||
return sorted([(player_locations[location_id][1], player_locations[location_id][0]) for
|
return sorted([player_locations[location_id][0] for
|
||||||
location_id in player_locations if
|
location_id in player_locations if
|
||||||
location_id not in checked])
|
location_id not in checked])
|
||||||
|
|
||||||
|
|
||||||
if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub
|
if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub
|
||||||
|
|||||||
81
Options.py
81
Options.py
@@ -8,17 +8,16 @@ import numbers
|
|||||||
import random
|
import random
|
||||||
import typing
|
import typing
|
||||||
import enum
|
import enum
|
||||||
from collections import defaultdict
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from schema import And, Optional, Or, Schema
|
from schema import And, Optional, Or, Schema
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from Utils import get_fuzzy_results, is_iterable_except_str, output_path
|
from Utils import get_fuzzy_results, is_iterable_except_str
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from BaseClasses import MultiWorld, PlandoOptions
|
from BaseClasses import PlandoOptions
|
||||||
from worlds.AutoWorld import World
|
from worlds.AutoWorld import World
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
@@ -974,19 +973,7 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
|
|||||||
if random.random() < float(text.get("percentage", 100)/100):
|
if random.random() < float(text.get("percentage", 100)/100):
|
||||||
at = text.get("at", None)
|
at = text.get("at", None)
|
||||||
if at is not None:
|
if at is not None:
|
||||||
if isinstance(at, dict):
|
|
||||||
if at:
|
|
||||||
at = random.choices(list(at.keys()),
|
|
||||||
weights=list(at.values()), k=1)[0]
|
|
||||||
else:
|
|
||||||
raise OptionError("\"at\" must be a valid string or weighted list of strings!")
|
|
||||||
given_text = text.get("text", [])
|
given_text = text.get("text", [])
|
||||||
if isinstance(given_text, dict):
|
|
||||||
if not given_text:
|
|
||||||
given_text = []
|
|
||||||
else:
|
|
||||||
given_text = random.choices(list(given_text.keys()),
|
|
||||||
weights=list(given_text.values()), k=1)
|
|
||||||
if isinstance(given_text, str):
|
if isinstance(given_text, str):
|
||||||
given_text = [given_text]
|
given_text = [given_text]
|
||||||
texts.append(PlandoText(
|
texts.append(PlandoText(
|
||||||
@@ -994,8 +981,6 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
|
|||||||
given_text,
|
given_text,
|
||||||
text.get("percentage", 100)
|
text.get("percentage", 100)
|
||||||
))
|
))
|
||||||
else:
|
|
||||||
raise OptionError("\"at\" must be a valid string or weighted list of strings!")
|
|
||||||
elif isinstance(text, PlandoText):
|
elif isinstance(text, PlandoText):
|
||||||
if random.random() < float(text.percentage/100):
|
if random.random() < float(text.percentage/100):
|
||||||
texts.append(text)
|
texts.append(text)
|
||||||
@@ -1251,7 +1236,6 @@ class CommonOptions(metaclass=OptionsMetaProperty):
|
|||||||
:param option_names: names of the options to return
|
:param option_names: names of the options to return
|
||||||
:param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`
|
:param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`
|
||||||
"""
|
"""
|
||||||
assert option_names, "options.as_dict() was used without any option names."
|
|
||||||
option_results = {}
|
option_results = {}
|
||||||
for option_name in option_names:
|
for option_name in option_names:
|
||||||
if option_name in type(self).type_hints:
|
if option_name in type(self).type_hints:
|
||||||
@@ -1336,7 +1320,7 @@ class PriorityLocations(LocationSet):
|
|||||||
|
|
||||||
|
|
||||||
class DeathLink(Toggle):
|
class DeathLink(Toggle):
|
||||||
"""When you die, everyone who enabled death link dies. Of course, the reverse is true too."""
|
"""When you die, everyone dies. Of course the reverse is true too."""
|
||||||
display_name = "Death Link"
|
display_name = "Death Link"
|
||||||
rich_text_doc = True
|
rich_text_doc = True
|
||||||
|
|
||||||
@@ -1535,40 +1519,29 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
|||||||
f.write(res)
|
f.write(res)
|
||||||
|
|
||||||
|
|
||||||
def dump_player_options(multiworld: MultiWorld) -> None:
|
if __name__ == "__main__":
|
||||||
from csv import DictWriter
|
|
||||||
|
|
||||||
game_players = defaultdict(list)
|
from worlds.alttp.Options import Logic
|
||||||
for player, game in multiworld.game.items():
|
import argparse
|
||||||
game_players[game].append(player)
|
|
||||||
game_players = dict(sorted(game_players.items()))
|
|
||||||
|
|
||||||
output = []
|
map_shuffle = Toggle
|
||||||
per_game_option_names = [
|
compass_shuffle = Toggle
|
||||||
getattr(option, "display_name", option_key)
|
key_shuffle = Toggle
|
||||||
for option_key, option in PerGameCommonOptions.type_hints.items()
|
big_key_shuffle = Toggle
|
||||||
]
|
hints = Toggle
|
||||||
all_option_names = per_game_option_names.copy()
|
test = argparse.Namespace()
|
||||||
for game, players in game_players.items():
|
test.logic = Logic.from_text("no_logic")
|
||||||
game_option_names = per_game_option_names.copy()
|
test.map_shuffle = map_shuffle.from_text("ON")
|
||||||
for player in players:
|
test.hints = hints.from_text('OFF')
|
||||||
world = multiworld.worlds[player]
|
try:
|
||||||
player_output = {
|
test.logic = Logic.from_text("overworld_glitches_typo")
|
||||||
"Game": multiworld.game[player],
|
except KeyError as e:
|
||||||
"Name": multiworld.get_player_name(player),
|
print(e)
|
||||||
}
|
try:
|
||||||
output.append(player_output)
|
test.logic_owg = Logic.from_text("owg")
|
||||||
for option_key, option in world.options_dataclass.type_hints.items():
|
except KeyError as e:
|
||||||
if issubclass(Removed, option):
|
print(e)
|
||||||
continue
|
if test.map_shuffle:
|
||||||
display_name = getattr(option, "display_name", option_key)
|
print("map_shuffle is on")
|
||||||
player_output[display_name] = getattr(world.options, option_key).current_option_name
|
print(f"Hints are {bool(test.hints)}")
|
||||||
if display_name not in game_option_names:
|
print(test)
|
||||||
all_option_names.append(display_name)
|
|
||||||
game_option_names.append(display_name)
|
|
||||||
|
|
||||||
with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file:
|
|
||||||
fields = ["Game", "Name", *all_option_names]
|
|
||||||
writer = DictWriter(file, fields)
|
|
||||||
writer.writeheader()
|
|
||||||
writer.writerows(output)
|
|
||||||
|
|||||||
@@ -72,10 +72,6 @@ Currently, the following games are supported:
|
|||||||
* Aquaria
|
* Aquaria
|
||||||
* Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
|
* Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
|
||||||
* A Hat in Time
|
* A Hat in Time
|
||||||
* Old School Runescape
|
|
||||||
* Kingdom Hearts 1
|
|
||||||
* Mega Man 2
|
|
||||||
* Yacht Dice
|
|
||||||
|
|
||||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
For setup and instructions check out our [tutorials page](https://archipelago.gg/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
|
||||||
|
|||||||
2
Utils.py
2
Utils.py
@@ -46,7 +46,7 @@ class Version(typing.NamedTuple):
|
|||||||
return ".".join(str(item) for item in self)
|
return ".".join(str(item) for item in self)
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.5.1"
|
__version__ = "0.5.0"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
is_linux = sys.platform.startswith("linux")
|
is_linux = sys.platform.startswith("linux")
|
||||||
|
|||||||
10
WebHost.py
10
WebHost.py
@@ -1,4 +1,3 @@
|
|||||||
import argparse
|
|
||||||
import os
|
import os
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import logging
|
import logging
|
||||||
@@ -32,15 +31,6 @@ def get_app() -> "Flask":
|
|||||||
import yaml
|
import yaml
|
||||||
app.config.from_file(configpath, yaml.safe_load)
|
app.config.from_file(configpath, yaml.safe_load)
|
||||||
logging.info(f"Updated config from {configpath}")
|
logging.info(f"Updated config from {configpath}")
|
||||||
# inside get_app() so it's usable in systems like gunicorn, which do not run WebHost.py, but import it.
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument('--config_override', default=None,
|
|
||||||
help="Path to yaml config file that overrules config.yaml.")
|
|
||||||
args = parser.parse_known_args()[0]
|
|
||||||
if args.config_override:
|
|
||||||
import yaml
|
|
||||||
app.config.from_file(os.path.abspath(args.config_override), yaml.safe_load)
|
|
||||||
logging.info(f"Updated config from {args.config_override}")
|
|
||||||
if not app.config["HOST_ADDRESS"]:
|
if not app.config["HOST_ADDRESS"]:
|
||||||
logging.info("Getting public IP, as HOST_ADDRESS is empty.")
|
logging.info("Getting public IP, as HOST_ADDRESS is empty.")
|
||||||
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
|
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
|
||||||
|
|||||||
@@ -1,15 +1,51 @@
|
|||||||
"""API endpoints package."""
|
"""API endpoints package."""
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from flask import Blueprint
|
from flask import Blueprint, abort, url_for
|
||||||
|
|
||||||
from ..models import Seed
|
import worlds.Files
|
||||||
|
from ..models import Room, Seed
|
||||||
|
|
||||||
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
|
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
|
||||||
|
|
||||||
|
# unsorted/misc endpoints
|
||||||
|
|
||||||
|
|
||||||
def get_players(seed: Seed) -> List[Tuple[str, str]]:
|
def get_players(seed: Seed) -> List[Tuple[str, str]]:
|
||||||
return [(slot.player_name, slot.game) for slot in seed.slots]
|
return [(slot.player_name, slot.game) for slot in seed.slots]
|
||||||
|
|
||||||
|
|
||||||
from . import datapackage, generate, room, user # trigger registration
|
@api_endpoints.route('/room_status/<suuid:room>')
|
||||||
|
def room_info(room: UUID):
|
||||||
|
room = Room.get(id=room)
|
||||||
|
if room is None:
|
||||||
|
return abort(404)
|
||||||
|
|
||||||
|
def supports_apdeltapatch(game: str):
|
||||||
|
return game in worlds.Files.AutoPatchRegister.patch_types
|
||||||
|
downloads = []
|
||||||
|
for slot in sorted(room.seed.slots):
|
||||||
|
if slot.data and not supports_apdeltapatch(slot.game):
|
||||||
|
slot_download = {
|
||||||
|
"slot": slot.player_id,
|
||||||
|
"download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id)
|
||||||
|
}
|
||||||
|
downloads.append(slot_download)
|
||||||
|
elif slot.data:
|
||||||
|
slot_download = {
|
||||||
|
"slot": slot.player_id,
|
||||||
|
"download": url_for("download_patch", patch_id=slot.id, room_id=room.id)
|
||||||
|
}
|
||||||
|
downloads.append(slot_download)
|
||||||
|
return {
|
||||||
|
"tracker": room.tracker,
|
||||||
|
"players": get_players(room.seed),
|
||||||
|
"last_port": room.last_port,
|
||||||
|
"last_activity": room.last_activity,
|
||||||
|
"timeout": room.timeout,
|
||||||
|
"downloads": downloads,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
from . import generate, user, datapackage # trigger registration
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
from typing import Any, Dict
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from flask import abort, url_for
|
|
||||||
|
|
||||||
import worlds.Files
|
|
||||||
from . import api_endpoints, get_players
|
|
||||||
from ..models import Room
|
|
||||||
|
|
||||||
|
|
||||||
@api_endpoints.route('/room_status/<suuid:room_id>')
|
|
||||||
def room_info(room_id: UUID) -> Dict[str, Any]:
|
|
||||||
room = Room.get(id=room_id)
|
|
||||||
if room is None:
|
|
||||||
return abort(404)
|
|
||||||
|
|
||||||
def supports_apdeltapatch(game: str) -> bool:
|
|
||||||
return game in worlds.Files.AutoPatchRegister.patch_types
|
|
||||||
|
|
||||||
downloads = []
|
|
||||||
for slot in sorted(room.seed.slots):
|
|
||||||
if slot.data and not supports_apdeltapatch(slot.game):
|
|
||||||
slot_download = {
|
|
||||||
"slot": slot.player_id,
|
|
||||||
"download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id)
|
|
||||||
}
|
|
||||||
downloads.append(slot_download)
|
|
||||||
elif slot.data:
|
|
||||||
slot_download = {
|
|
||||||
"slot": slot.player_id,
|
|
||||||
"download": url_for("download_patch", patch_id=slot.id, room_id=room.id)
|
|
||||||
}
|
|
||||||
downloads.append(slot_download)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"tracker": room.tracker,
|
|
||||||
"players": get_players(room.seed),
|
|
||||||
"last_port": room.last_port,
|
|
||||||
"last_activity": room.last_activity,
|
|
||||||
"timeout": room.timeout,
|
|
||||||
"downloads": downloads,
|
|
||||||
}
|
|
||||||
@@ -72,14 +72,6 @@ class WebHostContext(Context):
|
|||||||
self.video = {}
|
self.video = {}
|
||||||
self.tags = ["AP", "WebHost"]
|
self.tags = ["AP", "WebHost"]
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
try:
|
|
||||||
import psutil
|
|
||||||
from Utils import format_SI_prefix
|
|
||||||
self.logger.debug(f"Context destroyed, Mem: {format_SI_prefix(psutil.Process().memory_info().rss, 1024)}iB")
|
|
||||||
except ImportError:
|
|
||||||
self.logger.debug("Context destroyed")
|
|
||||||
|
|
||||||
def _load_game_data(self):
|
def _load_game_data(self):
|
||||||
for key, value in self.static_server_data.items():
|
for key, value in self.static_server_data.items():
|
||||||
# NOTE: attributes are mutable and shared, so they will have to be copied before being modified
|
# NOTE: attributes are mutable and shared, so they will have to be copied before being modified
|
||||||
@@ -257,7 +249,6 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
|||||||
ctx = WebHostContext(static_server_data, logger)
|
ctx = WebHostContext(static_server_data, logger)
|
||||||
ctx.load(room_id)
|
ctx.load(room_id)
|
||||||
ctx.init_save()
|
ctx.init_save()
|
||||||
assert ctx.server is None
|
|
||||||
try:
|
try:
|
||||||
ctx.server = websockets.serve(
|
ctx.server = websockets.serve(
|
||||||
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||||
@@ -288,7 +279,6 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
|||||||
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
||||||
if ctx.saving:
|
if ctx.saving:
|
||||||
setattr(asyncio.current_task(), "save", lambda: ctx._save(True))
|
setattr(asyncio.current_task(), "save", lambda: ctx._save(True))
|
||||||
assert ctx.shutdown_task is None
|
|
||||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
||||||
await ctx.shutdown_task
|
await ctx.shutdown_task
|
||||||
|
|
||||||
@@ -335,7 +325,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
|||||||
def run(self):
|
def run(self):
|
||||||
while 1:
|
while 1:
|
||||||
next_room = rooms_to_run.get(block=True, timeout=None)
|
next_room = rooms_to_run.get(block=True, timeout=None)
|
||||||
gc.collect()
|
gc.collect(0)
|
||||||
task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
|
task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
|
||||||
self._tasks.append(task)
|
self._tasks.append(task)
|
||||||
task.add_done_callback(self._done)
|
task.add_done_callback(self._done)
|
||||||
|
|||||||
@@ -134,7 +134,6 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
|
|||||||
{"bosses", "items", "connections", "texts"}))
|
{"bosses", "items", "connections", "texts"}))
|
||||||
erargs.skip_prog_balancing = False
|
erargs.skip_prog_balancing = False
|
||||||
erargs.skip_output = False
|
erargs.skip_output = False
|
||||||
erargs.csv_output = False
|
|
||||||
|
|
||||||
name_counter = Counter()
|
name_counter = Counter()
|
||||||
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
||||||
|
|||||||
@@ -132,41 +132,26 @@ def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]:
|
|||||||
return "Access Denied", 403
|
return "Access Denied", 403
|
||||||
|
|
||||||
|
|
||||||
@app.post("/room/<suuid:room>")
|
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
|
||||||
def host_room_command(room: UUID):
|
|
||||||
room: Room = Room.get(id=room)
|
|
||||||
if room is None:
|
|
||||||
return abort(404)
|
|
||||||
|
|
||||||
if room.owner == session["_id"]:
|
|
||||||
cmd = request.form["cmd"]
|
|
||||||
if cmd:
|
|
||||||
Command(room=room, commandtext=cmd)
|
|
||||||
commit()
|
|
||||||
return redirect(url_for("host_room", room=room.id))
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/room/<suuid:room>")
|
|
||||||
def host_room(room: UUID):
|
def host_room(room: UUID):
|
||||||
room: Room = Room.get(id=room)
|
room: Room = Room.get(id=room)
|
||||||
if room is None:
|
if room is None:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
|
if request.method == "POST":
|
||||||
|
if room.owner == session["_id"]:
|
||||||
|
cmd = request.form["cmd"]
|
||||||
|
if cmd:
|
||||||
|
Command(room=room, commandtext=cmd)
|
||||||
|
commit()
|
||||||
|
return redirect(url_for("host_room", room=room.id))
|
||||||
|
|
||||||
now = datetime.datetime.utcnow()
|
now = datetime.datetime.utcnow()
|
||||||
# indicate that the page should reload to get the assigned port
|
# indicate that the page should reload to get the assigned port
|
||||||
should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
|
should_refresh = not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3)
|
||||||
or room.last_activity < now - datetime.timedelta(seconds=room.timeout))
|
|
||||||
with db_session:
|
with db_session:
|
||||||
room.last_activity = now # will trigger a spinup, if it's not already running
|
room.last_activity = now # will trigger a spinup, if it's not already running
|
||||||
|
|
||||||
browser_tokens = "Mozilla", "Chrome", "Safari"
|
def get_log(max_size: int = 1024000) -> str:
|
||||||
automated = ("update" in request.args
|
|
||||||
or "Discordbot" in request.user_agent.string
|
|
||||||
or not any(browser_token in request.user_agent.string for browser_token in browser_tokens))
|
|
||||||
|
|
||||||
def get_log(max_size: int = 0 if automated else 1024000) -> str:
|
|
||||||
if max_size == 0:
|
|
||||||
return "…"
|
|
||||||
try:
|
try:
|
||||||
with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log:
|
with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log:
|
||||||
raw_size = 0
|
raw_size = 0
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
flask>=3.0.3
|
flask>=3.0.3
|
||||||
werkzeug>=3.0.4
|
werkzeug>=3.0.3
|
||||||
pony>=0.7.19
|
pony>=0.7.17
|
||||||
waitress>=3.0.0
|
waitress>=3.0.0
|
||||||
Flask-Caching>=2.3.0
|
Flask-Caching>=2.3.0
|
||||||
Flask-Compress>=1.15
|
Flask-Compress>=1.15
|
||||||
Flask-Limiter>=3.8.0
|
Flask-Limiter>=3.7.0
|
||||||
bokeh>=3.1.1; python_version <= '3.8'
|
bokeh>=3.1.1; python_version <= '3.8'
|
||||||
bokeh>=3.4.3; python_version == '3.9'
|
bokeh>=3.4.1; python_version >= '3.9'
|
||||||
bokeh>=3.5.2; python_version >= '3.10'
|
|
||||||
markupsafe>=2.1.5
|
markupsafe>=2.1.5
|
||||||
|
|||||||
@@ -77,4 +77,4 @@ There, you will find examples of games in the `worlds` folder:
|
|||||||
You may also find developer documentation in the `docs` folder:
|
You may also find developer documentation in the `docs` folder:
|
||||||
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
|
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
|
||||||
|
|
||||||
If you have more questions, feel free to ask in the **#ap-world-dev** channel on our Discord.
|
If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.
|
||||||
|
|||||||
@@ -58,28 +58,3 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader{
|
|
||||||
display: inline-block;
|
|
||||||
visibility: hidden;
|
|
||||||
margin-left: 5px;
|
|
||||||
width: 40px;
|
|
||||||
aspect-ratio: 4;
|
|
||||||
--_g: no-repeat radial-gradient(circle closest-side,#fff 90%,#fff0);
|
|
||||||
background:
|
|
||||||
var(--_g) 0 50%,
|
|
||||||
var(--_g) 50% 50%,
|
|
||||||
var(--_g) 100% 50%;
|
|
||||||
background-size: calc(100%/3) 100%;
|
|
||||||
animation: l7 1s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader.loading{
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes l7{
|
|
||||||
33%{background-size:calc(100%/3) 0% ,calc(100%/3) 100%,calc(100%/3) 100%}
|
|
||||||
50%{background-size:calc(100%/3) 100%,calc(100%/3) 0 ,calc(100%/3) 100%}
|
|
||||||
66%{background-size:calc(100%/3) 100%,calc(100%/3) 100%,calc(100%/3) 0 }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,30 +19,28 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
{% include 'header/grassHeader.html' %}
|
{% include 'header/grassHeader.html' %}
|
||||||
<div id="host-room">
|
<div id="host-room">
|
||||||
<span id="host-room-info">
|
{% if room.owner == session["_id"] %}
|
||||||
{% if room.owner == session["_id"] %}
|
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id|suuid }}</a>
|
||||||
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id|suuid }}</a>
|
<br />
|
||||||
<br />
|
{% endif %}
|
||||||
{% endif %}
|
{% if room.tracker %}
|
||||||
{% if room.tracker %}
|
This room has a <a href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}">Multiworld Tracker</a>
|
||||||
This room has a <a href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}">Multiworld Tracker</a>
|
and a <a href="{{ url_for("get_multiworld_sphere_tracker", tracker=room.tracker) }}">Sphere Tracker</a> enabled.
|
||||||
and a <a href="{{ url_for("get_multiworld_sphere_tracker", tracker=room.tracker) }}">Sphere Tracker</a> enabled.
|
<br />
|
||||||
<br />
|
{% endif %}
|
||||||
{% endif %}
|
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
|
||||||
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
|
Should you wish to continue later,
|
||||||
Should you wish to continue later,
|
anyone can simply refresh this page and the server will resume.<br>
|
||||||
anyone can simply refresh this page and the server will resume.<br>
|
{% if room.last_port == -1 %}
|
||||||
{% if room.last_port == -1 %}
|
There was an error hosting this Room. Another attempt will be made on refreshing this page.
|
||||||
There was an error hosting this Room. Another attempt will be made on refreshing this page.
|
The most likely failure reason is that the multiworld is too old to be loaded now.
|
||||||
The most likely failure reason is that the multiworld is too old to be loaded now.
|
{% elif room.last_port %}
|
||||||
{% elif room.last_port %}
|
You can connect to this room by using <span class="interactive"
|
||||||
You can connect to this room by using <span class="interactive"
|
data-tooltip="This means address/ip is {{ config['HOST_ADDRESS'] }} and port is {{ room.last_port }}.">
|
||||||
data-tooltip="This means address/ip is {{ config['HOST_ADDRESS'] }} and port is {{ room.last_port }}.">
|
'/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}'
|
||||||
'/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}'
|
</span>
|
||||||
</span>
|
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>
|
||||||
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
{{ macros.list_patches_room(room) }}
|
{{ macros.list_patches_room(room) }}
|
||||||
{% if room.owner == session["_id"] %}
|
{% if room.owner == session["_id"] %}
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
<label for="cmd"></label>
|
<label for="cmd"></label>
|
||||||
<input class="form-control" type="text" id="cmd" name="cmd"
|
<input class="form-control" type="text" id="cmd" name="cmd"
|
||||||
placeholder="Server Command. /help to list them, list gets appended to log.">
|
placeholder="Server Command. /help to list them, list gets appended to log.">
|
||||||
<span class="loader"></span>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<a href="{{ url_for("display_log", room=room.id) }}">
|
<a href="{{ url_for("display_log", room=room.id) }}">
|
||||||
@@ -65,7 +62,6 @@
|
|||||||
let url = '{{ url_for('display_log', room = room.id) }}';
|
let url = '{{ url_for('display_log', room = room.id) }}';
|
||||||
let bytesReceived = {{ log_len }};
|
let bytesReceived = {{ log_len }};
|
||||||
let updateLogTimeout;
|
let updateLogTimeout;
|
||||||
let updateLogImmediately = false;
|
|
||||||
let awaitingCommandResponse = false;
|
let awaitingCommandResponse = false;
|
||||||
let logger = document.getElementById("logger");
|
let logger = document.getElementById("logger");
|
||||||
|
|
||||||
@@ -82,36 +78,29 @@
|
|||||||
|
|
||||||
async function updateLog() {
|
async function updateLog() {
|
||||||
try {
|
try {
|
||||||
if (!document.hidden) {
|
let res = await fetch(url, {
|
||||||
updateLogImmediately = false;
|
headers: {
|
||||||
let res = await fetch(url, {
|
'Range': `bytes=${bytesReceived}-`,
|
||||||
headers: {
|
}
|
||||||
'Range': `bytes=${bytesReceived}-`,
|
});
|
||||||
}
|
if (res.ok) {
|
||||||
});
|
let text = await res.text();
|
||||||
if (res.ok) {
|
if (text.length > 0) {
|
||||||
let text = await res.text();
|
awaitingCommandResponse = false;
|
||||||
if (text.length > 0) {
|
if (bytesReceived === 0 || res.status !== 206) {
|
||||||
awaitingCommandResponse = false;
|
logger.innerHTML = '';
|
||||||
if (bytesReceived === 0 || res.status !== 206) {
|
}
|
||||||
logger.innerHTML = '';
|
if (res.status !== 206) {
|
||||||
}
|
bytesReceived = 0;
|
||||||
if (res.status !== 206) {
|
} else {
|
||||||
bytesReceived = 0;
|
bytesReceived += new Blob([text]).size;
|
||||||
} else {
|
}
|
||||||
bytesReceived += new Blob([text]).size;
|
if (logger.innerHTML.endsWith('…')) {
|
||||||
}
|
logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1);
|
||||||
if (logger.innerHTML.endsWith('…')) {
|
}
|
||||||
logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1);
|
logger.appendChild(document.createTextNode(text));
|
||||||
}
|
scrollToBottom(logger);
|
||||||
logger.appendChild(document.createTextNode(text));
|
|
||||||
scrollToBottom(logger);
|
|
||||||
let loader = document.getElementById("command-form").getElementsByClassName("loader")[0];
|
|
||||||
loader.classList.remove("loading");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
updateLogImmediately = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@@ -136,62 +125,20 @@
|
|||||||
});
|
});
|
||||||
ev.preventDefault(); // has to happen before first await
|
ev.preventDefault(); // has to happen before first await
|
||||||
form.reset();
|
form.reset();
|
||||||
let loader = form.getElementsByClassName("loader")[0];
|
let res = await req;
|
||||||
loader.classList.add("loading");
|
if (res.ok || res.type === 'opaqueredirect') {
|
||||||
try {
|
awaitingCommandResponse = true;
|
||||||
let res = await req;
|
window.clearTimeout(updateLogTimeout);
|
||||||
if (res.ok || res.type === 'opaqueredirect') {
|
updateLogTimeout = window.setTimeout(updateLog, 100);
|
||||||
awaitingCommandResponse = true;
|
} else {
|
||||||
window.clearTimeout(updateLogTimeout);
|
window.alert(res.statusText);
|
||||||
updateLogTimeout = window.setTimeout(updateLog, 100);
|
|
||||||
} else {
|
|
||||||
loader.classList.remove("loading");
|
|
||||||
window.alert(res.statusText);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
loader.classList.remove("loading");
|
|
||||||
window.alert(e.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("command-form").addEventListener("submit", postForm);
|
document.getElementById("command-form").addEventListener("submit", postForm);
|
||||||
updateLogTimeout = window.setTimeout(updateLog, 1000);
|
updateLogTimeout = window.setTimeout(updateLog, 1000);
|
||||||
logger.scrollTop = logger.scrollHeight;
|
logger.scrollTop = logger.scrollHeight;
|
||||||
document.addEventListener("visibilitychange", () => {
|
|
||||||
if (!document.hidden && updateLogImmediately) {
|
|
||||||
updateLog();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<script>
|
|
||||||
function updateInfo() {
|
|
||||||
let url = new URL(window.location.href);
|
|
||||||
url.search = "?update";
|
|
||||||
fetch(url)
|
|
||||||
.then(res => {
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`HTTP error ${res.status}`);
|
|
||||||
}
|
|
||||||
return res.text()
|
|
||||||
})
|
|
||||||
.then(text => new DOMParser().parseFromString(text, 'text/html'))
|
|
||||||
.then(newDocument => {
|
|
||||||
let el = newDocument.getElementById("host-room-info");
|
|
||||||
document.getElementById("host-room-info").innerHTML = el.innerHTML;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.querySelector("meta[http-equiv='refresh']")) {
|
|
||||||
console.log("Refresh!");
|
|
||||||
window.addEventListener('load', function () {
|
|
||||||
for (let i=0; i<3; i++) {
|
|
||||||
window.setTimeout(updateInfo, Math.pow(2, i) * 2000); // 2, 4, 8s
|
|
||||||
}
|
|
||||||
window.stop(); // cancel meta refresh
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ patch.player_id }}</td>
|
<td>{{ patch.player_id }}</td>
|
||||||
<td data-tooltip="Connect via Game Client"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}?game={{ patch.game }}&room={{ room.id | suuid }}">{{ patch.player_name }}</a></td>
|
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
|
||||||
<td>{{ patch.game }}</td>
|
<td>{{ patch.game }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if patch.data %}
|
{% if patch.data %}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
You have not generated any seeds yet!
|
You have no generated any seeds yet!
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
id="{{ option_name }}-{{ key }}"
|
id="{{ option_name }}-{{ key }}"
|
||||||
name="{{ option_name }}||{{ key }}"
|
name="{{ option_name }}||{{ key }}"
|
||||||
value="1"
|
value="1"
|
||||||
{{ "checked" if key in option.default }}
|
checked="{{ "checked" if key in option.default else "" }}"
|
||||||
/>
|
/>
|
||||||
<label for="{{ option_name }}-{{ key }}">
|
<label for="{{ option_name }}-{{ key }}">
|
||||||
{{ key }}
|
{{ key }}
|
||||||
|
|||||||
@@ -287,15 +287,15 @@ cdef class LocationStore:
|
|||||||
entry in self.entries[start:start + count] if
|
entry in self.entries[start:start + count] if
|
||||||
entry.location not in checked]
|
entry.location not in checked]
|
||||||
|
|
||||||
def get_remaining(self, state: State, team: int, slot: int) -> List[Tuple[int, int]]:
|
def get_remaining(self, state: State, team: int, slot: int) -> List[int]:
|
||||||
cdef LocationEntry* entry
|
cdef LocationEntry* entry
|
||||||
cdef ap_player_t sender = slot
|
cdef ap_player_t sender = slot
|
||||||
cdef size_t start = self.sender_index[sender].start
|
cdef size_t start = self.sender_index[sender].start
|
||||||
cdef size_t count = self.sender_index[sender].count
|
cdef size_t count = self.sender_index[sender].count
|
||||||
cdef set checked = state[team, slot]
|
cdef set checked = state[team, slot]
|
||||||
return sorted([(entry.receiver, entry.item) for
|
return sorted([entry.item for
|
||||||
entry in self.entries[start:start+count] if
|
entry in self.entries[start:start+count] if
|
||||||
entry.location not in checked])
|
entry.location not in checked])
|
||||||
|
|
||||||
|
|
||||||
@cython.auto_pickle(False)
|
@cython.auto_pickle(False)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
/worlds/clique/ @ThePhar
|
/worlds/clique/ @ThePhar
|
||||||
|
|
||||||
# Dark Souls III
|
# Dark Souls III
|
||||||
/worlds/dark_souls_3/ @Marechal-L @nex3
|
/worlds/dark_souls_3/ @Marechal-L
|
||||||
|
|
||||||
# Donkey Kong Country 3
|
# Donkey Kong Country 3
|
||||||
/worlds/dkc3/ @PoryGone
|
/worlds/dkc3/ @PoryGone
|
||||||
@@ -78,9 +78,6 @@
|
|||||||
# Kirby's Dream Land 3
|
# Kirby's Dream Land 3
|
||||||
/worlds/kdl3/ @Silvris
|
/worlds/kdl3/ @Silvris
|
||||||
|
|
||||||
# Kingdom Hearts
|
|
||||||
/worlds/kh1/ @gaithern
|
|
||||||
|
|
||||||
# Kingdom Hearts 2
|
# Kingdom Hearts 2
|
||||||
/worlds/kh2/ @JaredWeakStrike
|
/worlds/kh2/ @JaredWeakStrike
|
||||||
|
|
||||||
@@ -106,9 +103,6 @@
|
|||||||
# Minecraft
|
# Minecraft
|
||||||
/worlds/minecraft/ @KonoTyran @espeon65536
|
/worlds/minecraft/ @KonoTyran @espeon65536
|
||||||
|
|
||||||
# Mega Man 2
|
|
||||||
/worlds/mm2/ @Silvris
|
|
||||||
|
|
||||||
# MegaMan Battle Network 3
|
# MegaMan Battle Network 3
|
||||||
/worlds/mmbn3/ @digiholic
|
/worlds/mmbn3/ @digiholic
|
||||||
|
|
||||||
@@ -118,8 +112,8 @@
|
|||||||
# Noita
|
# Noita
|
||||||
/worlds/noita/ @ScipioWright @heinermann
|
/worlds/noita/ @ScipioWright @heinermann
|
||||||
|
|
||||||
# Old School Runescape
|
# Ocarina of Time
|
||||||
/worlds/osrs @digiholic
|
/worlds/oot/ @espeon65536
|
||||||
|
|
||||||
# Overcooked! 2
|
# Overcooked! 2
|
||||||
/worlds/overcooked2/ @toasterparty
|
/worlds/overcooked2/ @toasterparty
|
||||||
@@ -199,9 +193,6 @@
|
|||||||
# The Witness
|
# The Witness
|
||||||
/worlds/witness/ @NewSoupVi @blastron
|
/worlds/witness/ @NewSoupVi @blastron
|
||||||
|
|
||||||
# Yacht Dice
|
|
||||||
/worlds/yachtdice/ @spinerak
|
|
||||||
|
|
||||||
# Yoshi's Island
|
# Yoshi's Island
|
||||||
/worlds/yoshisisland/ @PinkSwitch
|
/worlds/yoshisisland/ @PinkSwitch
|
||||||
|
|
||||||
@@ -227,9 +218,6 @@
|
|||||||
# Links Awakening DX
|
# Links Awakening DX
|
||||||
# /worlds/ladx/
|
# /worlds/ladx/
|
||||||
|
|
||||||
# Ocarina of Time
|
|
||||||
# /worlds/oot/
|
|
||||||
|
|
||||||
## Disabled Unmaintained Worlds
|
## Disabled Unmaintained Worlds
|
||||||
|
|
||||||
# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are
|
# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are
|
||||||
|
|||||||
@@ -702,18 +702,14 @@ GameData is a **dict** but contains these keys and values. It's broken out into
|
|||||||
| checksum | str | A checksum hash of this game's data. |
|
| checksum | str | A checksum hash of this game's data. |
|
||||||
|
|
||||||
### Tags
|
### Tags
|
||||||
Tags are represented as a list of strings, the common client tags follow:
|
Tags are represented as a list of strings, the common Client tags follow:
|
||||||
|
|
||||||
| Name | Notes |
|
| Name | Notes |
|
||||||
|-----------|--------------------------------------------------------------------------------------------------------------------------------------|
|
|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. |
|
| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. |
|
||||||
| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets. |
|
| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets |
|
||||||
| HintGame | Indicates the client is a hint game, made to send hints instead of locations. Special join/leave message,¹ `game` is optional.² |
|
| Tracker | Tells the server that this client will not send locations and is actually a Tracker. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |
|
||||||
| Tracker | Indicates the client is a tracker, made to track instead of sending locations. Special join/leave message,¹ `game` is optional.² |
|
| TextOnly | Tells the server that this client will not send locations and is intended for chat. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |
|
||||||
| TextOnly | Indicates the client is a basic client, made to chat instead of sending locations. Special join/leave message,¹ `game` is optional.² |
|
|
||||||
|
|
||||||
¹: When connecting or disconnecting, the chat message shows e.g. "tracking".\
|
|
||||||
²: Allows `game` to be empty or null in [Connect](#connect). Game and version validation will then be skipped.
|
|
||||||
|
|
||||||
### DeathLink
|
### DeathLink
|
||||||
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
|
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ display as `Value1` on the webhost.
|
|||||||
files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a
|
files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a
|
||||||
Choice, and defining `alias_true = option_full`.
|
Choice, and defining `alias_true = option_full`.
|
||||||
- All options with a fixed set of possible values (i.e. those which inherit from `Toggle`, `(Text)Choice` or
|
- All options with a fixed set of possible values (i.e. those which inherit from `Toggle`, `(Text)Choice` or
|
||||||
`(Named)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
|
`(Named/Special)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
|
||||||
option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
|
option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
|
||||||
However, you can override `from_text` and handle `text == "random"` to customize its behavior or
|
However, you can override `from_text` and handle `text == "random"` to customize its behavior or
|
||||||
implement it for additional option types.
|
implement it for additional option types.
|
||||||
@@ -129,23 +129,6 @@ class Difficulty(Choice):
|
|||||||
default = 1
|
default = 1
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option Visibility
|
|
||||||
Every option has a Visibility IntFlag, defaulting to `all` (`0b1111`). This lets you choose where the option will be
|
|
||||||
displayed. This only impacts where options are displayed, not how they can be used. Hidden options are still valid
|
|
||||||
options in a yaml. The flags are as follows:
|
|
||||||
* `none` (`0b0000`): This option is not shown anywhere
|
|
||||||
* `template` (`0b0001`): This option shows up in template yamls
|
|
||||||
* `simple_ui` (`0b0010`): This option shows up on the options page
|
|
||||||
* `complex_ui` (`0b0100`): This option shows up on the advanced/weighted options page
|
|
||||||
* `spoiler` (`0b1000`): This option shows up in spoiler logs
|
|
||||||
|
|
||||||
```python
|
|
||||||
from Options import Choice, Visibility
|
|
||||||
|
|
||||||
class HiddenChoiceOption(Choice):
|
|
||||||
visibility = Visibility.none
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option Groups
|
### Option Groups
|
||||||
Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified
|
Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified
|
||||||
by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment
|
by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use that version. These steps are for developers or platforms without compiled r
|
|||||||
|
|
||||||
What you'll need:
|
What you'll need:
|
||||||
* [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version
|
* [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version
|
||||||
* Python 3.12.x is currently the newest supported version
|
* **Python 3.12 is currently unsupported**
|
||||||
* pip: included in downloads from python.org, separate in many Linux distributions
|
* pip: included in downloads from python.org, separate in many Linux distributions
|
||||||
* Matching C compiler
|
* Matching C compiler
|
||||||
* possibly optional, read operating system specific sections
|
* possibly optional, read operating system specific sections
|
||||||
@@ -31,14 +31,14 @@ After this, you should be able to run the programs.
|
|||||||
|
|
||||||
Recommended steps
|
Recommended steps
|
||||||
* Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads)
|
* Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads)
|
||||||
* [read above](#General) which versions are supported
|
* **Python 3.12 is currently unsupported**
|
||||||
|
|
||||||
* **Optional**: Download and install Visual Studio Build Tools from
|
* **Optional**: Download and install Visual Studio Build Tools from
|
||||||
[Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/).
|
[Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/).
|
||||||
* Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details.
|
* Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details.
|
||||||
Generally, selecting the box for "Desktop Development with C++" will provide what you need.
|
Generally, selecting the box for "Desktop Development with C++" will provide what you need.
|
||||||
* Build tools are not required if all modules are installed pre-compiled. Pre-compiled modules are pinned on
|
* Build tools are not required if all modules are installed pre-compiled. Pre-compiled modules are pinned on
|
||||||
[Discord in #ap-core-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
|
[Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
|
||||||
|
|
||||||
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
|
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
|
||||||
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
|
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
|
||||||
|
|||||||
@@ -303,31 +303,6 @@ generation (entrance randomization).
|
|||||||
An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state`
|
An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state`
|
||||||
(items that have been collected).
|
(items that have been collected).
|
||||||
|
|
||||||
The two possible ways to make a [CollectionRule](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10) are:
|
|
||||||
- `def rule(state: CollectionState) -> bool:`
|
|
||||||
- `lambda state: ... boolean expression ...`
|
|
||||||
|
|
||||||
An access rule can be assigned through `set_rule(location, rule)`.
|
|
||||||
|
|
||||||
Access rules usually check for one of two things.
|
|
||||||
- Items that have been collected (e.g. `state.has("Sword", player)`)
|
|
||||||
- Locations, Regions or Entrances that have been reached (e.g. `state.can_reach_region("Boss Room")`)
|
|
||||||
|
|
||||||
Keep in mind that entrances and locations implicitly check for the accessibility of their parent region, so you do not need to check explicitly for it.
|
|
||||||
|
|
||||||
#### An important note on Entrance access rules:
|
|
||||||
When using `state.can_reach` within an entrance access condition, you must also use `multiworld.register_indirect_condition`.
|
|
||||||
|
|
||||||
For efficiency reasons, every time reachable regions are searched, every entrance is only checked once in a somewhat non-deterministic order.
|
|
||||||
This is fine when checking for items using `state.has`, because items do not change during a region sweep.
|
|
||||||
However, `state.can_reach` checks for the very same thing we are updating: Regions.
|
|
||||||
This can lead to non-deterministic behavior and, in the worst case, even generation failures.
|
|
||||||
Even doing `state.can_reach_location` or `state.can_reach_entrance` is problematic, as these functions call `state.can_reach_region` on the respective parent region.
|
|
||||||
|
|
||||||
**Therefore, it is considered unsafe to perform `state.can_reach` from within an access condition for an entrance**, unless you are checking for something that sits in the source region of the entrance.
|
|
||||||
You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance.
|
|
||||||
You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case.
|
|
||||||
|
|
||||||
### Item Rules
|
### Item Rules
|
||||||
|
|
||||||
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
|
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
|
||||||
@@ -655,7 +630,7 @@ def set_rules(self) -> None:
|
|||||||
|
|
||||||
Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or
|
Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or
|
||||||
Entrance should be
|
Entrance should be
|
||||||
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10).
|
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L9).
|
||||||
Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other
|
Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other
|
||||||
functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly.
|
functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly.
|
||||||
For an example, see [The Messenger](/worlds/messenger/rules.py).
|
For an example, see [The Messenger](/worlds/messenger/rules.py).
|
||||||
@@ -696,92 +671,9 @@ When importing a file that defines a class that inherits from `worlds.AutoWorld.
|
|||||||
is automatically extended by the mixin's members. These members should be prefixed with the name of the implementing
|
is automatically extended by the mixin's members. These members should be prefixed with the name of the implementing
|
||||||
world since the namespace is shared with all other logic mixins.
|
world since the namespace is shared with all other logic mixins.
|
||||||
|
|
||||||
LogicMixin is handy when your logic is more complex than one-to-one location-item relationships.
|
Some uses could be to add additional variables to the state object, or to have a custom state machine that gets modified
|
||||||
A game in which "The red key opens the red door" can just express this relationship through a one-line access rule.
|
with the state.
|
||||||
But now, consider a game with a heavy focus on combat, where the main logical consideration is which enemies you can
|
Please do this with caution and only when necessary.
|
||||||
defeat with your current items.
|
|
||||||
There could be dozens of weapons, armor pieces, or consumables that each improve your ability to defeat
|
|
||||||
specific enemies to varying degrees. It would be useful to be able to keep track of "defeatable enemies" as a state variable,
|
|
||||||
and have this variable be recalculated as necessary based on newly collected/removed items.
|
|
||||||
This is the capability of LogicMixin: Adding custom variables to state that get recalculated as necessary.
|
|
||||||
|
|
||||||
In general, a LogicMixin class should have at least one mutable variable that is tracking some custom state per player,
|
|
||||||
as well as `init_mixin` and `copy_mixin` functions so that this variable gets initialized and copied correctly when
|
|
||||||
`CollectionState()` and `CollectionState.copy()` are called respectively.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from BaseClasses import CollectionState, MultiWorld
|
|
||||||
from worlds.AutoWorld import LogicMixin
|
|
||||||
|
|
||||||
class MyGameState(LogicMixin):
|
|
||||||
mygame_defeatable_enemies: Dict[int, Set[str]] # per player
|
|
||||||
|
|
||||||
def init_mixin(self, multiworld: MultiWorld) -> None:
|
|
||||||
# Initialize per player with the corresponding "nothing" value, such as 0 or an empty set.
|
|
||||||
# You can also use something like Collections.defaultdict
|
|
||||||
self.mygame_defeatable_enemies = {
|
|
||||||
player: set() for player in multiworld.get_game_players("My Game")
|
|
||||||
}
|
|
||||||
|
|
||||||
def copy_mixin(self, new_state: CollectionState) -> CollectionState:
|
|
||||||
# Be careful to make a "deep enough" copy here!
|
|
||||||
new_state.mygame_defeatable_enemies = {
|
|
||||||
player: enemies.copy() for player, enemies in self.mygame_defeatable_enemies.items()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
After doing this, you can now access `state.mygame_defeatable_enemies[player]` from your access rules.
|
|
||||||
|
|
||||||
Usually, doing this coincides with an override of `World.collect` and `World.remove`, where the custom state variable
|
|
||||||
gets recalculated when a relevant item is collected or removed.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# __init__.py
|
|
||||||
|
|
||||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
|
||||||
change = super().collect(state, item)
|
|
||||||
if change and item in COMBAT_ITEMS:
|
|
||||||
state.mygame_defeatable_enemies[self.player] |= get_newly_unlocked_enemies(state)
|
|
||||||
return change
|
|
||||||
|
|
||||||
def remove(self, state: CollectionState, item: Item) -> bool:
|
|
||||||
change = super().remove(state, item)
|
|
||||||
if change and item in COMBAT_ITEMS:
|
|
||||||
state.mygame_defeatable_enemies[self.player] -= get_newly_locked_enemies(state)
|
|
||||||
return change
|
|
||||||
```
|
|
||||||
|
|
||||||
Using LogicMixin can greatly slow down your code if you don't use it intelligently. This is because `collect`
|
|
||||||
and `remove` are called very frequently during fill. If your `collect` & `remove` cause a heavy calculation
|
|
||||||
every time, your code might end up being *slower* than just doing calculations in your access rules.
|
|
||||||
|
|
||||||
One way to optimise recalculations is to make use of the fact that `collect` should only unlock things,
|
|
||||||
and `remove` should only lock things.
|
|
||||||
In our example, we have two different functions: `get_newly_unlocked_enemies` and `get_newly_locked_enemies`.
|
|
||||||
`get_newly_unlocked_enemies` should only consider enemies that are *not already in the set*
|
|
||||||
and check whether they were **unlocked**.
|
|
||||||
`get_newly_locked_enemies` should only consider enemies that are *already in the set*
|
|
||||||
and check whether they **became locked**.
|
|
||||||
|
|
||||||
Another impactful way to optimise LogicMixin is to use caching.
|
|
||||||
Your custom state variables don't actually need to be recalculated on every `collect` / `remove`, because there are
|
|
||||||
often multiple calls to `collect` / `remove` between access rule calls. Thus, it would be much more efficient to hold
|
|
||||||
off on recaculating until the an actual access rule call happens.
|
|
||||||
A common way to realize this is to define a `mygame_state_is_stale` variable that is set to True in `collect`, `remove`,
|
|
||||||
and `init_mixin`. The calls to the actual recalculating functions are then moved to the start of the relevant
|
|
||||||
access rules like this:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def can_defeat_enemy(state: CollectionState, player: int, enemy: str) -> bool:
|
|
||||||
if state.mygame_state_is_stale[player]:
|
|
||||||
state.mygame_defeatable_enemies[player] = recalculate_defeatable_enemies(state)
|
|
||||||
state.mygame_state_is_stale[player] = False
|
|
||||||
|
|
||||||
return enemy in state.mygame_defeatable_enemies[player]
|
|
||||||
```
|
|
||||||
|
|
||||||
Only use LogicMixin if necessary. There are often other ways to achieve what it does, like making clever use of
|
|
||||||
`state.prog_items`, using event items, pseudo-regions, etc.
|
|
||||||
|
|
||||||
#### pre_fill
|
#### pre_fill
|
||||||
|
|
||||||
|
|||||||
@@ -26,17 +26,8 @@ Unless these are shared between multiple people, we expect the following from ea
|
|||||||
### Adding a World
|
### Adding a World
|
||||||
|
|
||||||
When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you
|
When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you
|
||||||
nominate someone else (i.e. there are multiple devs).
|
nominate someone else (i.e. there are multiple devs). You can define who is allowed to approve changes to your world
|
||||||
|
in the [CODEOWNERS](/docs/CODEOWNERS) document.
|
||||||
### Being added as a maintainer to an existing implementation
|
|
||||||
|
|
||||||
At any point, a world maintainer can approve the addition of another maintainer to their world.
|
|
||||||
In order to do this, either an existing maintainer or the new maintainer must open a PR updating the
|
|
||||||
[CODEOWNERS](/docs/CODEOWNERS) file.
|
|
||||||
This change must be approved by all existing maintainers of the affected world, the new maintainer candidate, and
|
|
||||||
one core maintainer.
|
|
||||||
To help the core team review the change, information about the new maintainer and their contributions should be
|
|
||||||
included in the PR description.
|
|
||||||
|
|
||||||
### Getting Voted
|
### Getting Voted
|
||||||
|
|
||||||
@@ -44,7 +35,7 @@ When a world is unmaintained, the [core maintainers](https://github.com/orgs/Arc
|
|||||||
can vote for a new maintainer if there is a candidate.
|
can vote for a new maintainer if there is a candidate.
|
||||||
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
||||||
The time limit is 1 week, but can end early if the majority is reached earlier.
|
The time limit is 1 week, but can end early if the majority is reached earlier.
|
||||||
Voting shall be conducted on Discord in #ap-core-dev.
|
Voting shall be conducted on Discord in #archipelago-dev.
|
||||||
|
|
||||||
## Dropping out
|
## Dropping out
|
||||||
|
|
||||||
@@ -60,7 +51,7 @@ for example when they become unreachable.
|
|||||||
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
||||||
The time limit is 2 weeks, but can end early if the majority is reached earlier AND the world maintainer was pinged and
|
The time limit is 2 weeks, but can end early if the majority is reached earlier AND the world maintainer was pinged and
|
||||||
made their case or was pinged and has been unreachable for more than 2 weeks already.
|
made their case or was pinged and has been unreachable for more than 2 weeks already.
|
||||||
Voting shall be conducted on Discord in #ap-core-dev. Commits that are a direct result of the voting shall include
|
Voting shall be conducted on Discord in #archipelago-dev. Commits that are a direct result of the voting shall include
|
||||||
date, voting members and final result in the commit message.
|
date, voting members and final result in the commit message.
|
||||||
|
|
||||||
## Handling of Unmaintained Worlds
|
## Handling of Unmaintained Worlds
|
||||||
|
|||||||
@@ -186,11 +186,6 @@ Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Arc
|
|||||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}cv64patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".apmm2"; ValueData: "{#MyAppName}mm2patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
|
||||||
Root: HKCR; Subkey: "{#MyAppName}mm2patch"; ValueData: "Archipelago Mega Man 2 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
|
||||||
Root: HKCR; Subkey: "{#MyAppName}mm2patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
|
||||||
Root: HKCR; Subkey: "{#MyAppName}mm2patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
|
||||||
@@ -228,8 +223,8 @@ Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{a
|
|||||||
|
|
||||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
|
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
|
||||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
|
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
|
||||||
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoLauncher.exe,0";
|
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0";
|
||||||
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1""";
|
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1""";
|
||||||
|
|
||||||
[Code]
|
[Code]
|
||||||
// See: https://stackoverflow.com/a/51614652/2287576
|
// See: https://stackoverflow.com/a/51614652/2287576
|
||||||
|
|||||||
2
kvui.py
2
kvui.py
@@ -5,8 +5,6 @@ import typing
|
|||||||
import re
|
import re
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility"
|
|
||||||
|
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
colorama>=0.4.6
|
colorama>=0.4.6
|
||||||
websockets>=13.0.1
|
websockets>=12.0
|
||||||
PyYAML>=6.0.2
|
PyYAML>=6.0.1
|
||||||
jellyfish>=1.1.0
|
jellyfish>=1.0.3
|
||||||
jinja2>=3.1.4
|
jinja2>=3.1.4
|
||||||
schema>=0.7.7
|
schema>=0.7.7
|
||||||
kivy>=2.3.0
|
kivy>=2.3.0
|
||||||
bsdiff4>=1.2.4
|
bsdiff4>=1.2.4
|
||||||
platformdirs>=4.2.2
|
platformdirs>=4.2.2
|
||||||
certifi>=2024.8.30
|
certifi>=2024.6.2
|
||||||
cython>=3.0.11
|
cython>=3.0.10
|
||||||
cymem>=2.0.8
|
cymem>=2.0.8
|
||||||
orjson>=3.10.7
|
orjson>=3.10.3
|
||||||
typing_extensions>=4.12.2
|
typing_extensions>=4.12.1
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ class TestBase(unittest.TestCase):
|
|||||||
state = CollectionState(self.multiworld)
|
state = CollectionState(self.multiworld)
|
||||||
for item in items:
|
for item in items:
|
||||||
item.classification = ItemClassification.progression
|
item.classification = ItemClassification.progression
|
||||||
state.collect(item, prevent_sweep=True)
|
state.collect(item, event=True)
|
||||||
state.sweep_for_advancements()
|
state.sweep_for_events()
|
||||||
state.update_reachable_regions(1)
|
state.update_reachable_regions(1)
|
||||||
self._state_cache[self.multiworld, tuple(items)] = state
|
self._state_cache[self.multiworld, tuple(items)] = state
|
||||||
return state
|
return state
|
||||||
@@ -221,8 +221,8 @@ class WorldTestBase(unittest.TestCase):
|
|||||||
if isinstance(items, Item):
|
if isinstance(items, Item):
|
||||||
items = (items,)
|
items = (items,)
|
||||||
for item in items:
|
for item in items:
|
||||||
if item.location and item.advancement and item.location in self.multiworld.state.advancements:
|
if item.location and item.advancement and item.location in self.multiworld.state.events:
|
||||||
self.multiworld.state.advancements.remove(item.location)
|
self.multiworld.state.events.remove(item.location)
|
||||||
self.multiworld.state.remove(item)
|
self.multiworld.state.remove(item)
|
||||||
|
|
||||||
def can_reach_location(self, location: str) -> bool:
|
def can_reach_location(self, location: str) -> bool:
|
||||||
@@ -293,11 +293,13 @@ class WorldTestBase(unittest.TestCase):
|
|||||||
if not (self.run_default_tests and self.constructed):
|
if not (self.run_default_tests and self.constructed):
|
||||||
return
|
return
|
||||||
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
|
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
|
||||||
|
excluded = self.multiworld.worlds[self.player].options.exclude_locations.value
|
||||||
state = self.multiworld.get_all_state(False)
|
state = self.multiworld.get_all_state(False)
|
||||||
for location in self.multiworld.get_locations():
|
for location in self.multiworld.get_locations():
|
||||||
with self.subTest("Location should be reached", location=location.name):
|
if location.name not in excluded:
|
||||||
reachable = location.can_reach(state)
|
with self.subTest("Location should be reached", location=location.name):
|
||||||
self.assertTrue(reachable, f"{location.name} unreachable")
|
reachable = location.can_reach(state)
|
||||||
|
self.assertTrue(reachable, f"{location.name} unreachable")
|
||||||
with self.subTest("Beatable"):
|
with self.subTest("Beatable"):
|
||||||
self.multiworld.state = state
|
self.multiworld.state = state
|
||||||
self.assertBeatable(True)
|
self.assertBeatable(True)
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ class TestFillRestrictive(unittest.TestCase):
|
|||||||
location_pool = player1.locations[1:] + player2.locations
|
location_pool = player1.locations[1:] + player2.locations
|
||||||
item_pool = player1.prog_items[:-1] + player2.prog_items
|
item_pool = player1.prog_items[:-1] + player2.prog_items
|
||||||
fill_restrictive(multiworld, multiworld.state, location_pool, item_pool)
|
fill_restrictive(multiworld, multiworld.state, location_pool, item_pool)
|
||||||
multiworld.state.sweep_for_advancements() # collect everything
|
multiworld.state.sweep_for_events() # collect everything
|
||||||
|
|
||||||
# all of player2's locations and items should be accessible (not all of player1's)
|
# all of player2's locations and items should be accessible (not all of player1's)
|
||||||
for item in player2.prog_items:
|
for item in player2.prog_items:
|
||||||
@@ -443,8 +443,8 @@ class TestFillRestrictive(unittest.TestCase):
|
|||||||
item = player1.prog_items[0]
|
item = player1.prog_items[0]
|
||||||
item.code = None
|
item.code = None
|
||||||
location.place_locked_item(item)
|
location.place_locked_item(item)
|
||||||
multiworld.state.sweep_for_advancements()
|
multiworld.state.sweep_for_events()
|
||||||
multiworld.state.sweep_for_advancements()
|
multiworld.state.sweep_for_events()
|
||||||
self.assertTrue(multiworld.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed")
|
self.assertTrue(multiworld.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed")
|
||||||
self.assertEqual(multiworld.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times")
|
self.assertEqual(multiworld.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times")
|
||||||
|
|
||||||
|
|||||||
@@ -14,18 +14,6 @@ class TestBase(unittest.TestCase):
|
|||||||
"Desert Northern Cliffs", # on top of mountain, only reachable via OWG
|
"Desert Northern Cliffs", # on top of mountain, only reachable via OWG
|
||||||
"Dark Death Mountain Bunny Descent Area" # OWG Mountain descent
|
"Dark Death Mountain Bunny Descent Area" # OWG Mountain descent
|
||||||
},
|
},
|
||||||
# These Blasphemous regions are not reachable with default options
|
|
||||||
"Blasphemous": {
|
|
||||||
"D01Z04S13[SE]", # difficulty must be hard
|
|
||||||
"D01Z05S25[E]", # difficulty must be hard
|
|
||||||
"D02Z02S05[W]", # difficulty must be hard and purified_hand must be true
|
|
||||||
"D04Z01S06[E]", # purified_hand must be true
|
|
||||||
"D04Z02S02[NE]", # difficulty must be hard and purified_hand must be true
|
|
||||||
"D05Z01S11[SW]", # difficulty must be hard
|
|
||||||
"D06Z01S08[N]", # difficulty must be hard and purified_hand must be true
|
|
||||||
"D20Z02S11[NW]", # difficulty must be hard
|
|
||||||
"D20Z02S11[E]", # difficulty must be hard
|
|
||||||
},
|
|
||||||
"Ocarina of Time": {
|
"Ocarina of Time": {
|
||||||
"Prelude of Light Warp", # Prelude is not progression by default
|
"Prelude of Light Warp", # Prelude is not progression by default
|
||||||
"Serenade of Water Warp", # Serenade is not progression by default
|
"Serenade of Water Warp", # Serenade is not progression by default
|
||||||
@@ -49,10 +37,12 @@ class TestBase(unittest.TestCase):
|
|||||||
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
|
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
|
||||||
with self.subTest("Game", game=game_name):
|
with self.subTest("Game", game=game_name):
|
||||||
multiworld = setup_solo_multiworld(world_type)
|
multiworld = setup_solo_multiworld(world_type)
|
||||||
|
excluded = multiworld.worlds[1].options.exclude_locations.value
|
||||||
state = multiworld.get_all_state(False)
|
state = multiworld.get_all_state(False)
|
||||||
for location in multiworld.get_locations():
|
for location in multiworld.get_locations():
|
||||||
with self.subTest("Location should be reached", location=location.name):
|
if location.name not in excluded:
|
||||||
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
|
with self.subTest("Location should be reached", location=location.name):
|
||||||
|
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
|
||||||
|
|
||||||
for region in multiworld.get_regions():
|
for region in multiworld.get_regions():
|
||||||
if region.name in unreachable_regions:
|
if region.name in unreachable_regions:
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class TestAllGamesMultiworld(MultiworldTestBase):
|
|||||||
all_worlds = list(AutoWorldRegister.world_types.values())
|
all_worlds = list(AutoWorldRegister.world_types.values())
|
||||||
self.multiworld = setup_multiworld(all_worlds, ())
|
self.multiworld = setup_multiworld(all_worlds, ())
|
||||||
for world in self.multiworld.worlds.values():
|
for world in self.multiworld.worlds.values():
|
||||||
world.options.accessibility.value = Accessibility.option_full
|
world.options.accessibility.value = Accessibility.option_locations
|
||||||
self.assertSteps(gen_steps)
|
self.assertSteps(gen_steps)
|
||||||
with self.subTest("filling multiworld", seed=self.multiworld.seed):
|
with self.subTest("filling multiworld", seed=self.multiworld.seed):
|
||||||
distribute_items_restrictive(self.multiworld)
|
distribute_items_restrictive(self.multiworld)
|
||||||
@@ -66,8 +66,8 @@ class TestAllGamesMultiworld(MultiworldTestBase):
|
|||||||
class TestTwoPlayerMulti(MultiworldTestBase):
|
class TestTwoPlayerMulti(MultiworldTestBase):
|
||||||
def test_two_player_single_game_fills(self) -> None:
|
def test_two_player_single_game_fills(self) -> None:
|
||||||
"""Tests that a multiworld of two players for each registered game world can generate."""
|
"""Tests that a multiworld of two players for each registered game world can generate."""
|
||||||
for world_type in AutoWorldRegister.world_types.values():
|
for world in AutoWorldRegister.world_types.values():
|
||||||
self.multiworld = setup_multiworld([world_type, world_type], ())
|
self.multiworld = setup_multiworld([world, world], ())
|
||||||
for world in self.multiworld.worlds.values():
|
for world in self.multiworld.worlds.values():
|
||||||
world.options.accessibility.value = Accessibility.option_full
|
world.options.accessibility.value = Accessibility.option_full
|
||||||
self.assertSteps(gen_steps)
|
self.assertSteps(gen_steps)
|
||||||
|
|||||||
@@ -130,9 +130,9 @@ class Base:
|
|||||||
|
|
||||||
def test_get_remaining(self) -> None:
|
def test_get_remaining(self) -> None:
|
||||||
self.assertEqual(self.store.get_remaining(full_state, 0, 1), [])
|
self.assertEqual(self.store.get_remaining(full_state, 0, 1), [])
|
||||||
self.assertEqual(self.store.get_remaining(one_state, 0, 1), [(1, 13), (2, 21)])
|
self.assertEqual(self.store.get_remaining(one_state, 0, 1), [13, 21])
|
||||||
self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [(1, 13), (2, 21), (2, 22)])
|
self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [13, 21, 22])
|
||||||
self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [(4, 99)])
|
self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [99])
|
||||||
|
|
||||||
def test_location_set_intersection(self) -> None:
|
def test_location_set_intersection(self) -> None:
|
||||||
locations = {10, 11, 12}
|
locations = {10, 11, 12}
|
||||||
|
|||||||
@@ -131,8 +131,7 @@ class TestHostFakeRoom(TestBase):
|
|||||||
f.write(text)
|
f.write(text)
|
||||||
|
|
||||||
with self.app.app_context(), self.app.test_request_context():
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
response = self.client.get(url_for("host_room", room=self.room_id),
|
response = self.client.get(url_for("host_room", room=self.room_id))
|
||||||
headers={"User-Agent": "Mozilla/5.0"})
|
|
||||||
response_text = response.get_data(True)
|
response_text = response.get_data(True)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertIn("href=\"/seed/", response_text)
|
self.assertIn("href=\"/seed/", response_text)
|
||||||
|
|||||||
@@ -292,14 +292,6 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
web: ClassVar[WebWorld] = WebWorld()
|
web: ClassVar[WebWorld] = WebWorld()
|
||||||
"""see WebWorld for options"""
|
"""see WebWorld for options"""
|
||||||
|
|
||||||
origin_region_name: str = "Menu"
|
|
||||||
"""Name of the Region from which accessibility is tested."""
|
|
||||||
|
|
||||||
explicit_indirect_conditions: bool = True
|
|
||||||
"""If True, the world implementation is supposed to use MultiWorld.register_indirect_condition() correctly.
|
|
||||||
If False, everything is rechecked at every step, which is slower computationally,
|
|
||||||
but may be desirable in complex/dynamic worlds."""
|
|
||||||
|
|
||||||
multiworld: "MultiWorld"
|
multiworld: "MultiWorld"
|
||||||
"""autoset on creation. The MultiWorld object for the currently generating multiworld."""
|
"""autoset on creation. The MultiWorld object for the currently generating multiworld."""
|
||||||
player: int
|
player: int
|
||||||
|
|||||||
@@ -26,13 +26,10 @@ class Component:
|
|||||||
cli: bool
|
cli: bool
|
||||||
func: Optional[Callable]
|
func: Optional[Callable]
|
||||||
file_identifier: Optional[Callable[[str], bool]]
|
file_identifier: Optional[Callable[[str], bool]]
|
||||||
game_name: Optional[str]
|
|
||||||
supports_uri: Optional[bool]
|
|
||||||
|
|
||||||
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
||||||
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
|
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
|
||||||
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None,
|
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None):
|
||||||
game_name: Optional[str] = None, supports_uri: Optional[bool] = False):
|
|
||||||
self.display_name = display_name
|
self.display_name = display_name
|
||||||
self.script_name = script_name
|
self.script_name = script_name
|
||||||
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
||||||
@@ -48,8 +45,6 @@ class Component:
|
|||||||
Type.ADJUSTER if "Adjuster" in display_name else Type.MISC)
|
Type.ADJUSTER if "Adjuster" in display_name else Type.MISC)
|
||||||
self.func = func
|
self.func = func
|
||||||
self.file_identifier = file_identifier
|
self.file_identifier = file_identifier
|
||||||
self.game_name = game_name
|
|
||||||
self.supports_uri = supports_uri
|
|
||||||
|
|
||||||
def handles_file(self, path: str):
|
def handles_file(self, path: str):
|
||||||
return self.file_identifier(path) if self.file_identifier else False
|
return self.file_identifier(path) if self.file_identifier else False
|
||||||
@@ -61,10 +56,10 @@ class Component:
|
|||||||
processes = weakref.WeakSet()
|
processes = weakref.WeakSet()
|
||||||
|
|
||||||
|
|
||||||
def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None:
|
def launch_subprocess(func: Callable, name: str = None):
|
||||||
global processes
|
global processes
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
process = multiprocessing.Process(target=func, name=name, args=args)
|
process = multiprocessing.Process(target=func, name=name)
|
||||||
process.start()
|
process.start()
|
||||||
processes.add(process)
|
processes.add(process)
|
||||||
|
|
||||||
@@ -83,9 +78,9 @@ class SuffixIdentifier:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def launch_textclient(*args):
|
def launch_textclient():
|
||||||
import CommonClient
|
import CommonClient
|
||||||
launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args)
|
launch_subprocess(CommonClient.run_as_textclient, name="TextClient")
|
||||||
|
|
||||||
|
|
||||||
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
|
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
|
||||||
@@ -137,8 +132,7 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path
|
|||||||
break
|
break
|
||||||
if found_already_loaded:
|
if found_already_loaded:
|
||||||
raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded,\n"
|
raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded,\n"
|
||||||
"so a Launcher restart is required to use the new installation.\n"
|
"so a Launcher restart is required to use the new installation.")
|
||||||
"If the Launcher is not open, no action needs to be taken.")
|
|
||||||
world_source = worlds.WorldSource(str(target), is_zip=True)
|
world_source = worlds.WorldSource(str(target), is_zip=True)
|
||||||
bisect.insort(worlds.world_sources, world_source)
|
bisect.insort(worlds.world_sources, world_source)
|
||||||
world_source.load()
|
world_source.load()
|
||||||
|
|||||||
@@ -73,12 +73,7 @@ class WorldSource:
|
|||||||
else: # TODO: remove with 3.8 support
|
else: # TODO: remove with 3.8 support
|
||||||
mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0])
|
mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0])
|
||||||
|
|
||||||
if mod.__package__ is not None:
|
mod.__package__ = f"worlds.{mod.__package__}"
|
||||||
mod.__package__ = f"worlds.{mod.__package__}"
|
|
||||||
else:
|
|
||||||
# load_module does not populate package, we'll have to assume mod.__name__ is correct here
|
|
||||||
# probably safe to remove with 3.8 support
|
|
||||||
mod.__package__ = f"worlds.{mod.__name__}"
|
|
||||||
mod.__name__ = f"worlds.{mod.__name__}"
|
mod.__name__ = f"worlds.{mod.__name__}"
|
||||||
sys.modules[mod.__name__] = mod
|
sys.modules[mod.__name__] = mod
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
def launch_client(*args) -> None:
|
def launch_client(*args) -> None:
|
||||||
from .context import launch
|
from .context import launch
|
||||||
launch_subprocess(launch, name="BizHawkClient", args=args)
|
launch_subprocess(launch, name="BizHawkClient")
|
||||||
|
|
||||||
|
|
||||||
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
||||||
|
|||||||
@@ -59,10 +59,14 @@ class BizHawkClientContext(CommonContext):
|
|||||||
self.bizhawk_ctx = BizHawkContext()
|
self.bizhawk_ctx = BizHawkContext()
|
||||||
self.watcher_timeout = 0.5
|
self.watcher_timeout = 0.5
|
||||||
|
|
||||||
def make_gui(self):
|
def run_gui(self):
|
||||||
ui = super().make_gui()
|
from kvui import GameManager
|
||||||
ui.base_title = "Archipelago BizHawk Client"
|
|
||||||
return ui
|
class BizHawkManager(GameManager):
|
||||||
|
base_title = "Archipelago BizHawk Client"
|
||||||
|
|
||||||
|
self.ui = BizHawkManager(self)
|
||||||
|
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||||
|
|
||||||
def on_package(self, cmd, args):
|
def on_package(self, cmd, args):
|
||||||
if cmd == "Connected":
|
if cmd == "Connected":
|
||||||
@@ -239,11 +243,11 @@ async def _patch_and_run_game(patch_file: str):
|
|||||||
logger.exception(exc)
|
logger.exception(exc)
|
||||||
|
|
||||||
|
|
||||||
def launch(*launch_args) -> None:
|
def launch() -> None:
|
||||||
async def main():
|
async def main():
|
||||||
parser = get_base_parser()
|
parser = get_base_parser()
|
||||||
parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file")
|
parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file")
|
||||||
args = parser.parse_args(launch_args)
|
args = parser.parse_args()
|
||||||
|
|
||||||
ctx = BizHawkClientContext(args.connect, args.password)
|
ctx = BizHawkClientContext(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")
|
||||||
|
|||||||
@@ -968,35 +968,40 @@ def get_act_by_number(world: "HatInTimeWorld", chapter_name: str, num: int) -> R
|
|||||||
def create_thug_shops(world: "HatInTimeWorld"):
|
def create_thug_shops(world: "HatInTimeWorld"):
|
||||||
min_items: int = world.options.NyakuzaThugMinShopItems.value
|
min_items: int = world.options.NyakuzaThugMinShopItems.value
|
||||||
max_items: int = world.options.NyakuzaThugMaxShopItems.value
|
max_items: int = world.options.NyakuzaThugMaxShopItems.value
|
||||||
|
count = -1
|
||||||
thug_location_counts: Dict[str, int] = {}
|
step = 0
|
||||||
|
old_name = ""
|
||||||
|
|
||||||
for key, data in shop_locations.items():
|
for key, data in shop_locations.items():
|
||||||
thug_name = data.nyakuza_thug
|
if data.nyakuza_thug == "":
|
||||||
if thug_name == "":
|
|
||||||
# Different shop type.
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if thug_name not in world.nyakuza_thug_items:
|
if old_name != "" and old_name == data.nyakuza_thug:
|
||||||
shop_item_count = world.random.randint(min_items, max_items)
|
|
||||||
world.nyakuza_thug_items[thug_name] = shop_item_count
|
|
||||||
else:
|
|
||||||
shop_item_count = world.nyakuza_thug_items[thug_name]
|
|
||||||
|
|
||||||
if shop_item_count <= 0:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
location_count = thug_location_counts.setdefault(thug_name, 0)
|
try:
|
||||||
if location_count >= shop_item_count:
|
if world.nyakuza_thug_items[data.nyakuza_thug] <= 0:
|
||||||
# Already created all the locations for this thug.
|
continue
|
||||||
continue
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Create the shop location.
|
if count == -1:
|
||||||
region = world.multiworld.get_region(data.region, world.player)
|
count = world.random.randint(min_items, max_items)
|
||||||
loc = HatInTimeLocation(world.player, key, data.id, region)
|
world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count)
|
||||||
region.locations.append(loc)
|
if count <= 0:
|
||||||
world.shop_locs.append(loc.name)
|
continue
|
||||||
thug_location_counts[thug_name] = location_count + 1
|
|
||||||
|
if count >= 1:
|
||||||
|
region = world.multiworld.get_region(data.region, world.player)
|
||||||
|
loc = HatInTimeLocation(world.player, key, data.id, region)
|
||||||
|
region.locations.append(loc)
|
||||||
|
world.shop_locs.append(loc.name)
|
||||||
|
|
||||||
|
step += 1
|
||||||
|
if step >= count:
|
||||||
|
old_name = data.nyakuza_thug
|
||||||
|
step = 0
|
||||||
|
count = -1
|
||||||
|
|
||||||
|
|
||||||
def create_events(world: "HatInTimeWorld") -> int:
|
def create_events(world: "HatInTimeWorld") -> int:
|
||||||
|
|||||||
@@ -381,8 +381,8 @@ def set_moderate_rules(world: "HatInTimeWorld"):
|
|||||||
lambda state: can_use_hat(state, world, HatType.ICE), "or")
|
lambda state: can_use_hat(state, world, HatType.ICE), "or")
|
||||||
|
|
||||||
# Moderate: Clock Tower Chest + Ruined Tower with nothing
|
# Moderate: Clock Tower Chest + Ruined Tower with nothing
|
||||||
set_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
|
add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
|
||||||
set_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
|
add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
|
||||||
|
|
||||||
# Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell
|
# Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell
|
||||||
for loc in world.multiworld.get_region("The Subcon Well", world.player).locations:
|
for loc in world.multiworld.get_region("The Subcon Well", world.player).locations:
|
||||||
@@ -432,8 +432,8 @@ def set_moderate_rules(world: "HatInTimeWorld"):
|
|||||||
|
|
||||||
if world.is_dlc1():
|
if world.is_dlc1():
|
||||||
# Moderate: clear Rock the Boat without Ice Hat
|
# Moderate: clear Rock the Boat without Ice Hat
|
||||||
set_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
|
add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
|
||||||
set_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
|
add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
|
||||||
|
|
||||||
# Moderate: clear Deep Sea without Ice Hat
|
# Moderate: clear Deep Sea without Ice Hat
|
||||||
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player),
|
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player),
|
||||||
@@ -855,9 +855,6 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]):
|
|||||||
|
|
||||||
for entrance in regions["Time Rift - Alpine Skyline"].entrances:
|
for entrance in regions["Time Rift - Alpine Skyline"].entrances:
|
||||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
|
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
|
||||||
if entrance.parent_region.name == "Alpine Free Roam":
|
|
||||||
add_rule(entrance,
|
|
||||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True))
|
|
||||||
|
|
||||||
if world.is_dlc1():
|
if world.is_dlc1():
|
||||||
for entrance in regions["Time Rift - Balcony"].entrances:
|
for entrance in regions["Time Rift - Balcony"].entrances:
|
||||||
@@ -936,9 +933,6 @@ def set_default_rift_rules(world: "HatInTimeWorld"):
|
|||||||
|
|
||||||
for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances:
|
for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances:
|
||||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
|
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
|
||||||
if entrance.parent_region.name == "Alpine Free Roam":
|
|
||||||
add_rule(entrance,
|
|
||||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True))
|
|
||||||
|
|
||||||
if world.is_dlc1():
|
if world.is_dlc1():
|
||||||
for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances:
|
for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances:
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
|
|||||||
pass
|
pass
|
||||||
for item in pre_fill_items:
|
for item in pre_fill_items:
|
||||||
multiworld.worlds[item.player].collect(all_state_base, item)
|
multiworld.worlds[item.player].collect(all_state_base, item)
|
||||||
all_state_base.sweep_for_advancements()
|
all_state_base.sweep_for_events()
|
||||||
|
|
||||||
# Remove completion condition so that minimal-accessibility worlds place keys properly
|
# Remove completion condition so that minimal-accessibility worlds place keys properly
|
||||||
for player in {item.player for item in in_dungeon_items}:
|
for player in {item.player for item in in_dungeon_items}:
|
||||||
@@ -262,8 +262,8 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
|
|||||||
all_state_base.remove(item_factory(key_data[3], multiworld.worlds[player]))
|
all_state_base.remove(item_factory(key_data[3], multiworld.worlds[player]))
|
||||||
loc = multiworld.get_location(key_loc, player)
|
loc = multiworld.get_location(key_loc, player)
|
||||||
|
|
||||||
if loc in all_state_base.advancements:
|
if loc in all_state_base.events:
|
||||||
all_state_base.advancements.remove(loc)
|
all_state_base.events.remove(loc)
|
||||||
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, lock=True, allow_excluded=True,
|
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, lock=True, allow_excluded=True,
|
||||||
name="LttP Dungeon Items")
|
name="LttP Dungeon Items")
|
||||||
|
|
||||||
|
|||||||
@@ -682,7 +682,7 @@ def get_pool_core(world, player: int):
|
|||||||
if 'triforce_hunt' in goal:
|
if 'triforce_hunt' in goal:
|
||||||
|
|
||||||
if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra:
|
if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra:
|
||||||
treasure_hunt_total = (world.triforce_pieces_required[player].value
|
treasure_hunt_total = (world.triforce_pieces_available[player].value
|
||||||
+ world.triforce_pieces_extra[player].value)
|
+ world.triforce_pieces_extra[player].value)
|
||||||
elif world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_percentage:
|
elif world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_percentage:
|
||||||
percentage = float(world.triforce_pieces_percentage[player].value) / 100
|
percentage = float(world.triforce_pieces_percentage[player].value) / 100
|
||||||
|
|||||||
@@ -728,7 +728,7 @@ class ALttPPlandoConnections(PlandoConnections):
|
|||||||
entrances = set([connection[0] for connection in (
|
entrances = set([connection[0] for connection in (
|
||||||
*default_connections, *default_dungeon_connections, *inverted_default_connections,
|
*default_connections, *default_dungeon_connections, *inverted_default_connections,
|
||||||
*inverted_default_dungeon_connections)])
|
*inverted_default_dungeon_connections)])
|
||||||
exits = set([connection[0] for connection in (
|
exits = set([connection[1] for connection in (
|
||||||
*default_connections, *default_dungeon_connections, *inverted_default_connections,
|
*default_connections, *default_dungeon_connections, *inverted_default_connections,
|
||||||
*inverted_default_dungeon_connections)])
|
*inverted_default_dungeon_connections)])
|
||||||
|
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player))
|
lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player))
|
||||||
set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player),
|
set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player),
|
||||||
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
||||||
if multiworld.accessibility[player] != 'full' and not multiworld.key_drop_shuffle[player]:
|
if multiworld.accessibility[player] != 'locations' and not multiworld.key_drop_shuffle[player]:
|
||||||
set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
|
set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
|
||||||
set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
|
set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
|
||||||
set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
|
set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
|
||||||
@@ -547,7 +547,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
|
location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
|
||||||
|
|
||||||
# this seemed to be causing generation failure, disable for now
|
# this seemed to be causing generation failure, disable for now
|
||||||
# if world.accessibility[player] != 'full':
|
# if world.accessibility[player] != 'locations':
|
||||||
# set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
|
# set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
|
||||||
|
|
||||||
# It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
|
# It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
|
||||||
|
|||||||
@@ -356,8 +356,6 @@ class ALTTPWorld(World):
|
|||||||
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
|
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
|
||||||
if option == "original_dungeon":
|
if option == "original_dungeon":
|
||||||
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group]
|
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group]
|
||||||
else:
|
|
||||||
self.options.local_items.value |= self.dungeon_local_item_names
|
|
||||||
|
|
||||||
self.difficulty_requirements = difficulties[multiworld.item_pool[player].current_key]
|
self.difficulty_requirements = difficulties[multiworld.item_pool[player].current_key]
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
1. All plando options are enabled by default, except for "items plando" which has to be enabled before it can be used (opt-in).
|
1. Plando features have to be enabled first, before they can be used (opt-in).
|
||||||
2. To enable it, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml
|
2. To do so, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml
|
||||||
file with a text editor.
|
file with a text editor.
|
||||||
3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the value
|
3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the value
|
||||||
to `bosses, items, texts, connections`
|
to `bosses, items, texts, connections`
|
||||||
@@ -66,7 +66,6 @@ boss_shuffle:
|
|||||||
- ignored if only one world is generated
|
- ignored if only one world is generated
|
||||||
- can be a number, to target that slot in the multiworld
|
- can be a number, to target that slot in the multiworld
|
||||||
- can be a name, to target that player's world
|
- can be a name, to target that player's world
|
||||||
- can be a list of names, to target those players' worlds
|
|
||||||
- can be true, to target any other player's world
|
- can be true, to target any other player's world
|
||||||
- can be false, to target own world and is the default
|
- can be false, to target own world and is the default
|
||||||
- can be null, to target a random world
|
- can be null, to target a random world
|
||||||
@@ -133,15 +132,17 @@ plando_items:
|
|||||||
|
|
||||||
### Texts
|
### Texts
|
||||||
|
|
||||||
|
- This module is disabled by default.
|
||||||
- Has the options `text`, `at`, and `percentage`
|
- Has the options `text`, `at`, and `percentage`
|
||||||
- All of these options support subweights
|
|
||||||
- percentage is the percentage chance for this text to be placed, can be omitted entirely for 100%
|
- percentage is the percentage chance for this text to be placed, can be omitted entirely for 100%
|
||||||
- text is the text to be placed.
|
- text is the text to be placed.
|
||||||
|
- can be weighted.
|
||||||
- `\n` is a newline.
|
- `\n` is a newline.
|
||||||
- `@` is the entered player's name.
|
- `@` is the entered player's name.
|
||||||
- Warning: Text Mapper does not support full unicode.
|
- Warning: Text Mapper does not support full unicode.
|
||||||
- [Alphabet](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L758)
|
- [Alphabet](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L758)
|
||||||
- at is the location within the game to attach the text to.
|
- at is the location within the game to attach the text to.
|
||||||
|
- can be weighted.
|
||||||
- [List of targets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L1499)
|
- [List of targets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L1499)
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
@@ -161,6 +162,7 @@ and `uncle_dying_sewer`, then places the text "This is a plando. You've been war
|
|||||||
|
|
||||||
### Connections
|
### Connections
|
||||||
|
|
||||||
|
- This module is disabled by default.
|
||||||
- Has the options `percentage`, `entrance`, `exit` and `direction`.
|
- Has the options `percentage`, `entrance`, `exit` and `direction`.
|
||||||
- All options support subweights
|
- All options support subweights
|
||||||
- percentage is the percentage chance for this to be connected, can be omitted entirely for 100%
|
- percentage is the percentage chance for this to be connected, can be omitted entirely for 100%
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class TestDungeon(LTTPTestBase):
|
|||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
item.classification = ItemClassification.progression
|
item.classification = ItemClassification.progression
|
||||||
state.collect(item, prevent_sweep=True) # prevent_sweep=True prevents running sweep_for_advancements() and picking up
|
state.collect(item, event=True) # event=True prevents running sweep_for_events() and picking up
|
||||||
state.sweep_for_advancements() # key drop keys repeatedly
|
state.sweep_for_events() # key drop keys repeatedly
|
||||||
|
|
||||||
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
|
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
from unittest import TestCase
|
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
|
||||||
from test.general import gen_steps, setup_multiworld
|
|
||||||
from worlds.AutoWorld import call_all
|
|
||||||
from worlds.generic.Rules import locality_rules
|
|
||||||
from ... import ALTTPWorld
|
|
||||||
from ...Options import DungeonItem
|
|
||||||
|
|
||||||
|
|
||||||
class DungeonFillTestBase(TestCase):
|
|
||||||
multiworld: MultiWorld
|
|
||||||
world_1: ALTTPWorld
|
|
||||||
world_2: ALTTPWorld
|
|
||||||
options = (
|
|
||||||
"big_key_shuffle",
|
|
||||||
"small_key_shuffle",
|
|
||||||
"key_drop_shuffle",
|
|
||||||
"compass_shuffle",
|
|
||||||
"map_shuffle",
|
|
||||||
)
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.multiworld = setup_multiworld([ALTTPWorld, ALTTPWorld], ())
|
|
||||||
self.world_1 = self.multiworld.worlds[1]
|
|
||||||
self.world_2 = self.multiworld.worlds[2]
|
|
||||||
|
|
||||||
def generate_with_options(self, option_value: int):
|
|
||||||
for option in self.options:
|
|
||||||
getattr(self.world_1.options, option).value = getattr(self.world_2.options, option).value = option_value
|
|
||||||
|
|
||||||
for step in gen_steps:
|
|
||||||
call_all(self.multiworld, step)
|
|
||||||
# this is where locality rules are set in normal generation which we need to verify this test
|
|
||||||
if step == "set_rules":
|
|
||||||
locality_rules(self.multiworld)
|
|
||||||
|
|
||||||
def test_original_dungeons(self):
|
|
||||||
self.generate_with_options(DungeonItem.option_original_dungeon)
|
|
||||||
for location in self.multiworld.get_filled_locations():
|
|
||||||
with (self.subTest(location=location)):
|
|
||||||
if location.parent_region.dungeon is None:
|
|
||||||
self.assertIs(location.item.dungeon, None)
|
|
||||||
else:
|
|
||||||
self.assertEqual(location.player, location.item.player,
|
|
||||||
f"{location.item} does not belong to {location}'s player")
|
|
||||||
if location.item.dungeon is None:
|
|
||||||
continue
|
|
||||||
self.assertIs(location.item.dungeon, location.parent_region.dungeon,
|
|
||||||
f"{location.item} was not placed in its original dungeon.")
|
|
||||||
|
|
||||||
def test_own_dungeons(self):
|
|
||||||
self.generate_with_options(DungeonItem.option_own_dungeons)
|
|
||||||
for location in self.multiworld.get_filled_locations():
|
|
||||||
with self.subTest(location=location):
|
|
||||||
if location.parent_region.dungeon is None:
|
|
||||||
self.assertIs(location.item.dungeon, None)
|
|
||||||
else:
|
|
||||||
self.assertEqual(location.player, location.item.player,
|
|
||||||
f"{location.item} does not belong to {location}'s player")
|
|
||||||
@@ -4,7 +4,7 @@ from BaseClasses import Tutorial
|
|||||||
from ..AutoWorld import WebWorld, World
|
from ..AutoWorld import WebWorld, World
|
||||||
|
|
||||||
class AP_SudokuWebWorld(WebWorld):
|
class AP_SudokuWebWorld(WebWorld):
|
||||||
options_page = False
|
options_page = "games/Sudoku/info/en"
|
||||||
theme = 'partyTime'
|
theme = 'partyTime'
|
||||||
|
|
||||||
setup_en = Tutorial(
|
setup_en = Tutorial(
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
# APSudoku Setup Guide
|
# APSudoku Setup Guide
|
||||||
|
|
||||||
## Required Software
|
## Required Software
|
||||||
- [APSudoku](https://github.com/APSudoku/APSudoku)
|
- [APSudoku](https://github.com/EmilyV99/APSudoku)
|
||||||
|
- Windows (most tested on Win10)
|
||||||
|
- Other platforms might be able to build from source themselves; and may be included in the future.
|
||||||
|
|
||||||
## General Concept
|
## General Concept
|
||||||
|
|
||||||
@@ -11,33 +13,25 @@ Does not need to be added at the start of a seed, as it does not create any slot
|
|||||||
|
|
||||||
## Installation Procedures
|
## Installation Procedures
|
||||||
|
|
||||||
Go to the latest release from the [APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform.
|
Go to the latest release from the [APSudoku Releases page](https://github.com/EmilyV99/APSudoku/releases). Download and extract the `APSudoku.zip` file.
|
||||||
|
|
||||||
## Joining a MultiWorld Game
|
## Joining a MultiWorld Game
|
||||||
|
|
||||||
1. Run the APSudoku executable.
|
1. Run APSudoku.exe
|
||||||
2. Under `Settings` → `Connection` at the top-right:
|
2. Under the 'Archipelago' tab at the top-right:
|
||||||
- Enter the server address and port number
|
- Enter the server url & port number
|
||||||
- Enter the name of the slot you wish to connect to
|
- Enter the name of the slot you wish to connect to
|
||||||
- Enter the room password (optional)
|
- Enter the room password (optional)
|
||||||
- Select DeathLink related settings (optional)
|
- Select DeathLink related settings (optional)
|
||||||
- Press `Connect`
|
- Press connect
|
||||||
4. Under the `Sudoku` tab
|
3. Go back to the 'Sudoku' tab
|
||||||
- Choose puzzle difficulty
|
- Click the various '?' buttons for information on how to play / control
|
||||||
- Click `Start` to generate a puzzle
|
4. Choose puzzle difficulty
|
||||||
5. Try to solve the Sudoku. Click `Check` when done
|
5. Try to solve the Sudoku. Click 'Check' when done.
|
||||||
- A correct solution rewards you with 1 hint for a location in the world you are connected to
|
|
||||||
- An incorrect solution has no penalty, unless DeathLink is enabled (see below)
|
|
||||||
|
|
||||||
Info:
|
|
||||||
- You can set various settings under `Settings` → `Sudoku`, and can change the colors used under `Settings` → `Theme`.
|
|
||||||
- While connected, you can view the `Console` and `Hints` tabs for standard TextClient-like features
|
|
||||||
- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md)
|
|
||||||
- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted)
|
|
||||||
- Click the various `?` buttons for information on controls/how to play
|
|
||||||
## DeathLink Support
|
## DeathLink Support
|
||||||
|
|
||||||
If `DeathLink` is enabled when you click `Connect`:
|
If 'DeathLink' is enabled when you click 'Connect':
|
||||||
- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or if you quit a puzzle without solving it (including disconnecting).
|
- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or quit a puzzle without solving it (including disconnecting).
|
||||||
- Your life count is customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
|
- Life count customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
|
||||||
- On receiving a DeathLink from another player, your puzzle resets.
|
- On receiving a DeathLink from another player, your puzzle resets.
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ item_table = {
|
|||||||
"Mutant Costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume
|
"Mutant Costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume
|
||||||
"Baby Nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus
|
"Baby Nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus
|
||||||
"Baby Piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha
|
"Baby Piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha
|
||||||
"Arnassi Armor": ItemData(698023, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_seahorse_costume
|
"Arnassi Armor": ItemData(698023, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_seahorse_costume
|
||||||
"Seed Bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag
|
"Seed Bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag
|
||||||
"King's Skull": ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull
|
"King's Skull": ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull
|
||||||
"Song Plant Spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed
|
"Song Plant Spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class AquariaLocations:
|
|||||||
"Home Water, bulb below the grouper fish": 698058,
|
"Home Water, bulb below the grouper fish": 698058,
|
||||||
"Home Water, bulb in the path below Nautilus Prime": 698059,
|
"Home Water, bulb in the path below Nautilus Prime": 698059,
|
||||||
"Home Water, bulb in the little room above the grouper fish": 698060,
|
"Home Water, bulb in the little room above the grouper fish": 698060,
|
||||||
"Home Water, bulb in the end of the path close to the Verse Cave": 698061,
|
"Home Water, bulb in the end of the left path from the Verse Cave": 698061,
|
||||||
"Home Water, bulb in the top left path": 698062,
|
"Home Water, bulb in the top left path": 698062,
|
||||||
"Home Water, bulb in the bottom left room": 698063,
|
"Home Water, bulb in the bottom left room": 698063,
|
||||||
"Home Water, bulb close to Naija's Home": 698064,
|
"Home Water, bulb close to Naija's Home": 698064,
|
||||||
@@ -67,7 +67,7 @@ class AquariaLocations:
|
|||||||
|
|
||||||
locations_song_cave = {
|
locations_song_cave = {
|
||||||
"Song Cave, Erulian spirit": 698206,
|
"Song Cave, Erulian spirit": 698206,
|
||||||
"Song Cave, bulb in the top right part": 698071,
|
"Song Cave, bulb in the top left part": 698071,
|
||||||
"Song Cave, bulb in the big anemone room": 698072,
|
"Song Cave, bulb in the big anemone room": 698072,
|
||||||
"Song Cave, bulb in the path to the singing statues": 698073,
|
"Song Cave, bulb in the path to the singing statues": 698073,
|
||||||
"Song Cave, bulb under the rock in the path to the singing statues": 698074,
|
"Song Cave, bulb under the rock in the path to the singing statues": 698074,
|
||||||
@@ -152,9 +152,6 @@ class AquariaLocations:
|
|||||||
|
|
||||||
locations_arnassi_path = {
|
locations_arnassi_path = {
|
||||||
"Arnassi Ruins, Arnassi Statue": 698164,
|
"Arnassi Ruins, Arnassi Statue": 698164,
|
||||||
}
|
|
||||||
|
|
||||||
locations_arnassi_cave_transturtle = {
|
|
||||||
"Arnassi Ruins, Transturtle": 698217,
|
"Arnassi Ruins, Transturtle": 698217,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,12 +269,9 @@ class AquariaLocations:
|
|||||||
}
|
}
|
||||||
|
|
||||||
locations_forest_bl = {
|
locations_forest_bl = {
|
||||||
"Kelp Forest bottom left area, Transturtle": 698212,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_forest_bl_sc = {
|
|
||||||
"Kelp Forest bottom left area, bulb close to the spirit crystals": 698054,
|
"Kelp Forest bottom left area, bulb close to the spirit crystals": 698054,
|
||||||
"Kelp Forest bottom left area, Walker Baby": 698186,
|
"Kelp Forest bottom left area, Walker Baby": 698186,
|
||||||
|
"Kelp Forest bottom left area, Transturtle": 698212,
|
||||||
}
|
}
|
||||||
|
|
||||||
locations_forest_br = {
|
locations_forest_br = {
|
||||||
@@ -376,7 +370,7 @@ class AquariaLocations:
|
|||||||
|
|
||||||
locations_sun_temple_r = {
|
locations_sun_temple_r = {
|
||||||
"Sun Temple, first bulb of the temple": 698091,
|
"Sun Temple, first bulb of the temple": 698091,
|
||||||
"Sun Temple, bulb on the right part": 698092,
|
"Sun Temple, bulb on the left part": 698092,
|
||||||
"Sun Temple, bulb in the hidden room of the right part": 698093,
|
"Sun Temple, bulb in the hidden room of the right part": 698093,
|
||||||
"Sun Temple, Sun Key": 698182,
|
"Sun Temple, Sun Key": 698182,
|
||||||
}
|
}
|
||||||
@@ -408,9 +402,6 @@ class AquariaLocations:
|
|||||||
"Abyss right area, bulb in the middle path": 698110,
|
"Abyss right area, bulb in the middle path": 698110,
|
||||||
"Abyss right area, bulb behind the rock in the middle path": 698111,
|
"Abyss right area, bulb behind the rock in the middle path": 698111,
|
||||||
"Abyss right area, bulb in the left green room": 698112,
|
"Abyss right area, bulb in the left green room": 698112,
|
||||||
}
|
|
||||||
|
|
||||||
locations_abyss_r_transturtle = {
|
|
||||||
"Abyss right area, Transturtle": 698214,
|
"Abyss right area, Transturtle": 698214,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,7 +499,6 @@ location_table = {
|
|||||||
**AquariaLocations.locations_skeleton_path_sc,
|
**AquariaLocations.locations_skeleton_path_sc,
|
||||||
**AquariaLocations.locations_arnassi,
|
**AquariaLocations.locations_arnassi,
|
||||||
**AquariaLocations.locations_arnassi_path,
|
**AquariaLocations.locations_arnassi_path,
|
||||||
**AquariaLocations.locations_arnassi_cave_transturtle,
|
|
||||||
**AquariaLocations.locations_arnassi_crab_boss,
|
**AquariaLocations.locations_arnassi_crab_boss,
|
||||||
**AquariaLocations.locations_sun_temple_l,
|
**AquariaLocations.locations_sun_temple_l,
|
||||||
**AquariaLocations.locations_sun_temple_r,
|
**AquariaLocations.locations_sun_temple_r,
|
||||||
@@ -519,7 +509,6 @@ location_table = {
|
|||||||
**AquariaLocations.locations_abyss_l,
|
**AquariaLocations.locations_abyss_l,
|
||||||
**AquariaLocations.locations_abyss_lb,
|
**AquariaLocations.locations_abyss_lb,
|
||||||
**AquariaLocations.locations_abyss_r,
|
**AquariaLocations.locations_abyss_r,
|
||||||
**AquariaLocations.locations_abyss_r_transturtle,
|
|
||||||
**AquariaLocations.locations_energy_temple_1,
|
**AquariaLocations.locations_energy_temple_1,
|
||||||
**AquariaLocations.locations_energy_temple_2,
|
**AquariaLocations.locations_energy_temple_2,
|
||||||
**AquariaLocations.locations_energy_temple_3,
|
**AquariaLocations.locations_energy_temple_3,
|
||||||
@@ -541,7 +530,6 @@ location_table = {
|
|||||||
**AquariaLocations.locations_forest_tr,
|
**AquariaLocations.locations_forest_tr,
|
||||||
**AquariaLocations.locations_forest_tr_fp,
|
**AquariaLocations.locations_forest_tr_fp,
|
||||||
**AquariaLocations.locations_forest_bl,
|
**AquariaLocations.locations_forest_bl,
|
||||||
**AquariaLocations.locations_forest_bl_sc,
|
|
||||||
**AquariaLocations.locations_forest_br,
|
**AquariaLocations.locations_forest_br,
|
||||||
**AquariaLocations.locations_forest_boss,
|
**AquariaLocations.locations_forest_boss,
|
||||||
**AquariaLocations.locations_forest_boss_entrance,
|
**AquariaLocations.locations_forest_boss_entrance,
|
||||||
|
|||||||
@@ -14,112 +14,97 @@ from worlds.generic.Rules import add_rule, set_rule
|
|||||||
|
|
||||||
# Every condition to connect regions
|
# Every condition to connect regions
|
||||||
|
|
||||||
def _has_hot_soup(state: CollectionState, player: int) -> bool:
|
def _has_hot_soup(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has the hotsoup item"""
|
"""`player` in `state` has the hotsoup item"""
|
||||||
return state.has_any({"Hot soup", "Hot soup x 2"}, player)
|
return state.has("Hot soup", player)
|
||||||
|
|
||||||
|
|
||||||
def _has_tongue_cleared(state: CollectionState, player: int) -> bool:
|
def _has_tongue_cleared(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has the Body tongue cleared item"""
|
"""`player` in `state` has the Body tongue cleared item"""
|
||||||
return state.has("Body tongue cleared", player)
|
return state.has("Body tongue cleared", player)
|
||||||
|
|
||||||
|
|
||||||
def _has_sun_crystal(state: CollectionState, player: int) -> bool:
|
def _has_sun_crystal(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has the Sun crystal item"""
|
"""`player` in `state` has the Sun crystal item"""
|
||||||
return state.has("Has sun crystal", player) and _has_bind_song(state, player)
|
return state.has("Has sun crystal", player) and _has_bind_song(state, player)
|
||||||
|
|
||||||
|
|
||||||
def _has_li(state: CollectionState, player: int) -> bool:
|
def _has_li(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has Li in its team"""
|
"""`player` in `state` has Li in its team"""
|
||||||
return state.has("Li and Li song", player)
|
return state.has("Li and Li song", player)
|
||||||
|
|
||||||
|
|
||||||
def _has_damaging_item(state: CollectionState, player: int) -> bool:
|
def _has_damaging_item(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has the shield song item"""
|
"""`player` in `state` has the shield song item"""
|
||||||
return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby Nautilus",
|
return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby Nautilus",
|
||||||
"Baby Piranha", "Baby Blaster"}, player)
|
"Baby Piranha", "Baby Blaster"}, player)
|
||||||
|
|
||||||
|
|
||||||
def _has_energy_attack_item(state: CollectionState, player: int) -> bool:
|
def _has_shield_song(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has items that can do a lot of damage (enough to beat bosses)"""
|
|
||||||
return _has_energy_form(state, player) or _has_dual_form(state, player)
|
|
||||||
|
|
||||||
|
|
||||||
def _has_shield_song(state: CollectionState, player: int) -> bool:
|
|
||||||
"""`player` in `state` has the shield song item"""
|
"""`player` in `state` has the shield song item"""
|
||||||
return state.has("Shield song", player)
|
return state.has("Shield song", player)
|
||||||
|
|
||||||
|
|
||||||
def _has_bind_song(state: CollectionState, player: int) -> bool:
|
def _has_bind_song(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has the bind song item"""
|
"""`player` in `state` has the bind song item"""
|
||||||
return state.has("Bind song", player)
|
return state.has("Bind song", player)
|
||||||
|
|
||||||
|
|
||||||
def _has_energy_form(state: CollectionState, player: int) -> bool:
|
def _has_energy_form(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has the energy form item"""
|
"""`player` in `state` has the energy form item"""
|
||||||
return state.has("Energy form", player)
|
return state.has("Energy form", player)
|
||||||
|
|
||||||
|
|
||||||
def _has_beast_form(state: CollectionState, player: int) -> bool:
|
def _has_beast_form(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has the beast form item"""
|
"""`player` in `state` has the beast form item"""
|
||||||
return state.has("Beast form", player)
|
return state.has("Beast form", player)
|
||||||
|
|
||||||
|
|
||||||
def _has_beast_and_soup_form(state: CollectionState, player: int) -> bool:
|
def _has_nature_form(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has the beast form item"""
|
|
||||||
return _has_beast_form(state, player) and _has_hot_soup(state, player)
|
|
||||||
|
|
||||||
|
|
||||||
def _has_beast_form_or_arnassi_armor(state: CollectionState, player: int) -> bool:
|
|
||||||
"""`player` in `state` has the beast form item"""
|
|
||||||
return _has_beast_form(state, player) or state.has("Arnassi Armor", player)
|
|
||||||
|
|
||||||
|
|
||||||
def _has_nature_form(state: CollectionState, player: int) -> bool:
|
|
||||||
"""`player` in `state` has the nature form item"""
|
"""`player` in `state` has the nature form item"""
|
||||||
return state.has("Nature form", player)
|
return state.has("Nature form", player)
|
||||||
|
|
||||||
|
|
||||||
def _has_sun_form(state: CollectionState, player: int) -> bool:
|
def _has_sun_form(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has the sun form item"""
|
"""`player` in `state` has the sun form item"""
|
||||||
return state.has("Sun form", player)
|
return state.has("Sun form", player)
|
||||||
|
|
||||||
|
|
||||||
def _has_light(state: CollectionState, player: int) -> bool:
|
def _has_light(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has the light item"""
|
"""`player` in `state` has the light item"""
|
||||||
return state.has("Baby Dumbo", player) or _has_sun_form(state, player)
|
return state.has("Baby Dumbo", player) or _has_sun_form(state, player)
|
||||||
|
|
||||||
|
|
||||||
def _has_dual_form(state: CollectionState, player: int) -> bool:
|
def _has_dual_form(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has the dual form item"""
|
"""`player` in `state` has the dual form item"""
|
||||||
return _has_li(state, player) and state.has("Dual form", player)
|
return _has_li(state, player) and state.has("Dual form", player)
|
||||||
|
|
||||||
|
|
||||||
def _has_fish_form(state: CollectionState, player: int) -> bool:
|
def _has_fish_form(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has the fish form item"""
|
"""`player` in `state` has the fish form item"""
|
||||||
return state.has("Fish form", player)
|
return state.has("Fish form", player)
|
||||||
|
|
||||||
|
|
||||||
def _has_spirit_form(state: CollectionState, player: int) -> bool:
|
def _has_spirit_form(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has the spirit form item"""
|
"""`player` in `state` has the spirit form item"""
|
||||||
return state.has("Spirit form", player)
|
return state.has("Spirit form", player)
|
||||||
|
|
||||||
|
|
||||||
def _has_big_bosses(state: CollectionState, player: int) -> bool:
|
def _has_big_bosses(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has beated every big bosses"""
|
"""`player` in `state` has beated every big bosses"""
|
||||||
return state.has_all({"Fallen God beated", "Mithalan God beated", "Drunian God beated",
|
return state.has_all({"Fallen God beated", "Mithalan God beated", "Drunian God beated",
|
||||||
"Sun God beated", "The Golem beated"}, player)
|
"Sun God beated", "The Golem beated"}, player)
|
||||||
|
|
||||||
|
|
||||||
def _has_mini_bosses(state: CollectionState, player: int) -> bool:
|
def _has_mini_bosses(state:CollectionState, player: int) -> bool:
|
||||||
"""`player` in `state` has beated every big bosses"""
|
"""`player` in `state` has beated every big bosses"""
|
||||||
return state.has_all({"Nautilus Prime beated", "Blaster Peg Prime beated", "Mergog beated",
|
return state.has_all({"Nautilus Prime beated", "Blaster Peg Prime beated", "Mergog beated",
|
||||||
"Mithalan priests beated", "Octopus Prime beated", "Crabbius Maximus beated",
|
"Mithalan priests beated", "Octopus Prime beated", "Crabbius Maximus beated",
|
||||||
"Mantis Shrimp Prime beated", "King Jellyfish God Prime beated"}, player)
|
"Mantis Shrimp Prime beated", "King Jellyfish God Prime beated"}, player)
|
||||||
|
|
||||||
|
|
||||||
def _has_secrets(state: CollectionState, player: int) -> bool:
|
def _has_secrets(state:CollectionState, player: int) -> bool:
|
||||||
return state.has_all({"First secret obtained", "Second secret obtained", "Third secret obtained"}, player)
|
return state.has_all({"First secret obtained", "Second secret obtained", "Third secret obtained"},player)
|
||||||
|
|
||||||
|
|
||||||
class AquariaRegions:
|
class AquariaRegions:
|
||||||
@@ -149,7 +134,6 @@ class AquariaRegions:
|
|||||||
skeleton_path: Region
|
skeleton_path: Region
|
||||||
skeleton_path_sc: Region
|
skeleton_path_sc: Region
|
||||||
arnassi: Region
|
arnassi: Region
|
||||||
arnassi_cave_transturtle: Region
|
|
||||||
arnassi_path: Region
|
arnassi_path: Region
|
||||||
arnassi_crab_boss: Region
|
arnassi_crab_boss: Region
|
||||||
simon: Region
|
simon: Region
|
||||||
@@ -168,7 +152,6 @@ class AquariaRegions:
|
|||||||
forest_tr: Region
|
forest_tr: Region
|
||||||
forest_tr_fp: Region
|
forest_tr_fp: Region
|
||||||
forest_bl: Region
|
forest_bl: Region
|
||||||
forest_bl_sc: Region
|
|
||||||
forest_br: Region
|
forest_br: Region
|
||||||
forest_boss: Region
|
forest_boss: Region
|
||||||
forest_boss_entrance: Region
|
forest_boss_entrance: Region
|
||||||
@@ -196,7 +179,6 @@ class AquariaRegions:
|
|||||||
abyss_l: Region
|
abyss_l: Region
|
||||||
abyss_lb: Region
|
abyss_lb: Region
|
||||||
abyss_r: Region
|
abyss_r: Region
|
||||||
abyss_r_transturtle: Region
|
|
||||||
ice_cave: Region
|
ice_cave: Region
|
||||||
bubble_cave: Region
|
bubble_cave: Region
|
||||||
bubble_cave_boss: Region
|
bubble_cave_boss: Region
|
||||||
@@ -231,7 +213,7 @@ class AquariaRegions:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __add_region(self, hint: str,
|
def __add_region(self, hint: str,
|
||||||
locations: Optional[Dict[str, int]]) -> Region:
|
locations: Optional[Dict[str, Optional[int]]]) -> Region:
|
||||||
"""
|
"""
|
||||||
Create a new Region, add it to the `world` regions and return it.
|
Create a new Region, add it to the `world` regions and return it.
|
||||||
Be aware that this function have a side effect on ``world`.`regions`
|
Be aware that this function have a side effect on ``world`.`regions`
|
||||||
@@ -254,7 +236,7 @@ class AquariaRegions:
|
|||||||
self.home_water_nautilus = self.__add_region("Home Water, Nautilus nest",
|
self.home_water_nautilus = self.__add_region("Home Water, Nautilus nest",
|
||||||
AquariaLocations.locations_home_water_nautilus)
|
AquariaLocations.locations_home_water_nautilus)
|
||||||
self.home_water_transturtle = self.__add_region("Home Water, turtle room",
|
self.home_water_transturtle = self.__add_region("Home Water, turtle room",
|
||||||
AquariaLocations.locations_home_water_transturtle)
|
AquariaLocations.locations_home_water_transturtle)
|
||||||
self.naija_home = self.__add_region("Naija's Home", AquariaLocations.locations_naija_home)
|
self.naija_home = self.__add_region("Naija's Home", AquariaLocations.locations_naija_home)
|
||||||
self.song_cave = self.__add_region("Song Cave", AquariaLocations.locations_song_cave)
|
self.song_cave = self.__add_region("Song Cave", AquariaLocations.locations_song_cave)
|
||||||
|
|
||||||
@@ -298,8 +280,6 @@ class AquariaRegions:
|
|||||||
self.arnassi = self.__add_region("Arnassi Ruins", AquariaLocations.locations_arnassi)
|
self.arnassi = self.__add_region("Arnassi Ruins", AquariaLocations.locations_arnassi)
|
||||||
self.arnassi_path = self.__add_region("Arnassi Ruins, back entrance path",
|
self.arnassi_path = self.__add_region("Arnassi Ruins, back entrance path",
|
||||||
AquariaLocations.locations_arnassi_path)
|
AquariaLocations.locations_arnassi_path)
|
||||||
self.arnassi_cave_transturtle = self.__add_region("Arnassi Ruins, transturtle area",
|
|
||||||
AquariaLocations.locations_arnassi_cave_transturtle)
|
|
||||||
self.arnassi_crab_boss = self.__add_region("Arnassi Ruins, Crabbius Maximus lair",
|
self.arnassi_crab_boss = self.__add_region("Arnassi Ruins, Crabbius Maximus lair",
|
||||||
AquariaLocations.locations_arnassi_crab_boss)
|
AquariaLocations.locations_arnassi_crab_boss)
|
||||||
|
|
||||||
@@ -322,9 +302,9 @@ class AquariaRegions:
|
|||||||
AquariaLocations.locations_cathedral_r)
|
AquariaLocations.locations_cathedral_r)
|
||||||
self.cathedral_underground = self.__add_region("Mithalas Cathedral underground",
|
self.cathedral_underground = self.__add_region("Mithalas Cathedral underground",
|
||||||
AquariaLocations.locations_cathedral_underground)
|
AquariaLocations.locations_cathedral_underground)
|
||||||
self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, Mithalan God room", None)
|
self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, Mithalan God room",
|
||||||
self.cathedral_boss_l = self.__add_region("Mithalas Cathedral, after Mithalan God room",
|
|
||||||
AquariaLocations.locations_cathedral_boss)
|
AquariaLocations.locations_cathedral_boss)
|
||||||
|
self.cathedral_boss_l = self.__add_region("Mithalas Cathedral, after Mithalan God room", None)
|
||||||
|
|
||||||
def __create_forest(self) -> None:
|
def __create_forest(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -340,8 +320,6 @@ class AquariaRegions:
|
|||||||
AquariaLocations.locations_forest_tr_fp)
|
AquariaLocations.locations_forest_tr_fp)
|
||||||
self.forest_bl = self.__add_region("Kelp Forest bottom left area",
|
self.forest_bl = self.__add_region("Kelp Forest bottom left area",
|
||||||
AquariaLocations.locations_forest_bl)
|
AquariaLocations.locations_forest_bl)
|
||||||
self.forest_bl_sc = self.__add_region("Kelp Forest bottom left area, spirit crystals",
|
|
||||||
AquariaLocations.locations_forest_bl_sc)
|
|
||||||
self.forest_br = self.__add_region("Kelp Forest bottom right area",
|
self.forest_br = self.__add_region("Kelp Forest bottom right area",
|
||||||
AquariaLocations.locations_forest_br)
|
AquariaLocations.locations_forest_br)
|
||||||
self.forest_sprite_cave = self.__add_region("Kelp Forest spirit cave",
|
self.forest_sprite_cave = self.__add_region("Kelp Forest spirit cave",
|
||||||
@@ -397,9 +375,9 @@ class AquariaRegions:
|
|||||||
self.sun_temple_r = self.__add_region("Sun Temple right area",
|
self.sun_temple_r = self.__add_region("Sun Temple right area",
|
||||||
AquariaLocations.locations_sun_temple_r)
|
AquariaLocations.locations_sun_temple_r)
|
||||||
self.sun_temple_boss_path = self.__add_region("Sun Temple before boss area",
|
self.sun_temple_boss_path = self.__add_region("Sun Temple before boss area",
|
||||||
AquariaLocations.locations_sun_temple_boss_path)
|
AquariaLocations.locations_sun_temple_boss_path)
|
||||||
self.sun_temple_boss = self.__add_region("Sun Temple boss area",
|
self.sun_temple_boss = self.__add_region("Sun Temple boss area",
|
||||||
AquariaLocations.locations_sun_temple_boss)
|
AquariaLocations.locations_sun_temple_boss)
|
||||||
|
|
||||||
def __create_abyss(self) -> None:
|
def __create_abyss(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -410,8 +388,6 @@ class AquariaRegions:
|
|||||||
AquariaLocations.locations_abyss_l)
|
AquariaLocations.locations_abyss_l)
|
||||||
self.abyss_lb = self.__add_region("Abyss left bottom area", AquariaLocations.locations_abyss_lb)
|
self.abyss_lb = self.__add_region("Abyss left bottom area", AquariaLocations.locations_abyss_lb)
|
||||||
self.abyss_r = self.__add_region("Abyss right area", AquariaLocations.locations_abyss_r)
|
self.abyss_r = self.__add_region("Abyss right area", AquariaLocations.locations_abyss_r)
|
||||||
self.abyss_r_transturtle = self.__add_region("Abyss right area, transturtle",
|
|
||||||
AquariaLocations.locations_abyss_r_transturtle)
|
|
||||||
self.ice_cave = self.__add_region("Ice Cave", AquariaLocations.locations_ice_cave)
|
self.ice_cave = self.__add_region("Ice Cave", AquariaLocations.locations_ice_cave)
|
||||||
self.bubble_cave = self.__add_region("Bubble Cave", AquariaLocations.locations_bubble_cave)
|
self.bubble_cave = self.__add_region("Bubble Cave", AquariaLocations.locations_bubble_cave)
|
||||||
self.bubble_cave_boss = self.__add_region("Bubble Cave boss area", AquariaLocations.locations_bubble_cave_boss)
|
self.bubble_cave_boss = self.__add_region("Bubble Cave boss area", AquariaLocations.locations_bubble_cave_boss)
|
||||||
@@ -431,7 +407,7 @@ class AquariaRegions:
|
|||||||
self.sunken_city_r = self.__add_region("Sunken City right area",
|
self.sunken_city_r = self.__add_region("Sunken City right area",
|
||||||
AquariaLocations.locations_sunken_city_r)
|
AquariaLocations.locations_sunken_city_r)
|
||||||
self.sunken_city_boss = self.__add_region("Sunken City boss area",
|
self.sunken_city_boss = self.__add_region("Sunken City boss area",
|
||||||
AquariaLocations.locations_sunken_city_boss)
|
AquariaLocations.locations_sunken_city_boss)
|
||||||
|
|
||||||
def __create_body(self) -> None:
|
def __create_body(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -451,7 +427,7 @@ class AquariaRegions:
|
|||||||
self.final_boss_tube = self.__add_region("The Body, final boss area turtle room",
|
self.final_boss_tube = self.__add_region("The Body, final boss area turtle room",
|
||||||
AquariaLocations.locations_final_boss_tube)
|
AquariaLocations.locations_final_boss_tube)
|
||||||
self.final_boss = self.__add_region("The Body, final boss",
|
self.final_boss = self.__add_region("The Body, final boss",
|
||||||
AquariaLocations.locations_final_boss)
|
AquariaLocations.locations_final_boss)
|
||||||
self.final_boss_end = self.__add_region("The Body, final boss area", None)
|
self.final_boss_end = self.__add_region("The Body, final boss area", None)
|
||||||
|
|
||||||
def __connect_one_way_regions(self, source_name: str, destination_name: str,
|
def __connect_one_way_regions(self, source_name: str, destination_name: str,
|
||||||
@@ -479,8 +455,8 @@ class AquariaRegions:
|
|||||||
"""
|
"""
|
||||||
Connect entrances of the different regions around `home_water`
|
Connect entrances of the different regions around `home_water`
|
||||||
"""
|
"""
|
||||||
self.__connect_one_way_regions("Menu", "Verse Cave right area",
|
self.__connect_regions("Menu", "Verse Cave right area",
|
||||||
self.menu, self.verse_cave_r)
|
self.menu, self.verse_cave_r)
|
||||||
self.__connect_regions("Verse Cave left area", "Verse Cave right area",
|
self.__connect_regions("Verse Cave left area", "Verse Cave right area",
|
||||||
self.verse_cave_l, self.verse_cave_r)
|
self.verse_cave_l, self.verse_cave_r)
|
||||||
self.__connect_regions("Verse Cave", "Home Water", self.verse_cave_l, self.home_water)
|
self.__connect_regions("Verse Cave", "Home Water", self.verse_cave_l, self.home_water)
|
||||||
@@ -488,8 +464,7 @@ class AquariaRegions:
|
|||||||
self.__connect_regions("Home Water", "Song Cave", self.home_water, self.song_cave)
|
self.__connect_regions("Home Water", "Song Cave", self.home_water, self.song_cave)
|
||||||
self.__connect_regions("Home Water", "Home Water, nautilus nest",
|
self.__connect_regions("Home Water", "Home Water, nautilus nest",
|
||||||
self.home_water, self.home_water_nautilus,
|
self.home_water, self.home_water_nautilus,
|
||||||
lambda state: _has_energy_attack_item(state, self.player) and
|
lambda state: _has_energy_form(state, self.player) and _has_bind_song(state, self.player))
|
||||||
_has_bind_song(state, self.player))
|
|
||||||
self.__connect_regions("Home Water", "Home Water transturtle room",
|
self.__connect_regions("Home Water", "Home Water transturtle room",
|
||||||
self.home_water, self.home_water_transturtle)
|
self.home_water, self.home_water_transturtle)
|
||||||
self.__connect_regions("Home Water", "Energy Temple first area",
|
self.__connect_regions("Home Water", "Energy Temple first area",
|
||||||
@@ -497,7 +472,7 @@ class AquariaRegions:
|
|||||||
lambda state: _has_bind_song(state, self.player))
|
lambda state: _has_bind_song(state, self.player))
|
||||||
self.__connect_regions("Home Water", "Energy Temple_altar",
|
self.__connect_regions("Home Water", "Energy Temple_altar",
|
||||||
self.home_water, self.energy_temple_altar,
|
self.home_water, self.energy_temple_altar,
|
||||||
lambda state: _has_energy_attack_item(state, self.player) and
|
lambda state: _has_energy_form(state, self.player) and
|
||||||
_has_bind_song(state, self.player))
|
_has_bind_song(state, self.player))
|
||||||
self.__connect_regions("Energy Temple first area", "Energy Temple second area",
|
self.__connect_regions("Energy Temple first area", "Energy Temple second area",
|
||||||
self.energy_temple_1, self.energy_temple_2,
|
self.energy_temple_1, self.energy_temple_2,
|
||||||
@@ -507,28 +482,28 @@ class AquariaRegions:
|
|||||||
lambda state: _has_fish_form(state, self.player))
|
lambda state: _has_fish_form(state, self.player))
|
||||||
self.__connect_regions("Energy Temple idol room", "Energy Temple boss area",
|
self.__connect_regions("Energy Temple idol room", "Energy Temple boss area",
|
||||||
self.energy_temple_idol, self.energy_temple_boss,
|
self.energy_temple_idol, self.energy_temple_boss,
|
||||||
lambda state: _has_energy_attack_item(state, self.player) and
|
lambda state: _has_energy_form(state, self.player))
|
||||||
_has_fish_form(state, self.player))
|
|
||||||
self.__connect_one_way_regions("Energy Temple first area", "Energy Temple boss area",
|
self.__connect_one_way_regions("Energy Temple first area", "Energy Temple boss area",
|
||||||
self.energy_temple_1, self.energy_temple_boss,
|
self.energy_temple_1, self.energy_temple_boss,
|
||||||
lambda state: _has_beast_form(state, self.player) and
|
lambda state: _has_beast_form(state, self.player) and
|
||||||
_has_energy_attack_item(state, self.player))
|
_has_energy_form(state, self.player))
|
||||||
self.__connect_one_way_regions("Energy Temple boss area", "Energy Temple first area",
|
self.__connect_one_way_regions("Energy Temple boss area", "Energy Temple first area",
|
||||||
self.energy_temple_boss, self.energy_temple_1,
|
self.energy_temple_boss, self.energy_temple_1,
|
||||||
lambda state: _has_energy_attack_item(state, self.player))
|
lambda state: _has_energy_form(state, self.player))
|
||||||
self.__connect_regions("Energy Temple second area", "Energy Temple third area",
|
self.__connect_regions("Energy Temple second area", "Energy Temple third area",
|
||||||
self.energy_temple_2, self.energy_temple_3,
|
self.energy_temple_2, self.energy_temple_3,
|
||||||
lambda state: _has_energy_form(state, self.player))
|
lambda state: _has_bind_song(state, self.player) and
|
||||||
|
_has_energy_form(state, self.player))
|
||||||
self.__connect_regions("Energy Temple boss area", "Energy Temple blaster room",
|
self.__connect_regions("Energy Temple boss area", "Energy Temple blaster room",
|
||||||
self.energy_temple_boss, self.energy_temple_blaster_room,
|
self.energy_temple_boss, self.energy_temple_blaster_room,
|
||||||
lambda state: _has_nature_form(state, self.player) and
|
lambda state: _has_nature_form(state, self.player) and
|
||||||
_has_bind_song(state, self.player) and
|
_has_bind_song(state, self.player) and
|
||||||
_has_energy_attack_item(state, self.player))
|
_has_energy_form(state, self.player))
|
||||||
self.__connect_regions("Energy Temple first area", "Energy Temple blaster room",
|
self.__connect_regions("Energy Temple first area", "Energy Temple blaster room",
|
||||||
self.energy_temple_1, self.energy_temple_blaster_room,
|
self.energy_temple_1, self.energy_temple_blaster_room,
|
||||||
lambda state: _has_nature_form(state, self.player) and
|
lambda state: _has_nature_form(state, self.player) and
|
||||||
_has_bind_song(state, self.player) and
|
_has_bind_song(state, self.player) and
|
||||||
_has_energy_attack_item(state, self.player) and
|
_has_energy_form(state, self.player) and
|
||||||
_has_beast_form(state, self.player))
|
_has_beast_form(state, self.player))
|
||||||
self.__connect_regions("Home Water", "Open Water top left area",
|
self.__connect_regions("Home Water", "Open Water top left area",
|
||||||
self.home_water, self.openwater_tl)
|
self.home_water, self.openwater_tl)
|
||||||
@@ -545,7 +520,7 @@ class AquariaRegions:
|
|||||||
self.openwater_tl, self.forest_br)
|
self.openwater_tl, self.forest_br)
|
||||||
self.__connect_regions("Open Water top right area", "Open Water top right area, turtle room",
|
self.__connect_regions("Open Water top right area", "Open Water top right area, turtle room",
|
||||||
self.openwater_tr, self.openwater_tr_turtle,
|
self.openwater_tr, self.openwater_tr_turtle,
|
||||||
lambda state: _has_beast_form_or_arnassi_armor(state, self.player))
|
lambda state: _has_beast_form(state, self.player))
|
||||||
self.__connect_regions("Open Water top right area", "Open Water bottom right area",
|
self.__connect_regions("Open Water top right area", "Open Water bottom right area",
|
||||||
self.openwater_tr, self.openwater_br)
|
self.openwater_tr, self.openwater_br)
|
||||||
self.__connect_regions("Open Water top right area", "Mithalas City",
|
self.__connect_regions("Open Water top right area", "Mithalas City",
|
||||||
@@ -554,9 +529,10 @@ class AquariaRegions:
|
|||||||
self.openwater_tr, self.veil_bl)
|
self.openwater_tr, self.veil_bl)
|
||||||
self.__connect_one_way_regions("Open Water top right area", "Veil bottom right",
|
self.__connect_one_way_regions("Open Water top right area", "Veil bottom right",
|
||||||
self.openwater_tr, self.veil_br,
|
self.openwater_tr, self.veil_br,
|
||||||
lambda state: _has_beast_form_or_arnassi_armor(state, self.player))
|
lambda state: _has_beast_form(state, self.player))
|
||||||
self.__connect_one_way_regions("Veil bottom right", "Open Water top right area",
|
self.__connect_one_way_regions("Veil bottom right", "Open Water top right area",
|
||||||
self.veil_br, self.openwater_tr)
|
self.veil_br, self.openwater_tr,
|
||||||
|
lambda state: _has_beast_form(state, self.player))
|
||||||
self.__connect_regions("Open Water bottom left area", "Open Water bottom right area",
|
self.__connect_regions("Open Water bottom left area", "Open Water bottom right area",
|
||||||
self.openwater_bl, self.openwater_br)
|
self.openwater_bl, self.openwater_br)
|
||||||
self.__connect_regions("Open Water bottom left area", "Skeleton path",
|
self.__connect_regions("Open Water bottom left area", "Skeleton path",
|
||||||
@@ -575,14 +551,10 @@ class AquariaRegions:
|
|||||||
self.arnassi, self.openwater_br)
|
self.arnassi, self.openwater_br)
|
||||||
self.__connect_regions("Arnassi", "Arnassi path",
|
self.__connect_regions("Arnassi", "Arnassi path",
|
||||||
self.arnassi, self.arnassi_path)
|
self.arnassi, self.arnassi_path)
|
||||||
self.__connect_regions("Arnassi ruins, transturtle area", "Arnassi path",
|
|
||||||
self.arnassi_cave_transturtle, self.arnassi_path,
|
|
||||||
lambda state: _has_fish_form(state, self.player))
|
|
||||||
self.__connect_one_way_regions("Arnassi path", "Arnassi crab boss area",
|
self.__connect_one_way_regions("Arnassi path", "Arnassi crab boss area",
|
||||||
self.arnassi_path, self.arnassi_crab_boss,
|
self.arnassi_path, self.arnassi_crab_boss,
|
||||||
lambda state: _has_beast_form_or_arnassi_armor(state, self.player) and
|
lambda state: _has_beast_form(state, self.player) and
|
||||||
(_has_energy_attack_item(state, self.player) or
|
_has_energy_form(state, self.player))
|
||||||
_has_nature_form(state, self.player)))
|
|
||||||
self.__connect_one_way_regions("Arnassi crab boss area", "Arnassi path",
|
self.__connect_one_way_regions("Arnassi crab boss area", "Arnassi path",
|
||||||
self.arnassi_crab_boss, self.arnassi_path)
|
self.arnassi_crab_boss, self.arnassi_path)
|
||||||
|
|
||||||
@@ -592,62 +564,61 @@ class AquariaRegions:
|
|||||||
"""
|
"""
|
||||||
self.__connect_one_way_regions("Mithalas City", "Mithalas City top path",
|
self.__connect_one_way_regions("Mithalas City", "Mithalas City top path",
|
||||||
self.mithalas_city, self.mithalas_city_top_path,
|
self.mithalas_city, self.mithalas_city_top_path,
|
||||||
lambda state: _has_beast_form_or_arnassi_armor(state, self.player))
|
lambda state: _has_beast_form(state, self.player))
|
||||||
self.__connect_one_way_regions("Mithalas City_top_path", "Mithalas City",
|
self.__connect_one_way_regions("Mithalas City_top_path", "Mithalas City",
|
||||||
self.mithalas_city_top_path, self.mithalas_city)
|
self.mithalas_city_top_path, self.mithalas_city)
|
||||||
self.__connect_regions("Mithalas City", "Mithalas City home with fishpass",
|
self.__connect_regions("Mithalas City", "Mithalas City home with fishpass",
|
||||||
self.mithalas_city, self.mithalas_city_fishpass,
|
self.mithalas_city, self.mithalas_city_fishpass,
|
||||||
lambda state: _has_fish_form(state, self.player))
|
lambda state: _has_fish_form(state, self.player))
|
||||||
self.__connect_regions("Mithalas City", "Mithalas castle",
|
self.__connect_regions("Mithalas City", "Mithalas castle",
|
||||||
self.mithalas_city, self.cathedral_l)
|
self.mithalas_city, self.cathedral_l,
|
||||||
|
lambda state: _has_fish_form(state, self.player))
|
||||||
self.__connect_one_way_regions("Mithalas City top path", "Mithalas castle, flower tube",
|
self.__connect_one_way_regions("Mithalas City top path", "Mithalas castle, flower tube",
|
||||||
self.mithalas_city_top_path,
|
self.mithalas_city_top_path,
|
||||||
self.cathedral_l_tube,
|
self.cathedral_l_tube,
|
||||||
lambda state: _has_nature_form(state, self.player) and
|
lambda state: _has_nature_form(state, self.player) and
|
||||||
_has_energy_attack_item(state, self.player))
|
_has_energy_form(state, self.player))
|
||||||
self.__connect_one_way_regions("Mithalas castle, flower tube area", "Mithalas City top path",
|
self.__connect_one_way_regions("Mithalas castle, flower tube area", "Mithalas City top path",
|
||||||
self.cathedral_l_tube,
|
self.cathedral_l_tube,
|
||||||
self.mithalas_city_top_path,
|
self.mithalas_city_top_path,
|
||||||
lambda state: _has_nature_form(state, self.player))
|
lambda state: _has_beast_form(state, self.player) and
|
||||||
|
_has_nature_form(state, self.player))
|
||||||
self.__connect_one_way_regions("Mithalas castle flower tube area", "Mithalas castle, spirit crystals",
|
self.__connect_one_way_regions("Mithalas castle flower tube area", "Mithalas castle, spirit crystals",
|
||||||
self.cathedral_l_tube, self.cathedral_l_sc,
|
self.cathedral_l_tube, self.cathedral_l_sc,
|
||||||
lambda state: _has_spirit_form(state, self.player))
|
lambda state: _has_spirit_form(state, self.player))
|
||||||
self.__connect_one_way_regions("Mithalas castle_flower tube area", "Mithalas castle",
|
self.__connect_one_way_regions("Mithalas castle_flower tube area", "Mithalas castle",
|
||||||
self.cathedral_l_tube, self.cathedral_l,
|
self.cathedral_l_tube, self.cathedral_l,
|
||||||
lambda state: _has_spirit_form(state, self.player))
|
lambda state: _has_spirit_form(state, self.player))
|
||||||
self.__connect_regions("Mithalas castle", "Mithalas castle, spirit crystals",
|
self.__connect_regions("Mithalas castle", "Mithalas castle, spirit crystals",
|
||||||
self.cathedral_l, self.cathedral_l_sc,
|
self.cathedral_l, self.cathedral_l_sc,
|
||||||
lambda state: _has_spirit_form(state, self.player))
|
lambda state: _has_spirit_form(state, self.player))
|
||||||
self.__connect_one_way_regions("Mithalas castle", "Cathedral boss right area",
|
self.__connect_regions("Mithalas castle", "Cathedral boss left area",
|
||||||
self.cathedral_l, self.cathedral_boss_r,
|
self.cathedral_l, self.cathedral_boss_l,
|
||||||
lambda state: _has_beast_form(state, self.player))
|
lambda state: _has_beast_form(state, self.player) and
|
||||||
self.__connect_one_way_regions("Cathedral boss left area", "Mithalas castle",
|
_has_energy_form(state, self.player) and
|
||||||
self.cathedral_boss_l, self.cathedral_l,
|
_has_bind_song(state, self.player))
|
||||||
lambda state: _has_beast_form(state, self.player))
|
|
||||||
self.__connect_regions("Mithalas castle", "Mithalas Cathedral underground",
|
self.__connect_regions("Mithalas castle", "Mithalas Cathedral underground",
|
||||||
self.cathedral_l, self.cathedral_underground,
|
self.cathedral_l, self.cathedral_underground,
|
||||||
lambda state: _has_beast_form(state, self.player))
|
lambda state: _has_beast_form(state, self.player) and
|
||||||
self.__connect_one_way_regions("Mithalas castle", "Mithalas Cathedral",
|
_has_bind_song(state, self.player))
|
||||||
self.cathedral_l, self.cathedral_r,
|
self.__connect_regions("Mithalas castle", "Mithalas Cathedral",
|
||||||
lambda state: _has_bind_song(state, self.player) and
|
self.cathedral_l, self.cathedral_r,
|
||||||
_has_energy_attack_item(state, self.player))
|
lambda state: _has_bind_song(state, self.player) and
|
||||||
self.__connect_one_way_regions("Mithalas Cathedral", "Mithalas Cathedral underground",
|
_has_energy_form(state, self.player))
|
||||||
self.cathedral_r, self.cathedral_underground)
|
self.__connect_regions("Mithalas Cathedral", "Mithalas Cathedral underground",
|
||||||
self.__connect_one_way_regions("Mithalas Cathedral underground", "Mithalas Cathedral",
|
self.cathedral_r, self.cathedral_underground,
|
||||||
self.cathedral_underground, self.cathedral_r,
|
lambda state: _has_energy_form(state, self.player))
|
||||||
lambda state: _has_beast_form(state, self.player) and
|
self.__connect_one_way_regions("Mithalas Cathedral underground", "Cathedral boss left area",
|
||||||
_has_energy_attack_item(state, self.player))
|
self.cathedral_underground, self.cathedral_boss_r,
|
||||||
self.__connect_one_way_regions("Mithalas Cathedral underground", "Cathedral boss right area",
|
lambda state: _has_energy_form(state, self.player) and
|
||||||
self.cathedral_underground, self.cathedral_boss_r)
|
_has_bind_song(state, self.player))
|
||||||
self.__connect_one_way_regions("Cathedral boss right area", "Mithalas Cathedral underground",
|
self.__connect_one_way_regions("Cathedral boss left area", "Mithalas Cathedral underground",
|
||||||
self.cathedral_boss_r, self.cathedral_underground,
|
self.cathedral_boss_r, self.cathedral_underground,
|
||||||
lambda state: _has_beast_form(state, self.player))
|
lambda state: _has_beast_form(state, self.player))
|
||||||
self.__connect_one_way_regions("Cathedral boss right area", "Cathedral boss left area",
|
self.__connect_regions("Cathedral boss right area", "Cathedral boss left area",
|
||||||
self.cathedral_boss_r, self.cathedral_boss_l,
|
self.cathedral_boss_r, self.cathedral_boss_l,
|
||||||
lambda state: _has_bind_song(state, self.player) and
|
lambda state: _has_bind_song(state, self.player) and
|
||||||
_has_energy_attack_item(state, self.player))
|
_has_energy_form(state, self.player))
|
||||||
self.__connect_one_way_regions("Cathedral boss left area", "Cathedral boss right area",
|
|
||||||
self.cathedral_boss_l, self.cathedral_boss_r)
|
|
||||||
|
|
||||||
def __connect_forest_regions(self) -> None:
|
def __connect_forest_regions(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -657,12 +628,6 @@ class AquariaRegions:
|
|||||||
self.forest_br, self.veil_bl)
|
self.forest_br, self.veil_bl)
|
||||||
self.__connect_regions("Forest bottom right", "Forest bottom left area",
|
self.__connect_regions("Forest bottom right", "Forest bottom left area",
|
||||||
self.forest_br, self.forest_bl)
|
self.forest_br, self.forest_bl)
|
||||||
self.__connect_one_way_regions("Forest bottom left area", "Forest bottom left area, spirit crystals",
|
|
||||||
self.forest_bl, self.forest_bl_sc,
|
|
||||||
lambda state: _has_energy_attack_item(state, self.player) or
|
|
||||||
_has_fish_form(state, self.player))
|
|
||||||
self.__connect_one_way_regions("Forest bottom left area, spirit crystals", "Forest bottom left area",
|
|
||||||
self.forest_bl_sc, self.forest_bl)
|
|
||||||
self.__connect_regions("Forest bottom right", "Forest top right area",
|
self.__connect_regions("Forest bottom right", "Forest top right area",
|
||||||
self.forest_br, self.forest_tr)
|
self.forest_br, self.forest_tr)
|
||||||
self.__connect_regions("Forest bottom left area", "Forest fish cave",
|
self.__connect_regions("Forest bottom left area", "Forest fish cave",
|
||||||
@@ -676,7 +641,7 @@ class AquariaRegions:
|
|||||||
self.forest_tl, self.forest_tl_fp,
|
self.forest_tl, self.forest_tl_fp,
|
||||||
lambda state: _has_nature_form(state, self.player) and
|
lambda state: _has_nature_form(state, self.player) and
|
||||||
_has_bind_song(state, self.player) and
|
_has_bind_song(state, self.player) and
|
||||||
_has_energy_attack_item(state, self.player) and
|
_has_energy_form(state, self.player) and
|
||||||
_has_fish_form(state, self.player))
|
_has_fish_form(state, self.player))
|
||||||
self.__connect_regions("Forest top left area", "Forest top right area",
|
self.__connect_regions("Forest top left area", "Forest top right area",
|
||||||
self.forest_tl, self.forest_tr)
|
self.forest_tl, self.forest_tr)
|
||||||
@@ -684,7 +649,7 @@ class AquariaRegions:
|
|||||||
self.forest_tl, self.forest_boss_entrance)
|
self.forest_tl, self.forest_boss_entrance)
|
||||||
self.__connect_regions("Forest boss area", "Forest boss entrance",
|
self.__connect_regions("Forest boss area", "Forest boss entrance",
|
||||||
self.forest_boss, self.forest_boss_entrance,
|
self.forest_boss, self.forest_boss_entrance,
|
||||||
lambda state: _has_energy_attack_item(state, self.player))
|
lambda state: _has_energy_form(state, self.player))
|
||||||
self.__connect_regions("Forest top right area", "Forest top right area fish pass",
|
self.__connect_regions("Forest top right area", "Forest top right area fish pass",
|
||||||
self.forest_tr, self.forest_tr_fp,
|
self.forest_tr, self.forest_tr_fp,
|
||||||
lambda state: _has_fish_form(state, self.player))
|
lambda state: _has_fish_form(state, self.player))
|
||||||
@@ -698,7 +663,7 @@ class AquariaRegions:
|
|||||||
self.__connect_regions("Fermog cave", "Fermog boss",
|
self.__connect_regions("Fermog cave", "Fermog boss",
|
||||||
self.mermog_cave, self.mermog_boss,
|
self.mermog_cave, self.mermog_boss,
|
||||||
lambda state: _has_beast_form(state, self.player) and
|
lambda state: _has_beast_form(state, self.player) and
|
||||||
_has_energy_attack_item(state, self.player))
|
_has_energy_form(state, self.player))
|
||||||
|
|
||||||
def __connect_veil_regions(self) -> None:
|
def __connect_veil_regions(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -716,7 +681,8 @@ class AquariaRegions:
|
|||||||
self.veil_b_sc, self.veil_br,
|
self.veil_b_sc, self.veil_br,
|
||||||
lambda state: _has_spirit_form(state, self.player))
|
lambda state: _has_spirit_form(state, self.player))
|
||||||
self.__connect_regions("Veil bottom right", "Veil top left area",
|
self.__connect_regions("Veil bottom right", "Veil top left area",
|
||||||
self.veil_br, self.veil_tl)
|
self.veil_br, self.veil_tl,
|
||||||
|
lambda state: _has_beast_form(state, self.player))
|
||||||
self.__connect_regions("Veil top left area", "Veil_top left area, fish pass",
|
self.__connect_regions("Veil top left area", "Veil_top left area, fish pass",
|
||||||
self.veil_tl, self.veil_tl_fp,
|
self.veil_tl, self.veil_tl_fp,
|
||||||
lambda state: _has_fish_form(state, self.player))
|
lambda state: _has_fish_form(state, self.player))
|
||||||
@@ -725,25 +691,20 @@ class AquariaRegions:
|
|||||||
self.__connect_regions("Veil top left area", "Turtle cave",
|
self.__connect_regions("Veil top left area", "Turtle cave",
|
||||||
self.veil_tl, self.turtle_cave)
|
self.veil_tl, self.turtle_cave)
|
||||||
self.__connect_regions("Turtle cave", "Turtle cave Bubble Cliff",
|
self.__connect_regions("Turtle cave", "Turtle cave Bubble Cliff",
|
||||||
self.turtle_cave, self.turtle_cave_bubble)
|
self.turtle_cave, self.turtle_cave_bubble,
|
||||||
|
lambda state: _has_beast_form(state, self.player))
|
||||||
self.__connect_regions("Veil right of sun temple", "Sun Temple right area",
|
self.__connect_regions("Veil right of sun temple", "Sun Temple right area",
|
||||||
self.veil_tr_r, self.sun_temple_r)
|
self.veil_tr_r, self.sun_temple_r)
|
||||||
self.__connect_one_way_regions("Sun Temple right area", "Sun Temple left area",
|
self.__connect_regions("Sun Temple right area", "Sun Temple left area",
|
||||||
self.sun_temple_r, self.sun_temple_l,
|
self.sun_temple_r, self.sun_temple_l,
|
||||||
lambda state: _has_bind_song(state, self.player) or
|
lambda state: _has_bind_song(state, self.player))
|
||||||
_has_light(state, self.player))
|
|
||||||
self.__connect_one_way_regions("Sun Temple left area", "Sun Temple right area",
|
|
||||||
self.sun_temple_l, self.sun_temple_r,
|
|
||||||
lambda state: _has_light(state, self.player))
|
|
||||||
self.__connect_regions("Sun Temple left area", "Veil left of sun temple",
|
self.__connect_regions("Sun Temple left area", "Veil left of sun temple",
|
||||||
self.sun_temple_l, self.veil_tr_l)
|
self.sun_temple_l, self.veil_tr_l)
|
||||||
self.__connect_regions("Sun Temple left area", "Sun Temple before boss area",
|
self.__connect_regions("Sun Temple left area", "Sun Temple before boss area",
|
||||||
self.sun_temple_l, self.sun_temple_boss_path,
|
self.sun_temple_l, self.sun_temple_boss_path)
|
||||||
lambda state: _has_light(state, self.player) or
|
|
||||||
_has_sun_crystal(state, self.player))
|
|
||||||
self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area",
|
self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area",
|
||||||
self.sun_temple_boss_path, self.sun_temple_boss,
|
self.sun_temple_boss_path, self.sun_temple_boss,
|
||||||
lambda state: _has_energy_attack_item(state, self.player))
|
lambda state: _has_energy_form(state, self.player))
|
||||||
self.__connect_one_way_regions("Sun Temple boss area", "Veil left of sun temple",
|
self.__connect_one_way_regions("Sun Temple boss area", "Veil left of sun temple",
|
||||||
self.sun_temple_boss, self.veil_tr_l)
|
self.sun_temple_boss, self.veil_tr_l)
|
||||||
self.__connect_regions("Veil left of sun temple", "Octo cave top path",
|
self.__connect_regions("Veil left of sun temple", "Octo cave top path",
|
||||||
@@ -751,7 +712,7 @@ class AquariaRegions:
|
|||||||
lambda state: _has_fish_form(state, self.player) and
|
lambda state: _has_fish_form(state, self.player) and
|
||||||
_has_sun_form(state, self.player) and
|
_has_sun_form(state, self.player) and
|
||||||
_has_beast_form(state, self.player) and
|
_has_beast_form(state, self.player) and
|
||||||
_has_energy_attack_item(state, self.player))
|
_has_energy_form(state, self.player))
|
||||||
self.__connect_regions("Veil left of sun temple", "Octo cave bottom path",
|
self.__connect_regions("Veil left of sun temple", "Octo cave bottom path",
|
||||||
self.veil_tr_l, self.octo_cave_b,
|
self.veil_tr_l, self.octo_cave_b,
|
||||||
lambda state: _has_fish_form(state, self.player))
|
lambda state: _has_fish_form(state, self.player))
|
||||||
@@ -767,22 +728,16 @@ class AquariaRegions:
|
|||||||
self.abyss_lb, self.sunken_city_r,
|
self.abyss_lb, self.sunken_city_r,
|
||||||
lambda state: _has_li(state, self.player))
|
lambda state: _has_li(state, self.player))
|
||||||
self.__connect_one_way_regions("Abyss left bottom area", "Body center area",
|
self.__connect_one_way_regions("Abyss left bottom area", "Body center area",
|
||||||
self.abyss_lb, self.body_c,
|
self.abyss_lb, self.body_c,
|
||||||
lambda state: _has_tongue_cleared(state, self.player))
|
lambda state: _has_tongue_cleared(state, self.player))
|
||||||
self.__connect_one_way_regions("Body center area", "Abyss left bottom area",
|
self.__connect_one_way_regions("Body center area", "Abyss left bottom area",
|
||||||
self.body_c, self.abyss_lb)
|
self.body_c, self.abyss_lb)
|
||||||
self.__connect_regions("Abyss left area", "King jellyfish cave",
|
self.__connect_regions("Abyss left area", "King jellyfish cave",
|
||||||
self.abyss_l, self.king_jellyfish_cave,
|
self.abyss_l, self.king_jellyfish_cave,
|
||||||
lambda state: (_has_energy_form(state, self.player) and
|
lambda state: _has_energy_form(state, self.player) and
|
||||||
_has_beast_form(state, self.player)) or
|
_has_beast_form(state, self.player))
|
||||||
_has_dual_form(state, self.player))
|
|
||||||
self.__connect_regions("Abyss left area", "Abyss right area",
|
self.__connect_regions("Abyss left area", "Abyss right area",
|
||||||
self.abyss_l, self.abyss_r)
|
self.abyss_l, self.abyss_r)
|
||||||
self.__connect_one_way_regions("Abyss right area", "Abyss right area, transturtle",
|
|
||||||
self.abyss_r, self.abyss_r_transturtle)
|
|
||||||
self.__connect_one_way_regions("Abyss right area, transturtle", "Abyss right area",
|
|
||||||
self.abyss_r_transturtle, self.abyss_r,
|
|
||||||
lambda state: _has_light(state, self.player))
|
|
||||||
self.__connect_regions("Abyss right area", "Inside the whale",
|
self.__connect_regions("Abyss right area", "Inside the whale",
|
||||||
self.abyss_r, self.whale,
|
self.abyss_r, self.whale,
|
||||||
lambda state: _has_spirit_form(state, self.player) and
|
lambda state: _has_spirit_form(state, self.player) and
|
||||||
@@ -792,14 +747,13 @@ class AquariaRegions:
|
|||||||
lambda state: _has_spirit_form(state, self.player) and
|
lambda state: _has_spirit_form(state, self.player) and
|
||||||
_has_sun_form(state, self.player) and
|
_has_sun_form(state, self.player) and
|
||||||
_has_bind_song(state, self.player) and
|
_has_bind_song(state, self.player) and
|
||||||
_has_energy_attack_item(state, self.player))
|
_has_energy_form(state, self.player))
|
||||||
self.__connect_regions("Abyss right area", "Ice Cave",
|
self.__connect_regions("Abyss right area", "Ice Cave",
|
||||||
self.abyss_r, self.ice_cave,
|
self.abyss_r, self.ice_cave,
|
||||||
lambda state: _has_spirit_form(state, self.player))
|
lambda state: _has_spirit_form(state, self.player))
|
||||||
self.__connect_regions("Ice cave", "Bubble Cave",
|
self.__connect_regions("Abyss right area", "Bubble Cave",
|
||||||
self.ice_cave, self.bubble_cave,
|
self.ice_cave, self.bubble_cave,
|
||||||
lambda state: _has_beast_form(state, self.player) or
|
lambda state: _has_beast_form(state, self.player))
|
||||||
_has_hot_soup(state, self.player))
|
|
||||||
self.__connect_regions("Bubble Cave boss area", "Bubble Cave",
|
self.__connect_regions("Bubble Cave boss area", "Bubble Cave",
|
||||||
self.bubble_cave, self.bubble_cave_boss,
|
self.bubble_cave, self.bubble_cave_boss,
|
||||||
lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player)
|
lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player)
|
||||||
@@ -818,7 +772,7 @@ class AquariaRegions:
|
|||||||
self.sunken_city_l, self.sunken_city_boss,
|
self.sunken_city_l, self.sunken_city_boss,
|
||||||
lambda state: _has_beast_form(state, self.player) and
|
lambda state: _has_beast_form(state, self.player) and
|
||||||
_has_sun_form(state, self.player) and
|
_has_sun_form(state, self.player) and
|
||||||
_has_energy_attack_item(state, self.player) and
|
_has_energy_form(state, self.player) and
|
||||||
_has_bind_song(state, self.player))
|
_has_bind_song(state, self.player))
|
||||||
|
|
||||||
def __connect_body_regions(self) -> None:
|
def __connect_body_regions(self) -> None:
|
||||||
@@ -826,13 +780,11 @@ class AquariaRegions:
|
|||||||
Connect entrances of the different regions around The Body
|
Connect entrances of the different regions around The Body
|
||||||
"""
|
"""
|
||||||
self.__connect_regions("Body center area", "Body left area",
|
self.__connect_regions("Body center area", "Body left area",
|
||||||
self.body_c, self.body_l,
|
self.body_c, self.body_l)
|
||||||
lambda state: _has_energy_form(state, self.player))
|
|
||||||
self.__connect_regions("Body center area", "Body right area top path",
|
self.__connect_regions("Body center area", "Body right area top path",
|
||||||
self.body_c, self.body_rt)
|
self.body_c, self.body_rt)
|
||||||
self.__connect_regions("Body center area", "Body right area bottom path",
|
self.__connect_regions("Body center area", "Body right area bottom path",
|
||||||
self.body_c, self.body_rb,
|
self.body_c, self.body_rb)
|
||||||
lambda state: _has_energy_form(state, self.player))
|
|
||||||
self.__connect_regions("Body center area", "Body bottom area",
|
self.__connect_regions("Body center area", "Body bottom area",
|
||||||
self.body_c, self.body_b,
|
self.body_c, self.body_b,
|
||||||
lambda state: _has_dual_form(state, self.player))
|
lambda state: _has_dual_form(state, self.player))
|
||||||
@@ -851,12 +803,22 @@ class AquariaRegions:
|
|||||||
self.__connect_one_way_regions("final boss third form area", "final boss end",
|
self.__connect_one_way_regions("final boss third form area", "final boss end",
|
||||||
self.final_boss, self.final_boss_end)
|
self.final_boss, self.final_boss_end)
|
||||||
|
|
||||||
def __connect_transturtle(self, item_source: str, item_target: str, region_source: Region,
|
def __connect_transturtle(self, item_source: str, item_target: str, region_source: Region, region_target: Region,
|
||||||
region_target: Region) -> None:
|
rule=None) -> None:
|
||||||
"""Connect a single transturtle to another one"""
|
"""Connect a single transturtle to another one"""
|
||||||
if item_source != item_target:
|
if item_source != item_target:
|
||||||
self.__connect_one_way_regions(item_source, item_target, region_source, region_target,
|
if rule is None:
|
||||||
lambda state: state.has(item_target, self.player))
|
self.__connect_one_way_regions(item_source, item_target, region_source, region_target,
|
||||||
|
lambda state: state.has(item_target, self.player))
|
||||||
|
else:
|
||||||
|
self.__connect_one_way_regions(item_source, item_target, region_source, region_target, rule)
|
||||||
|
|
||||||
|
def __connect_arnassi_path_transturtle(self, item_source: str, item_target: str, region_source: Region,
|
||||||
|
region_target: Region) -> None:
|
||||||
|
"""Connect the Arnassi Ruins transturtle to another one"""
|
||||||
|
self.__connect_one_way_regions(item_source, item_target, region_source, region_target,
|
||||||
|
lambda state: state.has(item_target, self.player) and
|
||||||
|
_has_fish_form(state, self.player))
|
||||||
|
|
||||||
def _connect_transturtle_to_other(self, item: str, region: Region) -> None:
|
def _connect_transturtle_to_other(self, item: str, region: Region) -> None:
|
||||||
"""Connect a single transturtle to all others"""
|
"""Connect a single transturtle to all others"""
|
||||||
@@ -865,10 +827,24 @@ class AquariaRegions:
|
|||||||
self.__connect_transturtle(item, "Transturtle Open Water top right", region, self.openwater_tr_turtle)
|
self.__connect_transturtle(item, "Transturtle Open Water top right", region, self.openwater_tr_turtle)
|
||||||
self.__connect_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl)
|
self.__connect_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl)
|
||||||
self.__connect_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle)
|
self.__connect_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle)
|
||||||
self.__connect_transturtle(item, "Transturtle Abyss right", region, self.abyss_r_transturtle)
|
self.__connect_transturtle(item, "Transturtle Abyss right", region, self.abyss_r)
|
||||||
self.__connect_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube)
|
self.__connect_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube)
|
||||||
self.__connect_transturtle(item, "Transturtle Simon Says", region, self.simon)
|
self.__connect_transturtle(item, "Transturtle Simon Says", region, self.simon)
|
||||||
self.__connect_transturtle(item, "Transturtle Arnassi Ruins", region, self.arnassi_cave_transturtle)
|
self.__connect_transturtle(item, "Transturtle Arnassi Ruins", region, self.arnassi_path,
|
||||||
|
lambda state: state.has("Transturtle Arnassi Ruins", self.player) and
|
||||||
|
_has_fish_form(state, self.player))
|
||||||
|
|
||||||
|
def _connect_arnassi_path_transturtle_to_other(self, item: str, region: Region) -> None:
|
||||||
|
"""Connect the Arnassi Ruins transturtle to all others"""
|
||||||
|
self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top left", region, self.veil_tl)
|
||||||
|
self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top right", region, self.veil_tr_l)
|
||||||
|
self.__connect_arnassi_path_transturtle(item, "Transturtle Open Water top right", region,
|
||||||
|
self.openwater_tr_turtle)
|
||||||
|
self.__connect_arnassi_path_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl)
|
||||||
|
self.__connect_arnassi_path_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle)
|
||||||
|
self.__connect_arnassi_path_transturtle(item, "Transturtle Abyss right", region, self.abyss_r)
|
||||||
|
self.__connect_arnassi_path_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube)
|
||||||
|
self.__connect_arnassi_path_transturtle(item, "Transturtle Simon Says", region, self.simon)
|
||||||
|
|
||||||
def __connect_transturtles(self) -> None:
|
def __connect_transturtles(self) -> None:
|
||||||
"""Connect every transturtle with others"""
|
"""Connect every transturtle with others"""
|
||||||
@@ -877,10 +853,10 @@ class AquariaRegions:
|
|||||||
self._connect_transturtle_to_other("Transturtle Open Water top right", self.openwater_tr_turtle)
|
self._connect_transturtle_to_other("Transturtle Open Water top right", self.openwater_tr_turtle)
|
||||||
self._connect_transturtle_to_other("Transturtle Forest bottom left", self.forest_bl)
|
self._connect_transturtle_to_other("Transturtle Forest bottom left", self.forest_bl)
|
||||||
self._connect_transturtle_to_other("Transturtle Home Water", self.home_water_transturtle)
|
self._connect_transturtle_to_other("Transturtle Home Water", self.home_water_transturtle)
|
||||||
self._connect_transturtle_to_other("Transturtle Abyss right", self.abyss_r_transturtle)
|
self._connect_transturtle_to_other("Transturtle Abyss right", self.abyss_r)
|
||||||
self._connect_transturtle_to_other("Transturtle Final Boss", self.final_boss_tube)
|
self._connect_transturtle_to_other("Transturtle Final Boss", self.final_boss_tube)
|
||||||
self._connect_transturtle_to_other("Transturtle Simon Says", self.simon)
|
self._connect_transturtle_to_other("Transturtle Simon Says", self.simon)
|
||||||
self._connect_transturtle_to_other("Transturtle Arnassi Ruins", self.arnassi_cave_transturtle)
|
self._connect_arnassi_path_transturtle_to_other("Transturtle Arnassi Ruins", self.arnassi_path)
|
||||||
|
|
||||||
def connect_regions(self) -> None:
|
def connect_regions(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -917,7 +893,7 @@ class AquariaRegions:
|
|||||||
self.__add_event_location(self.energy_temple_boss,
|
self.__add_event_location(self.energy_temple_boss,
|
||||||
"Beating Fallen God",
|
"Beating Fallen God",
|
||||||
"Fallen God beated")
|
"Fallen God beated")
|
||||||
self.__add_event_location(self.cathedral_boss_l,
|
self.__add_event_location(self.cathedral_boss_r,
|
||||||
"Beating Mithalan God",
|
"Beating Mithalan God",
|
||||||
"Mithalan God beated")
|
"Mithalan God beated")
|
||||||
self.__add_event_location(self.forest_boss,
|
self.__add_event_location(self.forest_boss,
|
||||||
@@ -994,9 +970,8 @@ class AquariaRegions:
|
|||||||
"""Since Urns need to be broken, add a damaging item to rules"""
|
"""Since Urns need to be broken, add a damaging item to rules"""
|
||||||
add_rule(self.multiworld.get_location("Open Water top right area, first urn in the Mithalas exit", self.player),
|
add_rule(self.multiworld.get_location("Open Water top right area, first urn in the Mithalas exit", self.player),
|
||||||
lambda state: _has_damaging_item(state, self.player))
|
lambda state: _has_damaging_item(state, self.player))
|
||||||
add_rule(
|
add_rule(self.multiworld.get_location("Open Water top right area, second urn in the Mithalas exit", self.player),
|
||||||
self.multiworld.get_location("Open Water top right area, second urn in the Mithalas exit", self.player),
|
lambda state: _has_damaging_item(state, self.player))
|
||||||
lambda state: _has_damaging_item(state, self.player))
|
|
||||||
add_rule(self.multiworld.get_location("Open Water top right area, third urn in the Mithalas exit", self.player),
|
add_rule(self.multiworld.get_location("Open Water top right area, third urn in the Mithalas exit", self.player),
|
||||||
lambda state: _has_damaging_item(state, self.player))
|
lambda state: _has_damaging_item(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Mithalas City, first urn in one of the homes", self.player),
|
add_rule(self.multiworld.get_location("Mithalas City, first urn in one of the homes", self.player),
|
||||||
@@ -1044,46 +1019,66 @@ class AquariaRegions:
|
|||||||
Modify rules for location that need soup
|
Modify rules for location that need soup
|
||||||
"""
|
"""
|
||||||
add_rule(self.multiworld.get_location("Turtle cave, Urchin Costume", self.player),
|
add_rule(self.multiworld.get_location("Turtle cave, Urchin Costume", self.player),
|
||||||
lambda state: _has_hot_soup(state, self.player))
|
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
|
||||||
|
add_rule(self.multiworld.get_location("Sun Worm path, first cliff bulb", self.player),
|
||||||
|
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
|
||||||
|
add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player),
|
||||||
|
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.player),
|
add_rule(self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.player),
|
||||||
lambda state: _has_beast_and_soup_form(state, self.player))
|
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
|
||||||
|
|
||||||
def __adjusting_under_rock_location(self) -> None:
|
def __adjusting_under_rock_location(self) -> None:
|
||||||
"""
|
"""
|
||||||
Modify rules implying bind song needed for bulb under rocks
|
Modify rules implying bind song needed for bulb under rocks
|
||||||
"""
|
"""
|
||||||
add_rule(self.multiworld.get_location("Home Water, bulb under the rock in the left path from the Verse Cave",
|
add_rule(self.multiworld.get_location("Home Water, bulb under the rock in the left path from the Verse Cave",
|
||||||
self.player), lambda state: _has_bind_song(state, self.player))
|
self.player), lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Verse Cave left area, bulb under the rock at the end of the path",
|
add_rule(self.multiworld.get_location("Verse Cave left area, bulb under the rock at the end of the path",
|
||||||
self.player), lambda state: _has_bind_song(state, self.player))
|
self.player), lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Naija's Home, bulb under the rock at the right of the main path",
|
add_rule(self.multiworld.get_location("Naija's Home, bulb under the rock at the right of the main path",
|
||||||
self.player), lambda state: _has_bind_song(state, self.player))
|
self.player), lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Song Cave, bulb under the rock in the path to the singing statues",
|
add_rule(self.multiworld.get_location("Song Cave, bulb under the rock in the path to the singing statues",
|
||||||
self.player), lambda state: _has_bind_song(state, self.player))
|
self.player), lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Song Cave, bulb under the rock close to the song door",
|
add_rule(self.multiworld.get_location("Song Cave, bulb under the rock close to the song door",
|
||||||
self.player), lambda state: _has_bind_song(state, self.player))
|
self.player), lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Energy Temple second area, bulb under the rock",
|
add_rule(self.multiworld.get_location("Energy Temple second area, bulb under the rock",
|
||||||
self.player), lambda state: _has_bind_song(state, self.player))
|
self.player), lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the right path",
|
add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the right path",
|
||||||
self.player), lambda state: _has_bind_song(state, self.player))
|
self.player), lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the left path",
|
add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the left path",
|
||||||
self.player), lambda state: _has_bind_song(state, self.player))
|
self.player), lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Kelp Forest top right area, bulb under the rock in the right path",
|
add_rule(self.multiworld.get_location("Kelp Forest top right area, bulb under the rock in the right path",
|
||||||
self.player), lambda state: _has_bind_song(state, self.player))
|
self.player), lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path",
|
add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path",
|
||||||
self.player), lambda state: _has_bind_song(state, self.player))
|
self.player), lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room",
|
add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room",
|
||||||
self.player), lambda state: _has_bind_song(state, self.player))
|
self.player), lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Abyss right area, bulb in the middle path",
|
add_rule(self.multiworld.get_location("Abyss right area, bulb in the middle path",
|
||||||
self.player), lambda state: _has_bind_song(state, self.player))
|
self.player), lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path",
|
add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path",
|
||||||
self.player), lambda state: _has_bind_song(state, self.player))
|
self.player), lambda state: _has_bind_song(state, self.player))
|
||||||
|
|
||||||
def __adjusting_light_in_dark_place_rules(self) -> None:
|
def __adjusting_light_in_dark_place_rules(self) -> None:
|
||||||
add_rule(self.multiworld.get_location("Kelp Forest top right area, Black Pearl", self.player),
|
add_rule(self.multiworld.get_location("Kelp Forest top right area, Black Pearl", self.player),
|
||||||
lambda state: _has_light(state, self.player))
|
lambda state: _has_light(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Kelp Forest bottom right area, Odd Container", self.player),
|
add_rule(self.multiworld.get_location("Kelp Forest bottom right area, Odd Container", self.player),
|
||||||
lambda state: _has_light(state, self.player))
|
lambda state: _has_light(state, self.player))
|
||||||
|
add_rule(self.multiworld.get_entrance("Transturtle Veil top left to Transturtle Abyss right", self.player),
|
||||||
|
lambda state: _has_light(state, self.player))
|
||||||
|
add_rule(self.multiworld.get_entrance("Transturtle Open Water top right to Transturtle Abyss right", self.player),
|
||||||
|
lambda state: _has_light(state, self.player))
|
||||||
|
add_rule(self.multiworld.get_entrance("Transturtle Veil top right to Transturtle Abyss right", self.player),
|
||||||
|
lambda state: _has_light(state, self.player))
|
||||||
|
add_rule(self.multiworld.get_entrance("Transturtle Forest bottom left to Transturtle Abyss right", self.player),
|
||||||
|
lambda state: _has_light(state, self.player))
|
||||||
|
add_rule(self.multiworld.get_entrance("Transturtle Home Water to Transturtle Abyss right", self.player),
|
||||||
|
lambda state: _has_light(state, self.player))
|
||||||
|
add_rule(self.multiworld.get_entrance("Transturtle Final Boss to Transturtle Abyss right", self.player),
|
||||||
|
lambda state: _has_light(state, self.player))
|
||||||
|
add_rule(self.multiworld.get_entrance("Transturtle Simon Says to Transturtle Abyss right", self.player),
|
||||||
|
lambda state: _has_light(state, self.player))
|
||||||
|
add_rule(self.multiworld.get_entrance("Transturtle Arnassi Ruins to Transturtle Abyss right", self.player),
|
||||||
|
lambda state: _has_light(state, self.player))
|
||||||
add_rule(self.multiworld.get_entrance("Body center area to Abyss left bottom area", self.player),
|
add_rule(self.multiworld.get_entrance("Body center area to Abyss left bottom area", self.player),
|
||||||
lambda state: _has_light(state, self.player))
|
lambda state: _has_light(state, self.player))
|
||||||
add_rule(self.multiworld.get_entrance("Veil left of sun temple to Octo cave top path", self.player),
|
add_rule(self.multiworld.get_entrance("Veil left of sun temple to Octo cave top path", self.player),
|
||||||
@@ -1102,14 +1097,12 @@ class AquariaRegions:
|
|||||||
def __adjusting_manual_rules(self) -> None:
|
def __adjusting_manual_rules(self) -> None:
|
||||||
add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player),
|
add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player),
|
||||||
lambda state: _has_beast_form(state, self.player))
|
lambda state: _has_beast_form(state, self.player))
|
||||||
add_rule(
|
add_rule(self.multiworld.get_location("Open Water bottom left area, bulb inside the lowest fish pass", self.player),
|
||||||
self.multiworld.get_location("Open Water bottom left area, bulb inside the lowest fish pass", self.player),
|
lambda state: _has_fish_form(state, self.player))
|
||||||
lambda state: _has_fish_form(state, self.player))
|
|
||||||
add_rule(self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", self.player),
|
add_rule(self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", self.player),
|
||||||
lambda state: _has_spirit_form(state, self.player))
|
lambda state: _has_spirit_form(state, self.player))
|
||||||
add_rule(
|
add_rule(self.multiworld.get_location("The Veil top left area, bulb hidden behind the blocking rock", self.player),
|
||||||
self.multiworld.get_location("The Veil top left area, bulb hidden behind the blocking rock", self.player),
|
lambda state: _has_bind_song(state, self.player))
|
||||||
lambda state: _has_bind_song(state, self.player))
|
|
||||||
add_rule(self.multiworld.get_location("Turtle cave, Turtle Egg", self.player),
|
add_rule(self.multiworld.get_location("Turtle cave, Turtle Egg", self.player),
|
||||||
lambda state: _has_bind_song(state, self.player))
|
lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Abyss left area, bulb in the bottom fish pass", self.player),
|
add_rule(self.multiworld.get_location("Abyss left area, bulb in the bottom fish pass", self.player),
|
||||||
@@ -1121,119 +1114,103 @@ class AquariaRegions:
|
|||||||
add_rule(self.multiworld.get_location("Verse Cave right area, Big Seed", self.player),
|
add_rule(self.multiworld.get_location("Verse Cave right area, Big Seed", self.player),
|
||||||
lambda state: _has_bind_song(state, self.player))
|
lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Arnassi Ruins, Song Plant Spore", self.player),
|
add_rule(self.multiworld.get_location("Arnassi Ruins, Song Plant Spore", self.player),
|
||||||
lambda state: _has_beast_form_or_arnassi_armor(state, self.player))
|
lambda state: _has_beast_form(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Energy Temple first area, bulb in the bottom room blocked by a rock",
|
add_rule(self.multiworld.get_location("Energy Temple first area, bulb in the bottom room blocked by a rock",
|
||||||
self.player), lambda state: _has_bind_song(state, self.player))
|
self.player), lambda state: _has_energy_form(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Home Water, bulb in the bottom left room", self.player),
|
add_rule(self.multiworld.get_location("Home Water, bulb in the bottom left room", self.player),
|
||||||
lambda state: _has_bind_song(state, self.player))
|
lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Home Water, bulb in the path below Nautilus Prime", self.player),
|
add_rule(self.multiworld.get_location("Home Water, bulb in the path below Nautilus Prime", self.player),
|
||||||
lambda state: _has_bind_song(state, self.player))
|
lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Naija's Home, bulb after the energy door", self.player),
|
add_rule(self.multiworld.get_location("Naija's Home, bulb after the energy door", self.player),
|
||||||
lambda state: _has_energy_attack_item(state, self.player))
|
lambda state: _has_energy_form(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", self.player),
|
add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", self.player),
|
||||||
lambda state: _has_spirit_form(state, self.player) and
|
lambda state: _has_spirit_form(state, self.player) and
|
||||||
_has_sun_form(state, self.player))
|
_has_sun_form(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player),
|
add_rule(self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player),
|
||||||
lambda state: _has_fish_form(state, self.player) or
|
lambda state: _has_fish_form(state, self.player) and
|
||||||
_has_beast_and_soup_form(state, self.player))
|
_has_spirit_form(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Mithalas City, urn inside a home fish pass", self.player),
|
|
||||||
lambda state: _has_damaging_item(state, self.player))
|
|
||||||
add_rule(self.multiworld.get_location("Mithalas City, urn in the Castle flower tube entrance", self.player),
|
|
||||||
lambda state: _has_damaging_item(state, self.player))
|
|
||||||
add_rule(self.multiworld.get_location(
|
|
||||||
"The Veil top right area, bulb in the middle of the wall jump cliff", self.player
|
|
||||||
), lambda state: _has_beast_form_or_arnassi_armor(state, self.player))
|
|
||||||
add_rule(self.multiworld.get_location("Kelp Forest top left area, Jelly Egg", self.player),
|
|
||||||
lambda state: _has_beast_form(state, self.player))
|
|
||||||
add_rule(self.multiworld.get_location("Sun Worm path, first cliff bulb", self.player),
|
|
||||||
lambda state: state.has("Sun God beated", self.player))
|
|
||||||
add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player),
|
|
||||||
lambda state: state.has("Sun God beated", self.player))
|
|
||||||
add_rule(self.multiworld.get_location("The Body center area, breaking Li's cage", self.player),
|
|
||||||
lambda state: _has_tongue_cleared(state, self.player))
|
|
||||||
|
|
||||||
def __no_progression_hard_or_hidden_location(self) -> None:
|
def __no_progression_hard_or_hidden_location(self) -> None:
|
||||||
self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth",
|
self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Mithalas boss area, beating Mithalan God",
|
self.multiworld.get_location("Mithalas boss area, beating Mithalan God",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Kelp Forest boss area, beating Drunian God",
|
self.multiworld.get_location("Kelp Forest boss area, beating Drunian God",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Sun Temple boss area, beating Sun God",
|
self.multiworld.get_location("Sun Temple boss area, beating Sun God",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Sunken City, bulb on top of the boss area",
|
self.multiworld.get_location("Sunken City, bulb on top of the boss area",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Home Water, Nautilus Egg",
|
self.multiworld.get_location("Home Water, Nautilus Egg",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Energy Temple blaster room, Blaster Egg",
|
self.multiworld.get_location("Energy Temple blaster room, Blaster Egg",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Mithalas City Castle, beating the Priests",
|
self.multiworld.get_location("Mithalas City Castle, beating the Priests",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Mermog cave, Piranha Egg",
|
self.multiworld.get_location("Mermog cave, Piranha Egg",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Octopus Cave, Dumbo Egg",
|
self.multiworld.get_location("Octopus Cave, Dumbo Egg",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly",
|
self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume",
|
self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Final Boss area, bulb in the boss third form room",
|
self.multiworld.get_location("Final Boss area, bulb in the boss third form room",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Sun Worm path, first cliff bulb",
|
self.multiworld.get_location("Sun Worm path, first cliff bulb",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Sun Worm path, second cliff bulb",
|
self.multiworld.get_location("Sun Worm path, second cliff bulb",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall",
|
self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Bubble Cave, bulb in the left cave wall",
|
self.multiworld.get_location("Bubble Cave, bulb in the left cave wall",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Bubble Cave, Verse Egg",
|
self.multiworld.get_location("Bubble Cave, Verse Egg",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals",
|
self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby",
|
self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Sun Temple, Sun Key",
|
self.multiworld.get_location("Sun Temple, Sun Key",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("The Body bottom area, Mutant Costume",
|
self.multiworld.get_location("The Body bottom area, Mutant Costume",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part",
|
self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Arnassi Ruins, Arnassi Armor",
|
self.multiworld.get_location("Arnassi Ruins, Arnassi Armor",
|
||||||
self.player).item_rule = \
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
|
|
||||||
def adjusting_rules(self, options: AquariaOptions) -> None:
|
def adjusting_rules(self, options: AquariaOptions) -> None:
|
||||||
"""
|
"""
|
||||||
Modify rules for single location or optional rules
|
Modify rules for single location or optional rules
|
||||||
"""
|
"""
|
||||||
self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player)
|
|
||||||
self.__adjusting_urns_rules()
|
self.__adjusting_urns_rules()
|
||||||
self.__adjusting_crates_rules()
|
self.__adjusting_crates_rules()
|
||||||
self.__adjusting_soup_rules()
|
self.__adjusting_soup_rules()
|
||||||
@@ -1257,7 +1234,7 @@ class AquariaRegions:
|
|||||||
lambda state: _has_bind_song(state, self.player))
|
lambda state: _has_bind_song(state, self.player))
|
||||||
if options.unconfine_home_water.value in [0, 2]:
|
if options.unconfine_home_water.value in [0, 2]:
|
||||||
add_rule(self.multiworld.get_entrance("Home Water to Open Water top left area", self.player),
|
add_rule(self.multiworld.get_entrance("Home Water to Open Water top left area", self.player),
|
||||||
lambda state: _has_bind_song(state, self.player) and _has_energy_attack_item(state, self.player))
|
lambda state: _has_bind_song(state, self.player) and _has_energy_form(state, self.player))
|
||||||
if options.early_energy_form:
|
if options.early_energy_form:
|
||||||
self.multiworld.early_items[self.player]["Energy form"] = 1
|
self.multiworld.early_items[self.player]["Energy form"] = 1
|
||||||
|
|
||||||
@@ -1297,7 +1274,6 @@ class AquariaRegions:
|
|||||||
self.multiworld.regions.append(self.arnassi)
|
self.multiworld.regions.append(self.arnassi)
|
||||||
self.multiworld.regions.append(self.arnassi_path)
|
self.multiworld.regions.append(self.arnassi_path)
|
||||||
self.multiworld.regions.append(self.arnassi_crab_boss)
|
self.multiworld.regions.append(self.arnassi_crab_boss)
|
||||||
self.multiworld.regions.append(self.arnassi_cave_transturtle)
|
|
||||||
self.multiworld.regions.append(self.simon)
|
self.multiworld.regions.append(self.simon)
|
||||||
|
|
||||||
def __add_mithalas_regions_to_world(self) -> None:
|
def __add_mithalas_regions_to_world(self) -> None:
|
||||||
@@ -1324,7 +1300,6 @@ class AquariaRegions:
|
|||||||
self.multiworld.regions.append(self.forest_tr)
|
self.multiworld.regions.append(self.forest_tr)
|
||||||
self.multiworld.regions.append(self.forest_tr_fp)
|
self.multiworld.regions.append(self.forest_tr_fp)
|
||||||
self.multiworld.regions.append(self.forest_bl)
|
self.multiworld.regions.append(self.forest_bl)
|
||||||
self.multiworld.regions.append(self.forest_bl_sc)
|
|
||||||
self.multiworld.regions.append(self.forest_br)
|
self.multiworld.regions.append(self.forest_br)
|
||||||
self.multiworld.regions.append(self.forest_boss)
|
self.multiworld.regions.append(self.forest_boss)
|
||||||
self.multiworld.regions.append(self.forest_boss_entrance)
|
self.multiworld.regions.append(self.forest_boss_entrance)
|
||||||
@@ -1362,7 +1337,6 @@ class AquariaRegions:
|
|||||||
self.multiworld.regions.append(self.abyss_l)
|
self.multiworld.regions.append(self.abyss_l)
|
||||||
self.multiworld.regions.append(self.abyss_lb)
|
self.multiworld.regions.append(self.abyss_lb)
|
||||||
self.multiworld.regions.append(self.abyss_r)
|
self.multiworld.regions.append(self.abyss_r)
|
||||||
self.multiworld.regions.append(self.abyss_r_transturtle)
|
|
||||||
self.multiworld.regions.append(self.ice_cave)
|
self.multiworld.regions.append(self.ice_cave)
|
||||||
self.multiworld.regions.append(self.bubble_cave)
|
self.multiworld.regions.append(self.bubble_cave)
|
||||||
self.multiworld.regions.append(self.bubble_cave_boss)
|
self.multiworld.regions.append(self.bubble_cave_boss)
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ after_home_water_locations = [
|
|||||||
"Sun Temple, bulb at the top of the high dark room",
|
"Sun Temple, bulb at the top of the high dark room",
|
||||||
"Sun Temple, Golden Gear",
|
"Sun Temple, Golden Gear",
|
||||||
"Sun Temple, first bulb of the temple",
|
"Sun Temple, first bulb of the temple",
|
||||||
"Sun Temple, bulb on the right part",
|
"Sun Temple, bulb on the left part",
|
||||||
"Sun Temple, bulb in the hidden room of the right part",
|
"Sun Temple, bulb in the hidden room of the right part",
|
||||||
"Sun Temple, Sun Key",
|
"Sun Temple, Sun Key",
|
||||||
"Sun Worm path, first path bulb",
|
"Sun Worm path, first path bulb",
|
||||||
|
|||||||
@@ -13,16 +13,36 @@ class BeastFormAccessTest(AquariaTestBase):
|
|||||||
def test_beast_form_location(self) -> None:
|
def test_beast_form_location(self) -> None:
|
||||||
"""Test locations that require beast form"""
|
"""Test locations that require beast form"""
|
||||||
locations = [
|
locations = [
|
||||||
|
"Mithalas City Castle, beating the Priests",
|
||||||
|
"Arnassi Ruins, Crab Armor",
|
||||||
|
"Arnassi Ruins, Song Plant Spore",
|
||||||
|
"Mithalas City, first bulb at the end of the top path",
|
||||||
|
"Mithalas City, second bulb at the end of the top path",
|
||||||
|
"Mithalas City, bulb in the top path",
|
||||||
|
"Mithalas City, Mithalas Pot",
|
||||||
|
"Mithalas City, urn in the Castle flower tube entrance",
|
||||||
"Mermog cave, Piranha Egg",
|
"Mermog cave, Piranha Egg",
|
||||||
"Kelp Forest top left area, Jelly Egg",
|
|
||||||
"Mithalas Cathedral, Mithalan Dress",
|
"Mithalas Cathedral, Mithalan Dress",
|
||||||
|
"Turtle cave, bulb in Bubble Cliff",
|
||||||
|
"Turtle cave, Urchin Costume",
|
||||||
|
"Sun Worm path, first cliff bulb",
|
||||||
|
"Sun Worm path, second cliff bulb",
|
||||||
"The Veil top right area, bulb at the top of the waterfall",
|
"The Veil top right area, bulb at the top of the waterfall",
|
||||||
|
"Bubble Cave, bulb in the left cave wall",
|
||||||
|
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||||
|
"Bubble Cave, Verse Egg",
|
||||||
"Sunken City, bulb on top of the boss area",
|
"Sunken City, bulb on top of the boss area",
|
||||||
"Octopus Cave, Dumbo Egg",
|
"Octopus Cave, Dumbo Egg",
|
||||||
"Beating the Golem",
|
"Beating the Golem",
|
||||||
"Beating Mergog",
|
"Beating Mergog",
|
||||||
|
"Beating Crabbius Maximus",
|
||||||
"Beating Octopus Prime",
|
"Beating Octopus Prime",
|
||||||
"Sunken City cleared",
|
"Beating Mantis Shrimp Prime",
|
||||||
|
"King Jellyfish Cave, Jellyfish Costume",
|
||||||
|
"King Jellyfish Cave, bulb in the right path from King Jelly",
|
||||||
|
"Beating King Jellyfish God Prime",
|
||||||
|
"Beating Mithalan priests",
|
||||||
|
"Sunken City cleared"
|
||||||
]
|
]
|
||||||
items = [["Beast form"]]
|
items = [["Beast form"]]
|
||||||
self.assertAccessDependency(locations, items)
|
self.assertAccessDependency(locations, items)
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|
||||||
Description: Unit test used to test accessibility of locations with and without the beast form or arnassi armor
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import AquariaTestBase
|
|
||||||
|
|
||||||
|
|
||||||
class BeastForArnassiArmormAccessTest(AquariaTestBase):
|
|
||||||
"""Unit test used to test accessibility of locations with and without the beast form or arnassi armor"""
|
|
||||||
|
|
||||||
def test_beast_form_arnassi_armor_location(self) -> None:
|
|
||||||
"""Test locations that require beast form or arnassi armor"""
|
|
||||||
locations = [
|
|
||||||
"Mithalas City Castle, beating the Priests",
|
|
||||||
"Arnassi Ruins, Crab Armor",
|
|
||||||
"Arnassi Ruins, Song Plant Spore",
|
|
||||||
"Mithalas City, first bulb at the end of the top path",
|
|
||||||
"Mithalas City, second bulb at the end of the top path",
|
|
||||||
"Mithalas City, bulb in the top path",
|
|
||||||
"Mithalas City, Mithalas Pot",
|
|
||||||
"Mithalas City, urn in the Castle flower tube entrance",
|
|
||||||
"Mermog cave, Piranha Egg",
|
|
||||||
"Mithalas Cathedral, Mithalan Dress",
|
|
||||||
"Kelp Forest top left area, Jelly Egg",
|
|
||||||
"The Veil top right area, bulb in the middle of the wall jump cliff",
|
|
||||||
"The Veil top right area, bulb at the top of the waterfall",
|
|
||||||
"Sunken City, bulb on top of the boss area",
|
|
||||||
"Octopus Cave, Dumbo Egg",
|
|
||||||
"Beating the Golem",
|
|
||||||
"Beating Mergog",
|
|
||||||
"Beating Crabbius Maximus",
|
|
||||||
"Beating Octopus Prime",
|
|
||||||
"Beating Mithalan priests",
|
|
||||||
"Sunken City cleared"
|
|
||||||
]
|
|
||||||
items = [["Beast form", "Arnassi Armor"]]
|
|
||||||
self.assertAccessDependency(locations, items)
|
|
||||||
@@ -17,16 +17,55 @@ class EnergyFormAccessTest(AquariaTestBase):
|
|||||||
def test_energy_form_location(self) -> None:
|
def test_energy_form_location(self) -> None:
|
||||||
"""Test locations that require Energy form"""
|
"""Test locations that require Energy form"""
|
||||||
locations = [
|
locations = [
|
||||||
|
"Home Water, Nautilus Egg",
|
||||||
|
"Naija's Home, bulb after the energy door",
|
||||||
|
"Energy Temple first area, bulb in the bottom room blocked by a rock",
|
||||||
"Energy Temple second area, bulb under the rock",
|
"Energy Temple second area, bulb under the rock",
|
||||||
|
"Energy Temple bottom entrance, Krotite Armor",
|
||||||
"Energy Temple third area, bulb in the bottom path",
|
"Energy Temple third area, bulb in the bottom path",
|
||||||
"The Body left area, first bulb in the top face room",
|
"Energy Temple boss area, Fallen God Tooth",
|
||||||
"The Body left area, second bulb in the top face room",
|
"Energy Temple blaster room, Blaster Egg",
|
||||||
"The Body left area, bulb below the water stream",
|
"Mithalas City Castle, beating the Priests",
|
||||||
"The Body left area, bulb in the top path to the top face room",
|
"Mithalas Cathedral, first urn in the top right room",
|
||||||
"The Body left area, bulb in the bottom face room",
|
"Mithalas Cathedral, second urn in the top right room",
|
||||||
"The Body right area, bulb in the top path to the bottom face room",
|
"Mithalas Cathedral, third urn in the top right room",
|
||||||
"The Body right area, bulb in the bottom face room",
|
"Mithalas Cathedral, urn in the flesh room with fleas",
|
||||||
|
"Mithalas Cathedral, first urn in the bottom right path",
|
||||||
|
"Mithalas Cathedral, second urn in the bottom right path",
|
||||||
|
"Mithalas Cathedral, urn behind the flesh vein",
|
||||||
|
"Mithalas Cathedral, urn in the top left eyes boss room",
|
||||||
|
"Mithalas Cathedral, first urn in the path behind the flesh vein",
|
||||||
|
"Mithalas Cathedral, second urn in the path behind the flesh vein",
|
||||||
|
"Mithalas Cathedral, third urn in the path behind the flesh vein",
|
||||||
|
"Mithalas Cathedral, fourth urn in the top right room",
|
||||||
|
"Mithalas Cathedral, Mithalan Dress",
|
||||||
|
"Mithalas Cathedral, urn below the left entrance",
|
||||||
|
"Mithalas boss area, beating Mithalan God",
|
||||||
|
"Kelp Forest top left area, bulb close to the Verse Egg",
|
||||||
|
"Kelp Forest top left area, Verse Egg",
|
||||||
|
"Kelp Forest boss area, beating Drunian God",
|
||||||
|
"Mermog cave, Piranha Egg",
|
||||||
|
"Octopus Cave, Dumbo Egg",
|
||||||
|
"Sun Temple boss area, beating Sun God",
|
||||||
|
"Arnassi Ruins, Crab Armor",
|
||||||
|
"King Jellyfish Cave, bulb in the right path from King Jelly",
|
||||||
|
"King Jellyfish Cave, Jellyfish Costume",
|
||||||
|
"Sunken City, bulb on top of the boss area",
|
||||||
"Final Boss area, bulb in the boss third form room",
|
"Final Boss area, bulb in the boss third form room",
|
||||||
|
"Beating Fallen God",
|
||||||
|
"Beating Mithalan God",
|
||||||
|
"Beating Drunian God",
|
||||||
|
"Beating Sun God",
|
||||||
|
"Beating the Golem",
|
||||||
|
"Beating Nautilus Prime",
|
||||||
|
"Beating Blaster Peg Prime",
|
||||||
|
"Beating Mergog",
|
||||||
|
"Beating Mithalan priests",
|
||||||
|
"Beating Octopus Prime",
|
||||||
|
"Beating Crabbius Maximus",
|
||||||
|
"Beating King Jellyfish God Prime",
|
||||||
|
"First secret",
|
||||||
|
"Sunken City cleared",
|
||||||
"Objective complete",
|
"Objective complete",
|
||||||
]
|
]
|
||||||
items = [["Energy form"]]
|
items = [["Energy form"]]
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|
||||||
Description: Unit test used to test accessibility of locations with and without the energy form and dual form (and Li)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import AquariaTestBase
|
|
||||||
|
|
||||||
|
|
||||||
class EnergyFormDualFormAccessTest(AquariaTestBase):
|
|
||||||
"""Unit test used to test accessibility of locations with and without the energy form and dual form (and Li)"""
|
|
||||||
options = {
|
|
||||||
"early_energy_form": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_energy_form_or_dual_form_location(self) -> None:
|
|
||||||
"""Test locations that require Energy form or dual form"""
|
|
||||||
locations = [
|
|
||||||
"Naija's Home, bulb after the energy door",
|
|
||||||
"Home Water, Nautilus Egg",
|
|
||||||
"Energy Temple second area, bulb under the rock",
|
|
||||||
"Energy Temple bottom entrance, Krotite Armor",
|
|
||||||
"Energy Temple third area, bulb in the bottom path",
|
|
||||||
"Energy Temple blaster room, Blaster Egg",
|
|
||||||
"Energy Temple boss area, Fallen God Tooth",
|
|
||||||
"Mithalas City Castle, beating the Priests",
|
|
||||||
"Mithalas boss area, beating Mithalan God",
|
|
||||||
"Mithalas Cathedral, first urn in the top right room",
|
|
||||||
"Mithalas Cathedral, second urn in the top right room",
|
|
||||||
"Mithalas Cathedral, third urn in the top right room",
|
|
||||||
"Mithalas Cathedral, urn in the flesh room with fleas",
|
|
||||||
"Mithalas Cathedral, first urn in the bottom right path",
|
|
||||||
"Mithalas Cathedral, second urn in the bottom right path",
|
|
||||||
"Mithalas Cathedral, urn behind the flesh vein",
|
|
||||||
"Mithalas Cathedral, urn in the top left eyes boss room",
|
|
||||||
"Mithalas Cathedral, first urn in the path behind the flesh vein",
|
|
||||||
"Mithalas Cathedral, second urn in the path behind the flesh vein",
|
|
||||||
"Mithalas Cathedral, third urn in the path behind the flesh vein",
|
|
||||||
"Mithalas Cathedral, fourth urn in the top right room",
|
|
||||||
"Mithalas Cathedral, Mithalan Dress",
|
|
||||||
"Mithalas Cathedral, urn below the left entrance",
|
|
||||||
"Kelp Forest top left area, bulb close to the Verse Egg",
|
|
||||||
"Kelp Forest top left area, Verse Egg",
|
|
||||||
"Kelp Forest boss area, beating Drunian God",
|
|
||||||
"Mermog cave, Piranha Egg",
|
|
||||||
"Octopus Cave, Dumbo Egg",
|
|
||||||
"Sun Temple boss area, beating Sun God",
|
|
||||||
"King Jellyfish Cave, bulb in the right path from King Jelly",
|
|
||||||
"King Jellyfish Cave, Jellyfish Costume",
|
|
||||||
"Sunken City right area, crate close to the save crystal",
|
|
||||||
"Sunken City right area, crate in the left bottom room",
|
|
||||||
"Sunken City left area, crate in the little pipe room",
|
|
||||||
"Sunken City left area, crate close to the save crystal",
|
|
||||||
"Sunken City left area, crate before the bedroom",
|
|
||||||
"Sunken City left area, Girl Costume",
|
|
||||||
"Sunken City, bulb on top of the boss area",
|
|
||||||
"The Body center area, breaking Li's cage",
|
|
||||||
"The Body center area, bulb on the main path blocking tube",
|
|
||||||
"The Body left area, first bulb in the top face room",
|
|
||||||
"The Body left area, second bulb in the top face room",
|
|
||||||
"The Body left area, bulb below the water stream",
|
|
||||||
"The Body left area, bulb in the top path to the top face room",
|
|
||||||
"The Body left area, bulb in the bottom face room",
|
|
||||||
"The Body right area, bulb in the top face room",
|
|
||||||
"The Body right area, bulb in the top path to the bottom face room",
|
|
||||||
"The Body right area, bulb in the bottom face room",
|
|
||||||
"The Body bottom area, bulb in the Jelly Zap room",
|
|
||||||
"The Body bottom area, bulb in the nautilus room",
|
|
||||||
"The Body bottom area, Mutant Costume",
|
|
||||||
"Final Boss area, bulb in the boss third form room",
|
|
||||||
"Final Boss area, first bulb in the turtle room",
|
|
||||||
"Final Boss area, second bulb in the turtle room",
|
|
||||||
"Final Boss area, third bulb in the turtle room",
|
|
||||||
"Final Boss area, Transturtle",
|
|
||||||
"Beating Fallen God",
|
|
||||||
"Beating Blaster Peg Prime",
|
|
||||||
"Beating Mithalan God",
|
|
||||||
"Beating Drunian God",
|
|
||||||
"Beating Sun God",
|
|
||||||
"Beating the Golem",
|
|
||||||
"Beating Nautilus Prime",
|
|
||||||
"Beating Mergog",
|
|
||||||
"Beating Mithalan priests",
|
|
||||||
"Beating Octopus Prime",
|
|
||||||
"Beating King Jellyfish God Prime",
|
|
||||||
"Beating the Golem",
|
|
||||||
"Sunken City cleared",
|
|
||||||
"First secret",
|
|
||||||
"Objective complete"
|
|
||||||
]
|
|
||||||
items = [["Energy form", "Dual form", "Li and Li song", "Body tongue cleared"]]
|
|
||||||
self.assertAccessDependency(locations, items)
|
|
||||||
@@ -17,7 +17,6 @@ class FishFormAccessTest(AquariaTestBase):
|
|||||||
"""Test locations that require fish form"""
|
"""Test locations that require fish form"""
|
||||||
locations = [
|
locations = [
|
||||||
"The Veil top left area, bulb inside the fish pass",
|
"The Veil top left area, bulb inside the fish pass",
|
||||||
"Energy Temple first area, Energy Idol",
|
|
||||||
"Mithalas City, Doll",
|
"Mithalas City, Doll",
|
||||||
"Mithalas City, urn inside a home fish pass",
|
"Mithalas City, urn inside a home fish pass",
|
||||||
"Kelp Forest top right area, bulb in the top fish pass",
|
"Kelp Forest top right area, bulb in the top fish pass",
|
||||||
@@ -31,7 +30,8 @@ class FishFormAccessTest(AquariaTestBase):
|
|||||||
"Octopus Cave, Dumbo Egg",
|
"Octopus Cave, Dumbo Egg",
|
||||||
"Octopus Cave, bulb in the path below the Octopus Cave path",
|
"Octopus Cave, bulb in the path below the Octopus Cave path",
|
||||||
"Beating Octopus Prime",
|
"Beating Octopus Prime",
|
||||||
"Abyss left area, bulb in the bottom fish pass"
|
"Abyss left area, bulb in the bottom fish pass",
|
||||||
|
"Arnassi Ruins, Arnassi Armor"
|
||||||
]
|
]
|
||||||
items = [["Fish form"]]
|
items = [["Fish form"]]
|
||||||
self.assertAccessDependency(locations, items)
|
self.assertAccessDependency(locations, items)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class LightAccessTest(AquariaTestBase):
|
|||||||
"Abyss right area, bulb in the middle path",
|
"Abyss right area, bulb in the middle path",
|
||||||
"Abyss right area, bulb behind the rock in the middle path",
|
"Abyss right area, bulb behind the rock in the middle path",
|
||||||
"Abyss right area, bulb in the left green room",
|
"Abyss right area, bulb in the left green room",
|
||||||
|
"Abyss right area, Transturtle",
|
||||||
"Ice Cave, bulb in the room to the right",
|
"Ice Cave, bulb in the room to the right",
|
||||||
"Ice Cave, first bulb in the top exit room",
|
"Ice Cave, first bulb in the top exit room",
|
||||||
"Ice Cave, second bulb in the top exit room",
|
"Ice Cave, second bulb in the top exit room",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class SpiritFormAccessTest(AquariaTestBase):
|
|||||||
"Sunken City left area, Girl Costume",
|
"Sunken City left area, Girl Costume",
|
||||||
"Beating Mantis Shrimp Prime",
|
"Beating Mantis Shrimp Prime",
|
||||||
"First secret",
|
"First secret",
|
||||||
|
"Arnassi Ruins, Arnassi Armor",
|
||||||
]
|
]
|
||||||
items = [["Spirit form"]]
|
items = [["Spirit form"]]
|
||||||
self.assertAccessDependency(locations, items)
|
self.assertAccessDependency(locations, items)
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "WorldDefinition",
|
|
||||||
"configuration": "./output/StringWorldDefinition.json",
|
|
||||||
"emptyRegionsToKeep": [
|
|
||||||
"D17Z01S01",
|
|
||||||
"D01Z02S01",
|
|
||||||
"D02Z03S09",
|
|
||||||
"D03Z03S11",
|
|
||||||
"D04Z03S01",
|
|
||||||
"D06Z01S09",
|
|
||||||
"D20Z02S09",
|
|
||||||
"D09Z01S09[Cell24]",
|
|
||||||
"D09Z01S08[Cell7]",
|
|
||||||
"D09Z01S08[Cell18]",
|
|
||||||
"D09BZ01S01[Cell24]",
|
|
||||||
"D09BZ01S01[Cell17]",
|
|
||||||
"D09BZ01S01[Cell19]"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -637,35 +637,52 @@ item_table: List[ItemDict] = [
|
|||||||
'classification': ItemClassification.filler}
|
'classification': ItemClassification.filler}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
event_table: Dict[str, str] = {
|
||||||
|
"OpenedDCGateW": "D01Z05S24",
|
||||||
|
"OpenedDCGateE": "D01Z05S12",
|
||||||
|
"OpenedDCLadder": "D01Z05S20",
|
||||||
|
"OpenedWOTWCave": "D02Z01S06",
|
||||||
|
"RodeGOTPElevator": "D02Z02S11",
|
||||||
|
"OpenedConventLadder": "D02Z03S11",
|
||||||
|
"BrokeJondoBellW": "D03Z02S09",
|
||||||
|
"BrokeJondoBellE": "D03Z02S05",
|
||||||
|
"OpenedMOMLadder": "D04Z02S06",
|
||||||
|
"OpenedTSCGate": "D05Z02S11",
|
||||||
|
"OpenedARLadder": "D06Z01S23",
|
||||||
|
"BrokeBOTTCStatue": "D08Z01S02",
|
||||||
|
"OpenedWOTHPGate": "D09Z01S05",
|
||||||
|
"OpenedBOTSSLadder": "D17Z01S04"
|
||||||
|
}
|
||||||
|
|
||||||
group_table: Dict[str, Set[str]] = {
|
group_table: Dict[str, Set[str]] = {
|
||||||
"wounds" : {"Holy Wound of Attrition",
|
"wounds" : ["Holy Wound of Attrition",
|
||||||
"Holy Wound of Contrition",
|
"Holy Wound of Contrition",
|
||||||
"Holy Wound of Compunction"},
|
"Holy Wound of Compunction"],
|
||||||
|
|
||||||
"masks" : {"Deformed Mask of Orestes",
|
"masks" : ["Deformed Mask of Orestes",
|
||||||
"Mirrored Mask of Dolphos",
|
"Mirrored Mask of Dolphos",
|
||||||
"Embossed Mask of Crescente"},
|
"Embossed Mask of Crescente"],
|
||||||
|
|
||||||
"marks" : {"Mark of the First Refuge",
|
"marks" : ["Mark of the First Refuge",
|
||||||
"Mark of the Second Refuge",
|
"Mark of the Second Refuge",
|
||||||
"Mark of the Third Refuge"},
|
"Mark of the Third Refuge"],
|
||||||
|
|
||||||
"tirso" : {"Bouquet of Rosemary",
|
"tirso" : ["Bouquet of Rosemary",
|
||||||
"Incense Garlic",
|
"Incense Garlic",
|
||||||
"Olive Seeds",
|
"Olive Seeds",
|
||||||
"Dried Clove",
|
"Dried Clove",
|
||||||
"Sooty Garlic",
|
"Sooty Garlic",
|
||||||
"Bouquet of Thyme"},
|
"Bouquet of Thyme"],
|
||||||
|
|
||||||
"tentudia": {"Tentudia's Carnal Remains",
|
"tentudia": ["Tentudia's Carnal Remains",
|
||||||
"Remains of Tentudia's Hair",
|
"Remains of Tentudia's Hair",
|
||||||
"Tentudia's Skeletal Remains"},
|
"Tentudia's Skeletal Remains"],
|
||||||
|
|
||||||
"egg" : {"Melted Golden Coins",
|
"egg" : ["Melted Golden Coins",
|
||||||
"Torn Bridal Ribbon",
|
"Torn Bridal Ribbon",
|
||||||
"Black Grieving Veil"},
|
"Black Grieving Veil"],
|
||||||
|
|
||||||
"bones" : {"Parietal bone of Lasser, the Inquisitor",
|
"bones" : ["Parietal bone of Lasser, the Inquisitor",
|
||||||
"Jaw of Ashgan, the Inquisitor",
|
"Jaw of Ashgan, the Inquisitor",
|
||||||
"Cervical vertebra of Zicher, the Brewmaster",
|
"Cervical vertebra of Zicher, the Brewmaster",
|
||||||
"Clavicle of Dalhuisen, the Schoolchild",
|
"Clavicle of Dalhuisen, the Schoolchild",
|
||||||
@@ -708,14 +725,14 @@ group_table: Dict[str, Set[str]] = {
|
|||||||
"Scaphoid of Fierce, the Leper",
|
"Scaphoid of Fierce, the Leper",
|
||||||
"Anklebone of Weston, the Pilgrim",
|
"Anklebone of Weston, the Pilgrim",
|
||||||
"Calcaneum of Persian, the Bandit",
|
"Calcaneum of Persian, the Bandit",
|
||||||
"Navicular of Kahnnyhoo, the Murderer"},
|
"Navicular of Kahnnyhoo, the Murderer"],
|
||||||
|
|
||||||
"power" : {"Life Upgrade",
|
"power" : ["Life Upgrade",
|
||||||
"Fervour Upgrade",
|
"Fervour Upgrade",
|
||||||
"Empty Bile Vessel",
|
"Empty Bile Vessel",
|
||||||
"Quicksilver"},
|
"Quicksilver"],
|
||||||
|
|
||||||
"prayer" : {"Seguiriya to your Eyes like Stars",
|
"prayer" : ["Seguiriya to your Eyes like Stars",
|
||||||
"Debla of the Lights",
|
"Debla of the Lights",
|
||||||
"Saeta Dolorosa",
|
"Saeta Dolorosa",
|
||||||
"Campanillero to the Sons of the Aurora",
|
"Campanillero to the Sons of the Aurora",
|
||||||
@@ -729,17 +746,10 @@ group_table: Dict[str, Set[str]] = {
|
|||||||
"Romance to the Crimson Mist",
|
"Romance to the Crimson Mist",
|
||||||
"Zambra to the Resplendent Crown",
|
"Zambra to the Resplendent Crown",
|
||||||
"Cantina of the Blue Rose",
|
"Cantina of the Blue Rose",
|
||||||
"Mirabras of the Return to Port"},
|
"Mirabras of the Return to Port"]
|
||||||
|
|
||||||
"toe" : {"Little Toe made of Limestone",
|
|
||||||
"Big Toe made of Limestone",
|
|
||||||
"Fourth Toe made of Limestone"},
|
|
||||||
|
|
||||||
"eye" : {"Severed Right Eye of the Traitor",
|
|
||||||
"Broken Left Eye of the Traitor"}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tears_list: List[str] = [
|
tears_set: Set[str] = [
|
||||||
"Tears of Atonement (500)",
|
"Tears of Atonement (500)",
|
||||||
"Tears of Atonement (625)",
|
"Tears of Atonement (625)",
|
||||||
"Tears of Atonement (750)",
|
"Tears of Atonement (750)",
|
||||||
@@ -762,16 +772,16 @@ tears_list: List[str] = [
|
|||||||
"Tears of Atonement (30000)"
|
"Tears of Atonement (30000)"
|
||||||
]
|
]
|
||||||
|
|
||||||
reliquary_set: Set[str] = {
|
reliquary_set: Set[str] = [
|
||||||
"Reliquary of the Fervent Heart",
|
"Reliquary of the Fervent Heart",
|
||||||
"Reliquary of the Suffering Heart",
|
"Reliquary of the Suffering Heart",
|
||||||
"Reliquary of the Sorrowful Heart"
|
"Reliquary of the Sorrowful Heart"
|
||||||
}
|
]
|
||||||
|
|
||||||
skill_set: Set[str] = {
|
skill_set: Set[str] = [
|
||||||
"Combo Skill",
|
"Combo Skill",
|
||||||
"Charged Skill",
|
"Charged Skill",
|
||||||
"Ranged Skill",
|
"Ranged Skill",
|
||||||
"Dive Skill",
|
"Dive Skill",
|
||||||
"Lunge Skill"
|
"Lunge Skill"
|
||||||
}
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
|||||||
from dataclasses import dataclass
|
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool
|
||||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, OptionGroup
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
|
||||||
@@ -21,30 +20,23 @@ class ChoiceIsRandom(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class PrieDieuWarp(DefaultOnToggle):
|
class PrieDieuWarp(DefaultOnToggle):
|
||||||
"""
|
"""Automatically unlocks the ability to warp between Prie Dieu shrines."""
|
||||||
Automatically unlocks the ability to warp between Prie Dieu shrines.
|
|
||||||
"""
|
|
||||||
display_name = "Unlock Fast Travel"
|
display_name = "Unlock Fast Travel"
|
||||||
|
|
||||||
|
|
||||||
class SkipCutscenes(DefaultOnToggle):
|
class SkipCutscenes(DefaultOnToggle):
|
||||||
"""
|
"""Automatically skips most cutscenes."""
|
||||||
Automatically skips most cutscenes.
|
|
||||||
"""
|
|
||||||
display_name = "Auto Skip Cutscenes"
|
display_name = "Auto Skip Cutscenes"
|
||||||
|
|
||||||
|
|
||||||
class CorpseHints(DefaultOnToggle):
|
class CorpseHints(DefaultOnToggle):
|
||||||
"""
|
"""Changes the 34 corpses in game to give various hints about item locations."""
|
||||||
Changes the 34 corpses in game to give various hints about item locations.
|
|
||||||
"""
|
|
||||||
display_name = "Corpse Hints"
|
display_name = "Corpse Hints"
|
||||||
|
|
||||||
|
|
||||||
class Difficulty(Choice):
|
class Difficulty(Choice):
|
||||||
"""
|
"""Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses
|
||||||
Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses and advanced movement tricks or glitches.
|
and advanced movement tricks or glitches."""
|
||||||
"""
|
|
||||||
display_name = "Difficulty"
|
display_name = "Difficulty"
|
||||||
option_easy = 0
|
option_easy = 0
|
||||||
option_normal = 1
|
option_normal = 1
|
||||||
@@ -53,18 +45,15 @@ class Difficulty(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class Penitence(Toggle):
|
class Penitence(Toggle):
|
||||||
"""
|
"""Allows one of the three Penitences to be chosen at the beginning of the game."""
|
||||||
Allows one of the three Penitences to be chosen at the beginning of the game.
|
|
||||||
"""
|
|
||||||
display_name = "Penitence"
|
display_name = "Penitence"
|
||||||
|
|
||||||
|
|
||||||
class StartingLocation(ChoiceIsRandom):
|
class StartingLocation(ChoiceIsRandom):
|
||||||
"""
|
"""Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain
|
||||||
Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain other options.
|
other options.
|
||||||
|
Specifically, Brotherhood and Mourning And Havoc cannot be chosen if Shuffle Dash is enabled, and Grievance Ascends
|
||||||
Specifically, Brotherhood and Mourning And Havoc cannot be chosen if Shuffle Dash is enabled, and Grievance Ascends cannot be chosen if Shuffle Wall Climb is enabled.
|
cannot be chosen if Shuffle Wall Climb is enabled."""
|
||||||
"""
|
|
||||||
display_name = "Starting Location"
|
display_name = "Starting Location"
|
||||||
option_brotherhood = 0
|
option_brotherhood = 0
|
||||||
option_albero = 1
|
option_albero = 1
|
||||||
@@ -77,15 +66,10 @@ class StartingLocation(ChoiceIsRandom):
|
|||||||
|
|
||||||
|
|
||||||
class Ending(Choice):
|
class Ending(Choice):
|
||||||
"""
|
"""Choose which ending is required to complete the game.
|
||||||
Choose which ending is required to complete the game.
|
|
||||||
|
|
||||||
Talking to Tirso in Albero will tell you the selected ending for the current game.
|
Talking to Tirso in Albero will tell you the selected ending for the current game.
|
||||||
|
|
||||||
Ending A: Collect all thorn upgrades.
|
Ending A: Collect all thorn upgrades.
|
||||||
|
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation."""
|
||||||
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation.
|
|
||||||
"""
|
|
||||||
display_name = "Ending"
|
display_name = "Ending"
|
||||||
option_any_ending = 0
|
option_any_ending = 0
|
||||||
option_ending_a = 1
|
option_ending_a = 1
|
||||||
@@ -94,18 +78,14 @@ class Ending(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class SkipLongQuests(Toggle):
|
class SkipLongQuests(Toggle):
|
||||||
"""
|
"""Ensures that the rewards for long quests will be filler items.
|
||||||
Ensures that the rewards for long quests will be filler items.
|
Affected locations: \"Albero: Donate 50000 Tears\", \"Ossuary: 11th reward\", \"AtTotS: Miriam's gift\",
|
||||||
|
\"TSC: Jocinero's final reward\""""
|
||||||
Affected locations: "Albero: Donate 50000 Tears", "Ossuary: 11th reward", "AtTotS: Miriam's gift", "TSC: Jocinero's final reward"
|
|
||||||
"""
|
|
||||||
display_name = "Skip Long Quests"
|
display_name = "Skip Long Quests"
|
||||||
|
|
||||||
|
|
||||||
class ThornShuffle(Choice):
|
class ThornShuffle(Choice):
|
||||||
"""
|
"""Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool."""
|
||||||
Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool.
|
|
||||||
"""
|
|
||||||
display_name = "Shuffle Thorn"
|
display_name = "Shuffle Thorn"
|
||||||
option_anywhere = 0
|
option_anywhere = 0
|
||||||
option_local_only = 1
|
option_local_only = 1
|
||||||
@@ -114,68 +94,50 @@ class ThornShuffle(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class DashShuffle(Toggle):
|
class DashShuffle(Toggle):
|
||||||
"""
|
"""Turns the ability to dash into an item that must be found in the multiworld."""
|
||||||
Turns the ability to dash into an item that must be found in the multiworld.
|
|
||||||
"""
|
|
||||||
display_name = "Shuffle Dash"
|
display_name = "Shuffle Dash"
|
||||||
|
|
||||||
|
|
||||||
class WallClimbShuffle(Toggle):
|
class WallClimbShuffle(Toggle):
|
||||||
"""
|
"""Turns the ability to climb walls with your sword into an item that must be found in the multiworld."""
|
||||||
Turns the ability to climb walls with your sword into an item that must be found in the multiworld.
|
|
||||||
"""
|
|
||||||
display_name = "Shuffle Wall Climb"
|
display_name = "Shuffle Wall Climb"
|
||||||
|
|
||||||
|
|
||||||
class ReliquaryShuffle(DefaultOnToggle):
|
class ReliquaryShuffle(DefaultOnToggle):
|
||||||
"""
|
"""Adds the True Torment exclusive Reliquary rosary beads into the item pool."""
|
||||||
Adds the True Torment exclusive Reliquary rosary beads into the item pool.
|
|
||||||
"""
|
|
||||||
display_name = "Shuffle Penitence Rewards"
|
display_name = "Shuffle Penitence Rewards"
|
||||||
|
|
||||||
|
|
||||||
class CustomItem1(Toggle):
|
class CustomItem1(Toggle):
|
||||||
"""
|
"""Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes
|
||||||
Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes and survive.
|
and survive.
|
||||||
|
Must have the \"Blasphemous-Boots-of-Pleading\" mod installed to connect to a multiworld."""
|
||||||
Must have the "Boots of Pleading" mod installed to connect to a multiworld.
|
|
||||||
"""
|
|
||||||
display_name = "Boots of Pleading"
|
display_name = "Boots of Pleading"
|
||||||
|
|
||||||
|
|
||||||
class CustomItem2(Toggle):
|
class CustomItem2(Toggle):
|
||||||
"""
|
"""Adds the custom relic Purified Hand of the Nun into the item pool, which grants the ability to jump
|
||||||
Adds the custom relic Purified Hand of the Nun into the item pool, which grants the ability to jump a second time in mid-air.
|
a second time in mid-air.
|
||||||
|
Must have the \"Blasphemous-Double-Jump\" mod installed to connect to a multiworld."""
|
||||||
Must have the "Double Jump" mod installed to connect to a multiworld.
|
|
||||||
"""
|
|
||||||
display_name = "Purified Hand of the Nun"
|
display_name = "Purified Hand of the Nun"
|
||||||
|
|
||||||
|
|
||||||
class StartWheel(Toggle):
|
class StartWheel(Toggle):
|
||||||
"""
|
"""Changes the beginning gift to The Young Mason's Wheel."""
|
||||||
Changes the beginning gift to The Young Mason's Wheel.
|
|
||||||
"""
|
|
||||||
display_name = "Start with Wheel"
|
display_name = "Start with Wheel"
|
||||||
|
|
||||||
|
|
||||||
class SkillRando(Toggle):
|
class SkillRando(Toggle):
|
||||||
"""
|
"""Randomizes the abilities from the skill tree into the item pool."""
|
||||||
Randomizes the abilities from the skill tree into the item pool.
|
|
||||||
"""
|
|
||||||
display_name = "Skill Randomizer"
|
display_name = "Skill Randomizer"
|
||||||
|
|
||||||
|
|
||||||
class EnemyRando(Choice):
|
class EnemyRando(Choice):
|
||||||
"""
|
"""Randomizes the enemies that appear in each room.
|
||||||
Randomizes the enemies that appear in each room.
|
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in
|
||||||
|
a standard game.
|
||||||
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in a standard game.
|
|
||||||
|
|
||||||
Randomized: Every enemy is completely random, and can appear any number of times.
|
Randomized: Every enemy is completely random, and can appear any number of times.
|
||||||
|
Some enemies will never be randomized."""
|
||||||
Some enemies will never be randomized.
|
|
||||||
"""
|
|
||||||
display_name = "Enemy Randomizer"
|
display_name = "Enemy Randomizer"
|
||||||
option_disabled = 0
|
option_disabled = 0
|
||||||
option_shuffled = 1
|
option_shuffled = 1
|
||||||
@@ -184,75 +146,43 @@ class EnemyRando(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class EnemyGroups(DefaultOnToggle):
|
class EnemyGroups(DefaultOnToggle):
|
||||||
"""
|
"""Randomized enemies will chosen from sets of specific groups.
|
||||||
Randomized enemies will be chosen from sets of specific groups.
|
|
||||||
|
|
||||||
(Weak, normal, large, flying)
|
(Weak, normal, large, flying)
|
||||||
|
Has no effect if Enemy Randomizer is disabled."""
|
||||||
Has no effect if Enemy Randomizer is disabled.
|
|
||||||
"""
|
|
||||||
display_name = "Enemy Groups"
|
display_name = "Enemy Groups"
|
||||||
|
|
||||||
|
|
||||||
class EnemyScaling(DefaultOnToggle):
|
class EnemyScaling(DefaultOnToggle):
|
||||||
"""
|
"""Randomized enemies will have their stats increased or decreased depending on the area they appear in.
|
||||||
Randomized enemies will have their stats increased or decreased depending on the area they appear in.
|
Has no effect if Enemy Randomizer is disabled."""
|
||||||
|
|
||||||
Has no effect if Enemy Randomizer is disabled.
|
|
||||||
"""
|
|
||||||
display_name = "Enemy Scaling"
|
display_name = "Enemy Scaling"
|
||||||
|
|
||||||
|
|
||||||
class BlasphemousDeathLink(DeathLink):
|
class BlasphemousDeathLink(DeathLink):
|
||||||
"""
|
"""When you die, everyone dies. The reverse is also true.
|
||||||
When you die, everyone dies. The reverse is also true.
|
Note that Guilt Fragments will not appear when killed by Death Link."""
|
||||||
|
|
||||||
Note that Guilt Fragments will not appear when killed by Death Link.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
blasphemous_options = {
|
||||||
class BlasphemousOptions(PerGameCommonOptions):
|
"prie_dieu_warp": PrieDieuWarp,
|
||||||
prie_dieu_warp: PrieDieuWarp
|
"skip_cutscenes": SkipCutscenes,
|
||||||
skip_cutscenes: SkipCutscenes
|
"corpse_hints": CorpseHints,
|
||||||
corpse_hints: CorpseHints
|
"difficulty": Difficulty,
|
||||||
difficulty: Difficulty
|
"penitence": Penitence,
|
||||||
penitence: Penitence
|
"starting_location": StartingLocation,
|
||||||
starting_location: StartingLocation
|
"ending": Ending,
|
||||||
ending: Ending
|
"skip_long_quests": SkipLongQuests,
|
||||||
skip_long_quests: SkipLongQuests
|
"thorn_shuffle" : ThornShuffle,
|
||||||
thorn_shuffle: ThornShuffle
|
"dash_shuffle": DashShuffle,
|
||||||
dash_shuffle: DashShuffle
|
"wall_climb_shuffle": WallClimbShuffle,
|
||||||
wall_climb_shuffle: WallClimbShuffle
|
"reliquary_shuffle": ReliquaryShuffle,
|
||||||
reliquary_shuffle: ReliquaryShuffle
|
"boots_of_pleading": CustomItem1,
|
||||||
boots_of_pleading: CustomItem1
|
"purified_hand": CustomItem2,
|
||||||
purified_hand: CustomItem2
|
"start_wheel": StartWheel,
|
||||||
start_wheel: StartWheel
|
"skill_randomizer": SkillRando,
|
||||||
skill_randomizer: SkillRando
|
"enemy_randomizer": EnemyRando,
|
||||||
enemy_randomizer: EnemyRando
|
"enemy_groups": EnemyGroups,
|
||||||
enemy_groups: EnemyGroups
|
"enemy_scaling": EnemyScaling,
|
||||||
enemy_scaling: EnemyScaling
|
"death_link": BlasphemousDeathLink,
|
||||||
death_link: BlasphemousDeathLink
|
"start_inventory": StartInventoryPool
|
||||||
|
}
|
||||||
|
|
||||||
blas_option_groups = [
|
|
||||||
OptionGroup("Quality of Life", [
|
|
||||||
PrieDieuWarp,
|
|
||||||
SkipCutscenes,
|
|
||||||
CorpseHints,
|
|
||||||
SkipLongQuests,
|
|
||||||
StartWheel
|
|
||||||
]),
|
|
||||||
OptionGroup("Moveset", [
|
|
||||||
DashShuffle,
|
|
||||||
WallClimbShuffle,
|
|
||||||
SkillRando,
|
|
||||||
CustomItem1,
|
|
||||||
CustomItem2
|
|
||||||
]),
|
|
||||||
OptionGroup("Enemy Randomizer", [
|
|
||||||
EnemyRando,
|
|
||||||
EnemyGroups,
|
|
||||||
EnemyScaling
|
|
||||||
])
|
|
||||||
]
|
|
||||||
@@ -1,582 +0,0 @@
|
|||||||
# Preprocessor to convert Blasphemous Randomizer logic into a StringWorldDefinition for use with APHKLogicExtractor
|
|
||||||
# https://github.com/BrandenEK/Blasphemous.Randomizer
|
|
||||||
# https://github.com/ArchipelagoMW-HollowKnight/APHKLogicExtractor
|
|
||||||
|
|
||||||
|
|
||||||
import json, requests, argparse
|
|
||||||
from typing import List, Dict, Any
|
|
||||||
|
|
||||||
|
|
||||||
def load_resource_local(file: str) -> List[Dict[str, Any]]:
|
|
||||||
print(f"Reading from {file}")
|
|
||||||
loaded = []
|
|
||||||
with open(file, encoding="utf-8") as f:
|
|
||||||
loaded = read_json(f.readlines())
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
return loaded
|
|
||||||
|
|
||||||
|
|
||||||
def load_resource_from_web(url: str) -> List[Dict[str, Any]]:
|
|
||||||
req = requests.get(url, timeout=1)
|
|
||||||
print(f"Reading from {url}")
|
|
||||||
req.encoding = "utf-8"
|
|
||||||
lines: List[str] = []
|
|
||||||
for line in req.text.splitlines():
|
|
||||||
while "\t" in line:
|
|
||||||
line = line[1::]
|
|
||||||
if line != "":
|
|
||||||
lines.append(line)
|
|
||||||
return read_json(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def read_json(lines: List[str]) -> List[Dict[str, Any]]:
|
|
||||||
loaded = []
|
|
||||||
creating_object: bool = False
|
|
||||||
obj: str = ""
|
|
||||||
for line in lines:
|
|
||||||
stripped = line.strip()
|
|
||||||
if "{" in stripped:
|
|
||||||
creating_object = True
|
|
||||||
obj += stripped
|
|
||||||
continue
|
|
||||||
elif "}," in stripped or "}" in stripped and "]" in lines[lines.index(line)+1]:
|
|
||||||
creating_object = False
|
|
||||||
obj += "}"
|
|
||||||
#print(f"obj = {obj}")
|
|
||||||
loaded.append(json.loads(obj))
|
|
||||||
obj = ""
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not creating_object:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
if "}," in lines[lines.index(line)+1] and stripped[-1] == ",":
|
|
||||||
obj += stripped[:-1]
|
|
||||||
else:
|
|
||||||
obj += stripped
|
|
||||||
except IndexError:
|
|
||||||
obj += stripped
|
|
||||||
|
|
||||||
return loaded
|
|
||||||
|
|
||||||
|
|
||||||
def get_room_from_door(door: str) -> str:
|
|
||||||
return door[:door.find("[")]
|
|
||||||
|
|
||||||
|
|
||||||
def preprocess_logic(is_door: bool, id: str, logic: str) -> str:
|
|
||||||
if id in logic and not is_door:
|
|
||||||
index: int = logic.find(id)
|
|
||||||
logic = logic[:index] + logic[index+len(id)+4:]
|
|
||||||
|
|
||||||
while ">=" in logic:
|
|
||||||
index: int = logic.find(">=")
|
|
||||||
logic = logic[:index-1] + logic[index+3:]
|
|
||||||
|
|
||||||
while ">" in logic:
|
|
||||||
index: int = logic.find(">")
|
|
||||||
count = int(logic[index+2])
|
|
||||||
count += 1
|
|
||||||
logic = logic[:index-1] + str(count) + logic[index+3:]
|
|
||||||
|
|
||||||
while "<=" in logic:
|
|
||||||
index: int = logic.find("<=")
|
|
||||||
logic = logic[:index-1] + logic[index+3:]
|
|
||||||
|
|
||||||
while "<" in logic:
|
|
||||||
index: int = logic.find("<")
|
|
||||||
count = int(logic[index+2])
|
|
||||||
count += 1
|
|
||||||
logic = logic[:index-1] + str(count) + logic[index+3:]
|
|
||||||
|
|
||||||
#print(logic)
|
|
||||||
return logic
|
|
||||||
|
|
||||||
|
|
||||||
def build_logic_conditions(logic: str) -> List[List[str]]:
|
|
||||||
all_conditions: List[List[str]] = []
|
|
||||||
|
|
||||||
parts = logic.split()
|
|
||||||
sub_part: str = ""
|
|
||||||
current_index: int = 0
|
|
||||||
parens: int = -1
|
|
||||||
current_condition: List[str] = []
|
|
||||||
parens_conditions: List[List[List[str]]] = []
|
|
||||||
|
|
||||||
for index, part in enumerate(parts):
|
|
||||||
#print(current_index, index, parens, part)
|
|
||||||
|
|
||||||
# skip parts that have already been handled
|
|
||||||
if index < current_index:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# break loop if reached final part
|
|
||||||
try:
|
|
||||||
parts[index+1]
|
|
||||||
except IndexError:
|
|
||||||
#print("INDEXERROR", part)
|
|
||||||
if parens < 0:
|
|
||||||
current_condition.append(part)
|
|
||||||
if len(parens_conditions) > 0:
|
|
||||||
for i in parens_conditions:
|
|
||||||
for j in i:
|
|
||||||
all_conditions.append(j + current_condition)
|
|
||||||
else:
|
|
||||||
all_conditions.append(current_condition)
|
|
||||||
break
|
|
||||||
|
|
||||||
#print(current_condition, parens, sub_part)
|
|
||||||
|
|
||||||
# prepare for subcondition
|
|
||||||
if "(" in part:
|
|
||||||
# keep track of nested parentheses
|
|
||||||
if parens == -1:
|
|
||||||
parens = 0
|
|
||||||
for char in part:
|
|
||||||
if char == "(":
|
|
||||||
parens += 1
|
|
||||||
|
|
||||||
# add to sub part
|
|
||||||
if sub_part == "":
|
|
||||||
sub_part = part
|
|
||||||
else:
|
|
||||||
sub_part += f" {part}"
|
|
||||||
#if not ")" in part:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# end of subcondition
|
|
||||||
if ")" in part:
|
|
||||||
# read every character in case of multiple closing parentheses
|
|
||||||
for char in part:
|
|
||||||
if char == ")":
|
|
||||||
parens -= 1
|
|
||||||
|
|
||||||
sub_part += f" {part}"
|
|
||||||
|
|
||||||
# if reached end of parentheses, handle subcondition
|
|
||||||
if parens == 0:
|
|
||||||
#print(current_condition, sub_part)
|
|
||||||
parens = -1
|
|
||||||
|
|
||||||
try:
|
|
||||||
parts[index+1]
|
|
||||||
except IndexError:
|
|
||||||
#print("END OF LOGIC")
|
|
||||||
if len(parens_conditions) > 0:
|
|
||||||
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
|
|
||||||
#print("PARENS:", parens_conditions)
|
|
||||||
|
|
||||||
temp_conditions: List[List[str]] = []
|
|
||||||
|
|
||||||
for i in parens_conditions[0]:
|
|
||||||
for j in parens_conditions[1]:
|
|
||||||
temp_conditions.append(i + j)
|
|
||||||
|
|
||||||
parens_conditions.pop(0)
|
|
||||||
parens_conditions.pop(0)
|
|
||||||
|
|
||||||
while len(parens_conditions) > 0:
|
|
||||||
temp_conditions2 = temp_conditions
|
|
||||||
temp_conditions = []
|
|
||||||
for k in temp_conditions2:
|
|
||||||
for l in parens_conditions[0]:
|
|
||||||
temp_conditions.append(k + l)
|
|
||||||
|
|
||||||
parens_conditions.pop(0)
|
|
||||||
|
|
||||||
#print("TEMP:", remove_duplicates(temp_conditions))
|
|
||||||
all_conditions += temp_conditions
|
|
||||||
else:
|
|
||||||
all_conditions += build_logic_subconditions(current_condition, sub_part)
|
|
||||||
else:
|
|
||||||
#print("NEXT PARTS:", parts[index+1], parts[index+2])
|
|
||||||
if parts[index+1] == "&&":
|
|
||||||
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
|
|
||||||
#print("PARENS:", parens_conditions)
|
|
||||||
else:
|
|
||||||
if len(parens_conditions) > 0:
|
|
||||||
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
|
|
||||||
#print("PARENS:", parens_conditions)
|
|
||||||
|
|
||||||
temp_conditions: List[List[str]] = []
|
|
||||||
|
|
||||||
for i in parens_conditions[0]:
|
|
||||||
for j in parens_conditions[1]:
|
|
||||||
temp_conditions.append(i + j)
|
|
||||||
|
|
||||||
parens_conditions.pop(0)
|
|
||||||
parens_conditions.pop(0)
|
|
||||||
|
|
||||||
while len(parens_conditions) > 0:
|
|
||||||
temp_conditions2 = temp_conditions
|
|
||||||
temp_conditions = []
|
|
||||||
for k in temp_conditions2:
|
|
||||||
for l in parens_conditions[0]:
|
|
||||||
temp_conditions.append(k + l)
|
|
||||||
|
|
||||||
parens_conditions.pop(0)
|
|
||||||
|
|
||||||
#print("TEMP:", remove_duplicates(temp_conditions))
|
|
||||||
all_conditions += temp_conditions
|
|
||||||
else:
|
|
||||||
all_conditions += build_logic_subconditions(current_condition, sub_part)
|
|
||||||
|
|
||||||
current_index = index+2
|
|
||||||
|
|
||||||
current_condition = []
|
|
||||||
sub_part = ""
|
|
||||||
|
|
||||||
continue
|
|
||||||
|
|
||||||
# collect all parts until reaching end of parentheses
|
|
||||||
if parens > 0:
|
|
||||||
sub_part += f" {part}"
|
|
||||||
continue
|
|
||||||
|
|
||||||
current_condition.append(part)
|
|
||||||
|
|
||||||
# continue with current condition
|
|
||||||
if parts[index+1] == "&&":
|
|
||||||
current_index = index+2
|
|
||||||
continue
|
|
||||||
|
|
||||||
# add condition to list and start new one
|
|
||||||
elif parts[index+1] == "||":
|
|
||||||
if len(parens_conditions) > 0:
|
|
||||||
for i in parens_conditions:
|
|
||||||
for j in i:
|
|
||||||
all_conditions.append(j + current_condition)
|
|
||||||
parens_conditions = []
|
|
||||||
else:
|
|
||||||
all_conditions.append(current_condition)
|
|
||||||
current_condition = []
|
|
||||||
current_index = index+2
|
|
||||||
continue
|
|
||||||
|
|
||||||
return remove_duplicates(all_conditions)
|
|
||||||
|
|
||||||
|
|
||||||
def build_logic_subconditions(current_condition: List[str], subcondition: str) -> List[List[str]]:
|
|
||||||
#print("STARTED SUBCONDITION", current_condition, subcondition)
|
|
||||||
subconditions = build_logic_conditions(subcondition[1:-1])
|
|
||||||
final_conditions = []
|
|
||||||
|
|
||||||
for condition in subconditions:
|
|
||||||
final_condition = current_condition + condition
|
|
||||||
final_conditions.append(final_condition)
|
|
||||||
|
|
||||||
#print("ENDED SUBCONDITION")
|
|
||||||
#print(final_conditions)
|
|
||||||
return final_conditions
|
|
||||||
|
|
||||||
|
|
||||||
def remove_duplicates(conditions: List[List[str]]) -> List[List[str]]:
|
|
||||||
final_conditions: List[List[str]] = []
|
|
||||||
for condition in conditions:
|
|
||||||
final_conditions.append(list(dict.fromkeys(condition)))
|
|
||||||
|
|
||||||
return final_conditions
|
|
||||||
|
|
||||||
|
|
||||||
def handle_door_visibility(door: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
if door.get("visibilityFlags") == None:
|
|
||||||
return door
|
|
||||||
else:
|
|
||||||
flags: List[str] = str(door.get("visibilityFlags")).split(", ")
|
|
||||||
#print(flags)
|
|
||||||
temp_flags: List[str] = []
|
|
||||||
this_door: bool = False
|
|
||||||
#required_doors: str = ""
|
|
||||||
|
|
||||||
if "ThisDoor" in flags:
|
|
||||||
this_door = True
|
|
||||||
|
|
||||||
#if "requiredDoors" in flags:
|
|
||||||
# required_doors: str = " || ".join(door.get("requiredDoors"))
|
|
||||||
|
|
||||||
if "DoubleJump" in flags:
|
|
||||||
temp_flags.append("DoubleJump")
|
|
||||||
|
|
||||||
if "NormalLogic" in flags:
|
|
||||||
temp_flags.append("NormalLogic")
|
|
||||||
|
|
||||||
if "NormalLogicAndDoubleJump" in flags:
|
|
||||||
temp_flags.append("NormalLogicAndDoubleJump")
|
|
||||||
|
|
||||||
if "HardLogic" in flags:
|
|
||||||
temp_flags.append("HardLogic")
|
|
||||||
|
|
||||||
if "HardLogicAndDoubleJump" in flags:
|
|
||||||
temp_flags.append("HardLogicAndDoubleJump")
|
|
||||||
|
|
||||||
if "EnemySkips" in flags:
|
|
||||||
temp_flags.append("EnemySkips")
|
|
||||||
|
|
||||||
if "EnemySkipsAndDoubleJump" in flags:
|
|
||||||
temp_flags.append("EnemySkipsAndDoubleJump")
|
|
||||||
|
|
||||||
# remove duplicates
|
|
||||||
temp_flags = list(dict.fromkeys(temp_flags))
|
|
||||||
|
|
||||||
original_logic: str = door.get("logic")
|
|
||||||
temp_logic: str = ""
|
|
||||||
|
|
||||||
if this_door:
|
|
||||||
temp_logic = door.get("id")
|
|
||||||
|
|
||||||
if temp_flags != []:
|
|
||||||
if temp_logic != "":
|
|
||||||
temp_logic += " || "
|
|
||||||
temp_logic += ' && '.join(temp_flags)
|
|
||||||
|
|
||||||
if temp_logic != "" and original_logic != None:
|
|
||||||
if len(original_logic.split()) == 1:
|
|
||||||
if len(temp_logic.split()) == 1:
|
|
||||||
door["logic"] = f"{temp_logic} && {original_logic}"
|
|
||||||
else:
|
|
||||||
door["logic"] = f"({temp_logic}) && {original_logic}"
|
|
||||||
else:
|
|
||||||
if len(temp_logic.split()) == 1:
|
|
||||||
door["logic"] = f"{temp_logic} && ({original_logic})"
|
|
||||||
else:
|
|
||||||
door["logic"] = f"({temp_logic}) && ({original_logic})"
|
|
||||||
elif temp_logic != "" and original_logic == None:
|
|
||||||
door["logic"] = temp_logic
|
|
||||||
|
|
||||||
return door
|
|
||||||
|
|
||||||
|
|
||||||
def get_state_provider_for_condition(condition: List[str]) -> str:
|
|
||||||
for item in condition:
|
|
||||||
if (item[0] == "D" and item[3] == "Z" and item[6] == "S")\
|
|
||||||
or (item[0] == "D" and item[3] == "B" and item[4] == "Z" and item[7] == "S"):
|
|
||||||
return item
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument('-l', '--local', action="store_true", help="Use local files in the same directory instead of reading resource files from the BrandenEK/Blasphemous-Randomizer repository.")
|
|
||||||
args = parser.parse_args()
|
|
||||||
return args
|
|
||||||
|
|
||||||
|
|
||||||
def main(args: argparse.Namespace):
|
|
||||||
doors = []
|
|
||||||
locations = []
|
|
||||||
|
|
||||||
if (args.local):
|
|
||||||
doors = load_resource_local("doors.json")
|
|
||||||
locations = load_resource_local("locations_items.json")
|
|
||||||
|
|
||||||
else:
|
|
||||||
doors = load_resource_from_web("https://raw.githubusercontent.com/BrandenEK/Blasphemous-Randomizer/main/resources/data/Randomizer/doors.json")
|
|
||||||
locations = load_resource_from_web("https://raw.githubusercontent.com/BrandenEK/Blasphemous-Randomizer/main/resources/data/Randomizer/locations_items.json")
|
|
||||||
|
|
||||||
original_connections: Dict[str, str] = {}
|
|
||||||
rooms: Dict[str, List[str]] = {}
|
|
||||||
output: Dict[str, Any] = {}
|
|
||||||
logic_objects: List[Dict[str, Any]] = []
|
|
||||||
|
|
||||||
for door in doors:
|
|
||||||
if door.get("originalDoor") != None:
|
|
||||||
if not door.get("id") in original_connections:
|
|
||||||
original_connections[door.get("id")] = door.get("originalDoor")
|
|
||||||
original_connections[door.get("originalDoor")] = door.get("id")
|
|
||||||
|
|
||||||
room: str = get_room_from_door(door.get("originalDoor"))
|
|
||||||
if not room in rooms.keys():
|
|
||||||
rooms[room] = [door.get("id")]
|
|
||||||
else:
|
|
||||||
rooms[room].append(door.get("id"))
|
|
||||||
|
|
||||||
def flip_doors_in_condition(condition: List[str]) -> List[str]:
|
|
||||||
new_condition = []
|
|
||||||
for item in condition:
|
|
||||||
if item in original_connections:
|
|
||||||
new_condition.append(original_connections[item])
|
|
||||||
else:
|
|
||||||
new_condition.append(item)
|
|
||||||
|
|
||||||
return new_condition
|
|
||||||
|
|
||||||
for room in rooms.keys():
|
|
||||||
obj = {
|
|
||||||
"Name": room,
|
|
||||||
"Logic": [],
|
|
||||||
"Handling": "Default"
|
|
||||||
}
|
|
||||||
|
|
||||||
for door in rooms[room]:
|
|
||||||
logic = {
|
|
||||||
"StateProvider": door,
|
|
||||||
"Conditions": [],
|
|
||||||
"StateModifiers": []
|
|
||||||
}
|
|
||||||
obj["Logic"].append(logic)
|
|
||||||
|
|
||||||
logic_objects.append(obj)
|
|
||||||
|
|
||||||
for door in doors:
|
|
||||||
if door.get("direction") == 5:
|
|
||||||
continue
|
|
||||||
|
|
||||||
handling: str = "Transition"
|
|
||||||
if "Cell" in door.get("id"):
|
|
||||||
handling = "Default"
|
|
||||||
obj = {
|
|
||||||
"Name": door.get("id"),
|
|
||||||
"Logic": [],
|
|
||||||
"Handling": handling
|
|
||||||
}
|
|
||||||
|
|
||||||
visibility_flags: List[str] = []
|
|
||||||
if door.get("visibilityFlags") != None:
|
|
||||||
visibility_flags = str(door.get("visibilityFlags")).split(", ")
|
|
||||||
if "1" in visibility_flags:
|
|
||||||
visibility_flags.remove("1")
|
|
||||||
visibility_flags.append("ThisDoor")
|
|
||||||
|
|
||||||
required_doors: List[str] = []
|
|
||||||
if door.get("requiredDoors"):
|
|
||||||
required_doors = door.get("requiredDoors")
|
|
||||||
|
|
||||||
if len(visibility_flags) > 0:
|
|
||||||
for flag in visibility_flags:
|
|
||||||
if flag == "RequiredDoors":
|
|
||||||
continue
|
|
||||||
|
|
||||||
if flag == "ThisDoor":
|
|
||||||
flag = original_connections[door.get("id")]
|
|
||||||
|
|
||||||
if door.get("logic") != None:
|
|
||||||
logic: str = door.get("logic")
|
|
||||||
logic = f"{flag} && ({logic})"
|
|
||||||
logic = preprocess_logic(True, door.get("id"), logic)
|
|
||||||
conditions = build_logic_conditions(logic)
|
|
||||||
for condition in conditions:
|
|
||||||
condition = flip_doors_in_condition(condition)
|
|
||||||
state_provider: str = get_room_from_door(door.get("id"))
|
|
||||||
|
|
||||||
if get_state_provider_for_condition(condition) != None:
|
|
||||||
state_provider = get_state_provider_for_condition(condition)
|
|
||||||
condition.remove(state_provider)
|
|
||||||
|
|
||||||
logic = {
|
|
||||||
"StateProvider": state_provider,
|
|
||||||
"Conditions": condition,
|
|
||||||
"StateModifiers": []
|
|
||||||
}
|
|
||||||
obj["Logic"].append(logic)
|
|
||||||
else:
|
|
||||||
logic = {
|
|
||||||
"StateProvider": get_room_from_door(door.get("id")),
|
|
||||||
"Conditions": [flag],
|
|
||||||
"StateModifiers": []
|
|
||||||
}
|
|
||||||
obj["Logic"].append(logic)
|
|
||||||
|
|
||||||
if "RequiredDoors" in visibility_flags:
|
|
||||||
for d in required_doors:
|
|
||||||
flipped = original_connections[d]
|
|
||||||
if door.get("logic") != None:
|
|
||||||
logic: str = preprocess_logic(True, door.get("id"), door.get("logic"))
|
|
||||||
conditions = build_logic_conditions(logic)
|
|
||||||
for condition in conditions:
|
|
||||||
condition = flip_doors_in_condition(condition)
|
|
||||||
state_provider: str = flipped
|
|
||||||
|
|
||||||
if flipped in condition:
|
|
||||||
condition.remove(flipped)
|
|
||||||
|
|
||||||
logic = {
|
|
||||||
"StateProvider": state_provider,
|
|
||||||
"Conditions": condition,
|
|
||||||
"StateModifiers": []
|
|
||||||
}
|
|
||||||
obj["Logic"].append(logic)
|
|
||||||
else:
|
|
||||||
logic = {
|
|
||||||
"StateProvider": flipped,
|
|
||||||
"Conditions": [],
|
|
||||||
"StateModifiers": []
|
|
||||||
}
|
|
||||||
obj["Logic"].append(logic)
|
|
||||||
|
|
||||||
else:
|
|
||||||
if door.get("logic") != None:
|
|
||||||
logic: str = preprocess_logic(True, door.get("id"), door.get("logic"))
|
|
||||||
conditions = build_logic_conditions(logic)
|
|
||||||
for condition in conditions:
|
|
||||||
condition = flip_doors_in_condition(condition)
|
|
||||||
stateProvider: str = get_room_from_door(door.get("id"))
|
|
||||||
|
|
||||||
if get_state_provider_for_condition(condition) != None:
|
|
||||||
stateProvider = get_state_provider_for_condition(condition)
|
|
||||||
condition.remove(stateProvider)
|
|
||||||
|
|
||||||
logic = {
|
|
||||||
"StateProvider": stateProvider,
|
|
||||||
"Conditions": condition,
|
|
||||||
"StateModifiers": []
|
|
||||||
}
|
|
||||||
obj["Logic"].append(logic)
|
|
||||||
else:
|
|
||||||
logic = {
|
|
||||||
"StateProvider": get_room_from_door(door.get("id")),
|
|
||||||
"Conditions": [],
|
|
||||||
"StateModifiers": []
|
|
||||||
}
|
|
||||||
obj["Logic"].append(logic)
|
|
||||||
|
|
||||||
logic_objects.append(obj)
|
|
||||||
|
|
||||||
for location in locations:
|
|
||||||
obj = {
|
|
||||||
"Name": location.get("id"),
|
|
||||||
"Logic": [],
|
|
||||||
"Handling": "Location"
|
|
||||||
}
|
|
||||||
|
|
||||||
if location.get("logic") != None:
|
|
||||||
for condition in build_logic_conditions(preprocess_logic(False, location.get("id"), location.get("logic"))):
|
|
||||||
condition = flip_doors_in_condition(condition)
|
|
||||||
stateProvider: str = location.get("room")
|
|
||||||
|
|
||||||
if get_state_provider_for_condition(condition) != None:
|
|
||||||
stateProvider = get_state_provider_for_condition(condition)
|
|
||||||
condition.remove(stateProvider)
|
|
||||||
|
|
||||||
if stateProvider == "Initial":
|
|
||||||
stateProvider = None
|
|
||||||
|
|
||||||
logic = {
|
|
||||||
"StateProvider": stateProvider,
|
|
||||||
"Conditions": condition,
|
|
||||||
"StateModifiers": []
|
|
||||||
}
|
|
||||||
obj["Logic"].append(logic)
|
|
||||||
else:
|
|
||||||
stateProvider: str = location.get("room")
|
|
||||||
if stateProvider == "Initial":
|
|
||||||
stateProvider = None
|
|
||||||
logic = {
|
|
||||||
"StateProvider": stateProvider,
|
|
||||||
"Conditions": [],
|
|
||||||
"StateModifiers": []
|
|
||||||
}
|
|
||||||
obj["Logic"].append(logic)
|
|
||||||
|
|
||||||
logic_objects.append(obj)
|
|
||||||
|
|
||||||
output["LogicObjects"] = logic_objects
|
|
||||||
|
|
||||||
with open("StringWorldDefinition.json", "w") as file:
|
|
||||||
print("Writing to StringWorldDefinition.json")
|
|
||||||
file.write(json.dumps(output, indent=4))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main(parse_args())
|
|
||||||
5405
worlds/blasphemous/Rooms.py
Normal file
5405
worlds/blasphemous/Rooms.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,12 +8,12 @@ unrandomized_dict: Dict[str, str] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
junk_locations: Set[str] = {
|
junk_locations: Set[str] = [
|
||||||
"Albero: Donate 50000 Tears",
|
"Albero: Donate 50000 Tears",
|
||||||
"Ossuary: 11th reward",
|
"Ossuary: 11th reward",
|
||||||
"AtTotS: Miriam's gift",
|
"AtTotS: Miriam's gift",
|
||||||
"TSC: Jocinero's final reward"
|
"TSC: Jocinero's final reward"
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
thorn_set: Set[str] = {
|
thorn_set: Set[str] = {
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
from typing import Dict, List, Set, Any
|
from typing import Dict, List, Set, Any
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
|
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
|
||||||
from Options import OptionError
|
|
||||||
from worlds.AutoWorld import World, WebWorld
|
from worlds.AutoWorld import World, WebWorld
|
||||||
from .Items import base_id, item_table, group_table, tears_list, reliquary_set
|
from .Items import base_id, item_table, group_table, tears_set, reliquary_set, event_table
|
||||||
from .Locations import location_names
|
from .Locations import location_table
|
||||||
from .Rules import BlasRules
|
from .Rooms import room_table, door_table
|
||||||
from worlds.generic.Rules import set_rule
|
from .Rules import rules
|
||||||
from .Options import BlasphemousOptions, blas_option_groups
|
from worlds.generic.Rules import set_rule, add_rule
|
||||||
|
from .Options import blasphemous_options
|
||||||
from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict
|
from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict
|
||||||
from .region_data import regions, locations
|
|
||||||
|
|
||||||
class BlasphemousWeb(WebWorld):
|
class BlasphemousWeb(WebWorld):
|
||||||
theme = "stone"
|
theme = "stone"
|
||||||
@@ -21,33 +21,39 @@ class BlasphemousWeb(WebWorld):
|
|||||||
"setup/en",
|
"setup/en",
|
||||||
["TRPG"]
|
["TRPG"]
|
||||||
)]
|
)]
|
||||||
option_groups = blas_option_groups
|
|
||||||
|
|
||||||
|
|
||||||
class BlasphemousWorld(World):
|
class BlasphemousWorld(World):
|
||||||
"""
|
"""
|
||||||
Blasphemous is a challenging Metroidvania set in the cursed land of Cvstodia. Play as the Penitent One, trapped
|
Blasphemous is a challenging Metroidvania set in the cursed land of Cvstodia. Play as the Penitent One, trapped
|
||||||
in an endless cycle of death and rebirth, and free the world from its terrible fate in your quest to break
|
in an endless cycle of death and rebirth, and free the world from it's terrible fate in your quest to break
|
||||||
your eternal damnation!
|
your eternal damnation!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
game = "Blasphemous"
|
game: str = "Blasphemous"
|
||||||
web = BlasphemousWeb()
|
web = BlasphemousWeb()
|
||||||
|
|
||||||
item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)}
|
item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)}
|
||||||
location_name_to_id = {loc: (base_id + index) for index, loc in enumerate(location_names.values())}
|
location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)}
|
||||||
|
location_name_to_game_id = {loc["name"]: loc["game_id"] for loc in location_table}
|
||||||
|
|
||||||
item_name_groups = group_table
|
item_name_groups = group_table
|
||||||
options_dataclass = BlasphemousOptions
|
option_definitions = blasphemous_options
|
||||||
options: BlasphemousOptions
|
|
||||||
|
|
||||||
required_client_version = (0, 4, 7)
|
required_client_version = (0, 4, 2)
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, multiworld, player):
|
def __init__(self, multiworld, player):
|
||||||
super(BlasphemousWorld, self).__init__(multiworld, player)
|
super(BlasphemousWorld, self).__init__(multiworld, player)
|
||||||
self.start_room: str = "D17Z01S01"
|
self.start_room: str = "D17Z01S01"
|
||||||
self.disabled_locations: List[str] = []
|
self.door_connections: Dict[str, str] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def set_rules(self):
|
||||||
|
rules(self)
|
||||||
|
for door in door_table:
|
||||||
|
add_rule(self.multiworld.get_location(door["Id"], self.player),
|
||||||
|
lambda state: state.can_reach(self.get_connected_door(door["Id"])), self.player)
|
||||||
|
|
||||||
|
|
||||||
def create_item(self, name: str) -> "BlasphemousItem":
|
def create_item(self, name: str) -> "BlasphemousItem":
|
||||||
@@ -62,56 +68,64 @@ class BlasphemousWorld(World):
|
|||||||
|
|
||||||
|
|
||||||
def get_filler_item_name(self) -> str:
|
def get_filler_item_name(self) -> str:
|
||||||
return self.random.choice(tears_list)
|
return self.multiworld.random.choice(tears_set)
|
||||||
|
|
||||||
|
|
||||||
def generate_early(self):
|
def generate_early(self):
|
||||||
if not self.options.starting_location.randomized:
|
world = self.multiworld
|
||||||
if self.options.starting_location == "mourning_havoc" and self.options.difficulty < 2:
|
player = self.player
|
||||||
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
|
|
||||||
f"{self.options.starting_location} cannot be chosen if Difficulty is lower than Hard.")
|
|
||||||
|
|
||||||
if (self.options.starting_location == "brotherhood" or self.options.starting_location == "mourning_havoc") \
|
if not world.starting_location[player].randomized:
|
||||||
and self.options.dash_shuffle:
|
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
||||||
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
|
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||||
f"{self.options.starting_location} cannot be chosen if Shuffle Dash is enabled.")
|
" cannot be chosen if Difficulty is lower than Hard.")
|
||||||
|
|
||||||
if self.options.starting_location == "grievance" and self.options.wall_climb_shuffle:
|
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
|
||||||
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
|
and world.dash_shuffle[player]:
|
||||||
f"{self.options.starting_location} cannot be chosen if Shuffle Wall Climb is enabled.")
|
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||||
|
" cannot be chosen if Shuffle Dash is enabled.")
|
||||||
|
|
||||||
|
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
|
||||||
|
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||||
|
" cannot be chosen if Shuffle Wall Climb is enabled.")
|
||||||
else:
|
else:
|
||||||
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
|
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
|
||||||
|
invalid: bool = False
|
||||||
|
|
||||||
if self.options.difficulty < 2:
|
if world.difficulty[player].value < 2:
|
||||||
locations.remove(6)
|
locations.remove(6)
|
||||||
|
|
||||||
if self.options.dash_shuffle:
|
if world.dash_shuffle[player]:
|
||||||
locations.remove(0)
|
locations.remove(0)
|
||||||
if 6 in locations:
|
if 6 in locations:
|
||||||
locations.remove(6)
|
locations.remove(6)
|
||||||
|
|
||||||
if self.options.wall_climb_shuffle:
|
if world.wall_climb_shuffle[player]:
|
||||||
locations.remove(3)
|
locations.remove(3)
|
||||||
|
|
||||||
if self.options.starting_location.value not in locations:
|
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
||||||
self.options.starting_location.value = self.random.choice(locations)
|
invalid = True
|
||||||
|
|
||||||
|
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
|
||||||
|
and world.dash_shuffle[player]:
|
||||||
|
invalid = True
|
||||||
|
|
||||||
|
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
|
||||||
|
invalid = True
|
||||||
|
|
||||||
|
if invalid:
|
||||||
|
world.starting_location[player].value = world.random.choice(locations)
|
||||||
|
|
||||||
|
|
||||||
if not self.options.dash_shuffle:
|
if not world.dash_shuffle[player]:
|
||||||
self.multiworld.push_precollected(self.create_item("Dash Ability"))
|
world.push_precollected(self.create_item("Dash Ability"))
|
||||||
|
|
||||||
if not self.options.wall_climb_shuffle:
|
if not world.wall_climb_shuffle[player]:
|
||||||
self.multiworld.push_precollected(self.create_item("Wall Climb Ability"))
|
world.push_precollected(self.create_item("Wall Climb Ability"))
|
||||||
|
|
||||||
if not self.options.boots_of_pleading:
|
if world.skip_long_quests[player]:
|
||||||
self.disabled_locations.append("RE401")
|
|
||||||
|
|
||||||
if not self.options.purified_hand:
|
|
||||||
self.disabled_locations.append("RE402")
|
|
||||||
|
|
||||||
if self.options.skip_long_quests:
|
|
||||||
for loc in junk_locations:
|
for loc in junk_locations:
|
||||||
self.options.exclude_locations.value.add(loc)
|
world.exclude_locations[player].value.add(loc)
|
||||||
|
|
||||||
start_rooms: Dict[int, str] = {
|
start_rooms: Dict[int, str] = {
|
||||||
0: "D17Z01S01",
|
0: "D17Z01S01",
|
||||||
@@ -123,10 +137,13 @@ class BlasphemousWorld(World):
|
|||||||
6: "D20Z02S09"
|
6: "D20Z02S09"
|
||||||
}
|
}
|
||||||
|
|
||||||
self.start_room = start_rooms[self.options.starting_location.value]
|
self.start_room = start_rooms[world.starting_location[player].value]
|
||||||
|
|
||||||
|
|
||||||
def create_items(self):
|
def create_items(self):
|
||||||
|
world = self.multiworld
|
||||||
|
player = self.player
|
||||||
|
|
||||||
removed: int = 0
|
removed: int = 0
|
||||||
to_remove: List[str] = [
|
to_remove: List[str] = [
|
||||||
"Tears of Atonement (250)",
|
"Tears of Atonement (250)",
|
||||||
@@ -139,46 +156,46 @@ class BlasphemousWorld(World):
|
|||||||
skipped_items = []
|
skipped_items = []
|
||||||
junk: int = 0
|
junk: int = 0
|
||||||
|
|
||||||
for item, count in self.options.start_inventory.value.items():
|
for item, count in world.start_inventory[player].value.items():
|
||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
skipped_items.append(item)
|
skipped_items.append(item)
|
||||||
junk += 1
|
junk += 1
|
||||||
|
|
||||||
skipped_items.extend(unrandomized_dict.values())
|
skipped_items.extend(unrandomized_dict.values())
|
||||||
|
|
||||||
if self.options.thorn_shuffle == "vanilla":
|
if world.thorn_shuffle[player] == 2:
|
||||||
for _ in range(8):
|
for i in range(8):
|
||||||
skipped_items.append("Thorn Upgrade")
|
skipped_items.append("Thorn Upgrade")
|
||||||
|
|
||||||
if self.options.dash_shuffle:
|
if world.dash_shuffle[player]:
|
||||||
skipped_items.append(to_remove[removed])
|
skipped_items.append(to_remove[removed])
|
||||||
removed += 1
|
removed += 1
|
||||||
elif not self.options.dash_shuffle:
|
elif not world.dash_shuffle[player]:
|
||||||
skipped_items.append("Dash Ability")
|
skipped_items.append("Dash Ability")
|
||||||
|
|
||||||
if self.options.wall_climb_shuffle:
|
if world.wall_climb_shuffle[player]:
|
||||||
skipped_items.append(to_remove[removed])
|
skipped_items.append(to_remove[removed])
|
||||||
removed += 1
|
removed += 1
|
||||||
elif not self.options.wall_climb_shuffle:
|
elif not world.wall_climb_shuffle[player]:
|
||||||
skipped_items.append("Wall Climb Ability")
|
skipped_items.append("Wall Climb Ability")
|
||||||
|
|
||||||
if not self.options.reliquary_shuffle:
|
if not world.reliquary_shuffle[player]:
|
||||||
skipped_items.extend(reliquary_set)
|
skipped_items.extend(reliquary_set)
|
||||||
elif self.options.reliquary_shuffle:
|
elif world.reliquary_shuffle[player]:
|
||||||
for _ in range(3):
|
for i in range(3):
|
||||||
skipped_items.append(to_remove[removed])
|
skipped_items.append(to_remove[removed])
|
||||||
removed += 1
|
removed += 1
|
||||||
|
|
||||||
if not self.options.boots_of_pleading:
|
if not world.boots_of_pleading[player]:
|
||||||
skipped_items.append("Boots of Pleading")
|
skipped_items.append("Boots of Pleading")
|
||||||
|
|
||||||
if not self.options.purified_hand:
|
if not world.purified_hand[player]:
|
||||||
skipped_items.append("Purified Hand of the Nun")
|
skipped_items.append("Purified Hand of the Nun")
|
||||||
|
|
||||||
if self.options.start_wheel:
|
if world.start_wheel[player]:
|
||||||
skipped_items.append("The Young Mason's Wheel")
|
skipped_items.append("The Young Mason's Wheel")
|
||||||
|
|
||||||
if not self.options.skill_randomizer:
|
if not world.skill_randomizer[player]:
|
||||||
skipped_items.extend(skill_dict.values())
|
skipped_items.extend(skill_dict.values())
|
||||||
|
|
||||||
counter = Counter(skipped_items)
|
counter = Counter(skipped_items)
|
||||||
@@ -191,138 +208,184 @@ class BlasphemousWorld(World):
|
|||||||
if count <= 0:
|
if count <= 0:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
for _ in range(count):
|
for i in range(count):
|
||||||
pool.append(self.create_item(item["name"]))
|
pool.append(self.create_item(item["name"]))
|
||||||
|
|
||||||
for _ in range(junk):
|
for _ in range(junk):
|
||||||
pool.append(self.create_item(self.get_filler_item_name()))
|
pool.append(self.create_item(self.get_filler_item_name()))
|
||||||
|
|
||||||
self.multiworld.itempool += pool
|
world.itempool += pool
|
||||||
|
|
||||||
|
|
||||||
|
def pre_fill(self):
|
||||||
|
world = self.multiworld
|
||||||
|
player = self.player
|
||||||
|
|
||||||
self.place_items_from_dict(unrandomized_dict)
|
self.place_items_from_dict(unrandomized_dict)
|
||||||
|
|
||||||
if self.options.thorn_shuffle == "vanilla":
|
if world.thorn_shuffle[player] == 2:
|
||||||
self.place_items_from_set(thorn_set, "Thorn Upgrade")
|
self.place_items_from_set(thorn_set, "Thorn Upgrade")
|
||||||
|
|
||||||
if self.options.start_wheel:
|
if world.start_wheel[player]:
|
||||||
self.get_location("Beginning gift").place_locked_item(self.create_item("The Young Mason's Wheel"))
|
world.get_location("Beginning gift", player)\
|
||||||
|
.place_locked_item(self.create_item("The Young Mason's Wheel"))
|
||||||
|
|
||||||
if not self.options.skill_randomizer:
|
if not world.skill_randomizer[player]:
|
||||||
self.place_items_from_dict(skill_dict)
|
self.place_items_from_dict(skill_dict)
|
||||||
|
|
||||||
if self.options.thorn_shuffle == "local_only":
|
if world.thorn_shuffle[player] == 1:
|
||||||
self.options.local_items.value.add("Thorn Upgrade")
|
world.local_items[player].value.add("Thorn Upgrade")
|
||||||
|
|
||||||
|
|
||||||
def place_items_from_set(self, location_set: Set[str], name: str):
|
def place_items_from_set(self, location_set: Set[str], name: str):
|
||||||
for loc in location_set:
|
for loc in location_set:
|
||||||
self.get_location(loc).place_locked_item(self.create_item(name))
|
self.multiworld.get_location(loc, self.player)\
|
||||||
|
.place_locked_item(self.create_item(name))
|
||||||
|
|
||||||
|
|
||||||
def place_items_from_dict(self, option_dict: Dict[str, str]):
|
def place_items_from_dict(self, option_dict: Dict[str, str]):
|
||||||
for loc, item in option_dict.items():
|
for loc, item in option_dict.items():
|
||||||
self.get_location(loc).place_locked_item(self.create_item(item))
|
self.multiworld.get_location(loc, self.player)\
|
||||||
|
.place_locked_item(self.create_item(item))
|
||||||
|
|
||||||
|
|
||||||
def create_regions(self) -> None:
|
def create_regions(self) -> None:
|
||||||
multiworld = self.multiworld
|
|
||||||
player = self.player
|
player = self.player
|
||||||
|
world = self.multiworld
|
||||||
|
|
||||||
created_regions: List[str] = []
|
menu_region = Region("Menu", player, world)
|
||||||
|
misc_region = Region("Misc", player, world)
|
||||||
|
world.regions += [menu_region, misc_region]
|
||||||
|
|
||||||
for r in regions:
|
for room in room_table:
|
||||||
multiworld.regions.append(Region(r["name"], player, multiworld))
|
region = Region(room, player, world)
|
||||||
created_regions.append(r["name"])
|
world.regions.append(region)
|
||||||
|
|
||||||
self.get_region("Menu").add_exits({self.start_room: "New Game"})
|
menu_region.add_exits({self.start_room: "New Game"})
|
||||||
|
world.get_region(self.start_room, player).add_exits({"Misc": "Misc"})
|
||||||
|
|
||||||
blas_logic = BlasRules(self)
|
for door in door_table:
|
||||||
|
if door.get("OriginalDoor") is None:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if not door["Id"] in self.door_connections.keys():
|
||||||
|
self.door_connections[door["Id"]] = door["OriginalDoor"]
|
||||||
|
self.door_connections[door["OriginalDoor"]] = door["Id"]
|
||||||
|
|
||||||
for r in regions:
|
parent_region: Region = self.get_room_from_door(door["Id"])
|
||||||
region = self.get_region(r["name"])
|
target_region: Region = self.get_room_from_door(door["OriginalDoor"])
|
||||||
|
parent_region.add_exits({
|
||||||
|
target_region.name: door["Id"]
|
||||||
|
}, {
|
||||||
|
target_region.name: lambda x: door.get("VisibilityFlags") != 1
|
||||||
|
})
|
||||||
|
|
||||||
for e in r["exits"]:
|
for index, loc in enumerate(location_table):
|
||||||
region.add_exits({e["target"]}, {e["target"]: blas_logic.load_rule(True, r["name"], e)})
|
if not world.boots_of_pleading[player] and loc["name"] == "BotSS: 2nd meeting with Redento":
|
||||||
|
continue
|
||||||
|
if not world.purified_hand[player] and loc["name"] == "MoM: Western room ledge":
|
||||||
|
continue
|
||||||
|
|
||||||
for l in [l for l in r["locations"] if l not in self.disabled_locations]:
|
region: Region = world.get_region(loc["room"], player)
|
||||||
region.add_locations({location_names[l]: self.location_name_to_id[location_names[l]]}, BlasphemousLocation)
|
region.add_locations({loc["name"]: base_id + index})
|
||||||
|
#id = base_id + location_table.index(loc)
|
||||||
|
#reg.locations.append(BlasphemousLocation(player, loc["name"], id, reg))
|
||||||
|
|
||||||
for t in r["transitions"]:
|
for e, r in event_table.items():
|
||||||
if t == r["name"]:
|
region: Region = world.get_region(r, player)
|
||||||
continue
|
event = BlasphemousLocation(player, e, None, region)
|
||||||
|
event.show_in_spoiler = False
|
||||||
|
event.place_locked_item(self.create_event(e))
|
||||||
|
region.locations.append(event)
|
||||||
|
|
||||||
if t in created_regions:
|
for door in door_table:
|
||||||
region.add_exits({t})
|
region: Region = self.get_room_from_door(self.door_connections[door["Id"]])
|
||||||
else:
|
event = BlasphemousLocation(player, door["Id"], None, region)
|
||||||
multiworld.regions.append(Region(t, player, multiworld))
|
event.show_in_spoiler = False
|
||||||
created_regions.append(t)
|
event.place_locked_item(self.create_event(door["Id"]))
|
||||||
region.add_exits({t})
|
region.locations.append(event)
|
||||||
|
|
||||||
|
victory = Location(player, "His Holiness Escribar", None, world.get_region("D07Z01S03", player))
|
||||||
for l in [l for l in locations if l["name"] not in self.disabled_locations]:
|
|
||||||
location = self.get_location(location_names[l["name"]])
|
|
||||||
set_rule(location, blas_logic.load_rule(False, l["name"], l))
|
|
||||||
|
|
||||||
for rname, ename in blas_logic.indirect_conditions:
|
|
||||||
self.multiworld.register_indirect_condition(self.get_region(rname), self.get_entrance(ename))
|
|
||||||
#from Utils import visualize_regions
|
|
||||||
#visualize_regions(self.get_region("Menu"), "blasphemous_regions.puml")
|
|
||||||
|
|
||||||
victory = Location(player, "His Holiness Escribar", None, self.get_region("D07Z01S03[W]"))
|
|
||||||
victory.place_locked_item(self.create_event("Victory"))
|
victory.place_locked_item(self.create_event("Victory"))
|
||||||
self.get_region("D07Z01S03[W]").locations.append(victory)
|
world.get_region("D07Z01S03", player).locations.append(victory)
|
||||||
|
|
||||||
if self.options.ending == "ending_a":
|
if world.ending[self.player].value == 1:
|
||||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
|
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
|
||||||
elif self.options.ending == "ending_c":
|
elif world.ending[self.player].value == 2:
|
||||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and
|
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and
|
||||||
state.has("Holy Wound of Abnegation", player))
|
state.has("Holy Wound of Abnegation", player))
|
||||||
|
|
||||||
multiworld.completion_condition[self.player] = lambda state: state.has("Victory", player)
|
world.completion_condition[self.player] = lambda state: state.has("Victory", player)
|
||||||
|
|
||||||
|
|
||||||
|
def get_room_from_door(self, door: str) -> Region:
|
||||||
|
return self.multiworld.get_region(door.split("[")[0], self.player)
|
||||||
|
|
||||||
|
|
||||||
|
def get_connected_door(self, door: str) -> Entrance:
|
||||||
|
return self.multiworld.get_entrance(self.door_connections[door], self.player)
|
||||||
|
|
||||||
|
|
||||||
def fill_slot_data(self) -> Dict[str, Any]:
|
def fill_slot_data(self) -> Dict[str, Any]:
|
||||||
slot_data: Dict[str, Any] = {}
|
slot_data: Dict[str, Any] = {}
|
||||||
|
locations = []
|
||||||
doors: Dict[str, str] = {}
|
doors: Dict[str, str] = {}
|
||||||
|
|
||||||
|
world = self.multiworld
|
||||||
|
player = self.player
|
||||||
thorns: bool = True
|
thorns: bool = True
|
||||||
|
|
||||||
if self.options.thorn_shuffle == "vanilla":
|
if world.thorn_shuffle[player].value == 2:
|
||||||
thorns = False
|
thorns = False
|
||||||
|
|
||||||
|
for loc in world.get_filled_locations(player):
|
||||||
|
if loc.item.code == None:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
data = {
|
||||||
|
"id": self.location_name_to_game_id[loc.name],
|
||||||
|
"ap_id": loc.address,
|
||||||
|
"name": loc.item.name,
|
||||||
|
"player_name": world.player_name[loc.item.player],
|
||||||
|
"type": int(loc.item.classification)
|
||||||
|
}
|
||||||
|
|
||||||
|
locations.append(data)
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"LogicDifficulty": self.options.difficulty.value,
|
"LogicDifficulty": world.difficulty[player].value,
|
||||||
"StartingLocation": self.options.starting_location.value,
|
"StartingLocation": world.starting_location[player].value,
|
||||||
"VersionCreated": "AP",
|
"VersionCreated": "AP",
|
||||||
|
|
||||||
"UnlockTeleportation": bool(self.options.prie_dieu_warp.value),
|
"UnlockTeleportation": bool(world.prie_dieu_warp[player].value),
|
||||||
"AllowHints": bool(self.options.corpse_hints.value),
|
"AllowHints": bool(world.corpse_hints[player].value),
|
||||||
"AllowPenitence": bool(self.options.penitence.value),
|
"AllowPenitence": bool(world.penitence[player].value),
|
||||||
|
|
||||||
"ShuffleReliquaries": bool(self.options.reliquary_shuffle.value),
|
"ShuffleReliquaries": bool(world.reliquary_shuffle[player].value),
|
||||||
"ShuffleBootsOfPleading": bool(self.options.boots_of_pleading.value),
|
"ShuffleBootsOfPleading": bool(world.boots_of_pleading[player].value),
|
||||||
"ShufflePurifiedHand": bool(self.options.purified_hand.value),
|
"ShufflePurifiedHand": bool(world.purified_hand[player].value),
|
||||||
"ShuffleDash": bool(self.options.dash_shuffle.value),
|
"ShuffleDash": bool(world.dash_shuffle[player].value),
|
||||||
"ShuffleWallClimb": bool(self.options.wall_climb_shuffle.value),
|
"ShuffleWallClimb": bool(world.wall_climb_shuffle[player].value),
|
||||||
|
|
||||||
"ShuffleSwordSkills": bool(self.options.wall_climb_shuffle.value),
|
"ShuffleSwordSkills": bool(world.skill_randomizer[player].value),
|
||||||
"ShuffleThorns": thorns,
|
"ShuffleThorns": thorns,
|
||||||
"JunkLongQuests": bool(self.options.skip_long_quests.value),
|
"JunkLongQuests": bool(world.skip_long_quests[player].value),
|
||||||
"StartWithWheel": bool(self.options.start_wheel.value),
|
"StartWithWheel": bool(world.start_wheel[player].value),
|
||||||
|
|
||||||
"EnemyShuffleType": self.options.enemy_randomizer.value,
|
"EnemyShuffleType": world.enemy_randomizer[player].value,
|
||||||
"MaintainClass": bool(self.options.enemy_groups.value),
|
"MaintainClass": bool(world.enemy_groups[player].value),
|
||||||
"AreaScaling": bool(self.options.enemy_scaling.value),
|
"AreaScaling": bool(world.enemy_scaling[player].value),
|
||||||
|
|
||||||
"BossShuffleType": 0,
|
"BossShuffleType": 0,
|
||||||
"DoorShuffleType": 0
|
"DoorShuffleType": 0
|
||||||
}
|
}
|
||||||
|
|
||||||
slot_data = {
|
slot_data = {
|
||||||
"locationinfo": [{"gameId": loc, "apId": (base_id + index)} for index, loc in enumerate(location_names)],
|
"locations": locations,
|
||||||
"doors": doors,
|
"doors": doors,
|
||||||
"cfg": config,
|
"cfg": config,
|
||||||
"ending": self.options.ending.value,
|
"ending": world.ending[self.player].value,
|
||||||
"death_link": bool(self.options.death_link.value)
|
"death_link": bool(world.death_link[self.player].value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return slot_data
|
return slot_data
|
||||||
|
|||||||
@@ -1,17 +1,48 @@
|
|||||||
# Blasphemous Multiworld Setup Guide
|
# Blasphemous Multiworld Setup Guide
|
||||||
|
|
||||||
It is recommended to use the [Mod Installer](https://github.com/BrandenEK/Blasphemous.Modding.Installer) to handle installing and updating mods. If you would prefer to install mods manually, instructions can also be found at the Mod Installer repository.
|
## Useful Links
|
||||||
|
|
||||||
You will need the [Multiworld](https://github.com/BrandenEK/Blasphemous.Randomizer.Multiworld) mod to play an Archipelago randomizer.
|
Required:
|
||||||
|
- Blasphemous: [Steam](https://store.steampowered.com/app/774361/Blasphemous/)
|
||||||
|
- The GOG version of Blasphemous will also work.
|
||||||
|
- Blasphemous Mod Installer: [GitHub](https://github.com/BrandenEK/Blasphemous-Mod-Installer)
|
||||||
|
- Blasphemous Modding API: [GitHub](https://github.com/BrandenEK/Blasphemous-Modding-API)
|
||||||
|
- Blasphemous Randomizer: [GitHub](https://github.com/BrandenEK/Blasphemous-Randomizer)
|
||||||
|
- Blasphemous Multiworld: [GitHub](https://github.com/BrandenEK/Blasphemous-Multiworld)
|
||||||
|
|
||||||
Some optional mods are also recommended:
|
Optional:
|
||||||
- [Rando Map](https://github.com/BrandenEK/Blasphemous.Randomizer.MapTracker)
|
- In-game map tracker: [GitHub](https://github.com/BrandenEK/Blasphemous-Rando-Map)
|
||||||
- [Boots of Pleading](https://github.com/BrandenEK/Blasphemous.BootsOfPleading) (Required if the "Boots of Pleading" option is enabled)
|
- Quick Prie Dieu warp mod: [GitHub](https://github.com/BadMagic100/Blasphemous-PrieWarp)
|
||||||
- [Double Jump](https://github.com/BrandenEK/Blasphemous.DoubleJump) (Required if the "Purified Hand of the Nun" option is enabled)
|
- Boots of Pleading mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Boots-of-Pleading)
|
||||||
|
- Double Jump mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Double-Jump)
|
||||||
|
|
||||||
To connect to a multiworld: Choose a save file and enter the address, your name, and the password (if the server has one) into the menu.
|
## Mod Installer (Recommended)
|
||||||
|
|
||||||
After connecting, there are some commands you can use in the console, which can be opened by pressing backslash `\`:
|
1. Download the [Mod Installer](https://github.com/BrandenEK/Blasphemous-Mod-Installer),
|
||||||
- `ap status` - Display connection status.
|
and point it to your install directory for Blasphemous.
|
||||||
- `ap say [message]` - Send a message to the server.
|
|
||||||
- `ap hint [item]` - Request a hint for an item from the server.
|
2. Install the `Modding API`, `Randomizer`, and `Multiworld` mods. Optionally, you can also install the
|
||||||
|
`Rando Map`, `PrieWarp`, `Boots of Pleading`, and `Double Jump` mods, and set up the PopTracker pack if desired.
|
||||||
|
|
||||||
|
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both
|
||||||
|
the Randomizer and Multiworld on the title screen.
|
||||||
|
|
||||||
|
## Manual Installation
|
||||||
|
|
||||||
|
1. Download the [Modding API](https://github.com/BrandenEK/Blasphemous-Modding-API/releases), and follow
|
||||||
|
the [installation instructions](https://github.com/BrandenEK/Blasphemous-Modding-API#installation) on the GitHub page.
|
||||||
|
|
||||||
|
2. After the Modding API has been installed, download the
|
||||||
|
[Randomizer](https://github.com/BrandenEK/Blasphemous-Randomizer/releases) and
|
||||||
|
[Multiworld](https://github.com/BrandenEK/Blasphemous-Multiworld/releases) archives, and extract the contents of both
|
||||||
|
into the `Modding` folder. Then, add any desired additional mods.
|
||||||
|
|
||||||
|
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both
|
||||||
|
the Randomizer and Multiworld on the title screen.
|
||||||
|
|
||||||
|
## Connecting
|
||||||
|
|
||||||
|
To connect to an Archipelago server, open the in-game console by pressing backslash `\` and use
|
||||||
|
the command `multiworld connect [address:port] [name] [password]`.
|
||||||
|
The port and password are both optional - if no port is provided then the default port of 38281 is used.
|
||||||
|
**Make sure to connect to the server before attempting to start a new save file.**
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
from test.bases import WorldTestBase
|
|
||||||
from .. import BlasphemousWorld
|
|
||||||
|
|
||||||
|
|
||||||
class BlasphemousTestBase(WorldTestBase):
|
|
||||||
game = "Blasphemous"
|
|
||||||
world: BlasphemousWorld
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
from . import BlasphemousTestBase
|
|
||||||
from ..Locations import location_names
|
|
||||||
|
|
||||||
|
|
||||||
class BotSSGauntletTest(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "albero",
|
|
||||||
"wall_climb_shuffle": True,
|
|
||||||
"dash_shuffle": True
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def run_default_tests(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_botss_gauntlet(self) -> None:
|
|
||||||
self.assertAccessDependency([location_names["CO25"]], [["Dash Ability", "Wall Climb Ability"]], True)
|
|
||||||
|
|
||||||
|
|
||||||
class BackgroundZonesTest(BlasphemousTestBase):
|
|
||||||
@property
|
|
||||||
def run_default_tests(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_dc_shroud(self) -> None:
|
|
||||||
self.assertAccessDependency([location_names["RB03"]], [["Shroud of Dreamt Sins"]], True)
|
|
||||||
|
|
||||||
def test_wothp_bronze_cells(self) -> None:
|
|
||||||
bronze_locations = [
|
|
||||||
location_names["QI70"],
|
|
||||||
location_names["RESCUED_CHERUB_03"]
|
|
||||||
]
|
|
||||||
|
|
||||||
self.assertAccessDependency(bronze_locations, [["Key of the Secular"]], True)
|
|
||||||
|
|
||||||
def test_wothp_silver_cells(self) -> None:
|
|
||||||
silver_locations = [
|
|
||||||
location_names["CO24"],
|
|
||||||
location_names["RESCUED_CHERUB_34"],
|
|
||||||
location_names["CO37"],
|
|
||||||
location_names["RESCUED_CHERUB_04"]
|
|
||||||
]
|
|
||||||
|
|
||||||
self.assertAccessDependency(silver_locations, [["Key of the Scribe"]], True)
|
|
||||||
|
|
||||||
def test_wothp_gold_cells(self) -> None:
|
|
||||||
gold_locations = [
|
|
||||||
location_names["QI51"],
|
|
||||||
location_names["CO26"],
|
|
||||||
location_names["CO02"]
|
|
||||||
]
|
|
||||||
|
|
||||||
self.assertAccessDependency(gold_locations, [["Key of the Inquisitor"]], True)
|
|
||||||
|
|
||||||
def test_wothp_quirce(self) -> None:
|
|
||||||
self.assertAccessDependency([location_names["BS14"]], [["Key of the Secular", "Key of the Scribe", "Key of the Inquisitor"]], True)
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
from . import BlasphemousTestBase
|
|
||||||
|
|
||||||
|
|
||||||
class TestBrotherhoodEasy(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "brotherhood",
|
|
||||||
"difficulty": "easy"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestBrotherhoodNormal(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "brotherhood",
|
|
||||||
"difficulty": "normal"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestBrotherhoodHard(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "brotherhood",
|
|
||||||
"difficulty": "hard"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestAlberoEasy(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "albero",
|
|
||||||
"difficulty": "easy"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestAlberoNormal(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "albero",
|
|
||||||
"difficulty": "normal"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestAlberoHard(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "albero",
|
|
||||||
"difficulty": "hard"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestConventEasy(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "convent",
|
|
||||||
"difficulty": "easy"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestConventNormal(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "convent",
|
|
||||||
"difficulty": "normal"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestConventHard(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "convent",
|
|
||||||
"difficulty": "hard"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestGrievanceEasy(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "grievance",
|
|
||||||
"difficulty": "easy"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestGrievanceNormal(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "grievance",
|
|
||||||
"difficulty": "normal"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestGrievanceHard(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "grievance",
|
|
||||||
"difficulty": "hard"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestKnotOfWordsEasy(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "knot_of_words",
|
|
||||||
"difficulty": "easy"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestKnotOfWordsNormal(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "knot_of_words",
|
|
||||||
"difficulty": "normal"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestKnotOfWordsHard(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "knot_of_words",
|
|
||||||
"difficulty": "hard"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestRooftopsEasy(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "rooftops",
|
|
||||||
"difficulty": "easy"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestRooftopsNormal(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "rooftops",
|
|
||||||
"difficulty": "normal"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestRooftopsHard(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "rooftops",
|
|
||||||
"difficulty": "hard"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# mourning and havoc can't be selected on easy or normal. hard only
|
|
||||||
class TestMourningHavocHard(BlasphemousTestBase):
|
|
||||||
options = {
|
|
||||||
"starting_location": "mourning_havoc",
|
|
||||||
"difficulty": "hard"
|
|
||||||
}
|
|
||||||
@@ -28,7 +28,7 @@ An Example `AP.json` file:
|
|||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"Url": "archipelago.gg:12345",
|
"Url": "archipelago:12345",
|
||||||
"SlotName": "Maddy",
|
"SlotName": "Maddy",
|
||||||
"Password": ""
|
"Password": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,15 +44,15 @@ class ChecksFinderWorld(World):
|
|||||||
self.multiworld.regions += [menu, board]
|
self.multiworld.regions += [menu, board]
|
||||||
|
|
||||||
def create_items(self):
|
def create_items(self):
|
||||||
# Generate list of items
|
# Generate item pool
|
||||||
items_to_create = []
|
itempool = []
|
||||||
# Add the map width and height stuff
|
# Add the map width and height stuff
|
||||||
items_to_create += ["Map Width"] * 5 # 10 - 5
|
itempool += ["Map Width"] * 5 # 10 - 5
|
||||||
items_to_create += ["Map Height"] * 5 # 10 - 5
|
itempool += ["Map Height"] * 5 # 10 - 5
|
||||||
# Add the map bombs
|
# Add the map bombs
|
||||||
items_to_create += ["Map Bombs"] * 15 # 20 - 5
|
itempool += ["Map Bombs"] * 15 # 20 - 5
|
||||||
# Convert list into real items
|
# Convert itempool into real items
|
||||||
itempool = [self.create_item(item) for item in items_to_create]
|
itempool = [self.create_item(item) for item in itempool]
|
||||||
|
|
||||||
self.multiworld.itempool += itempool
|
self.multiworld.itempool += itempool
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
from typing import Callable, Dict, NamedTuple, Optional, TYPE_CHECKING
|
from typing import Callable, Dict, NamedTuple, Optional
|
||||||
|
|
||||||
from BaseClasses import Item, ItemClassification
|
from BaseClasses import Item, ItemClassification, MultiWorld
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from . import CliqueWorld
|
|
||||||
|
|
||||||
|
|
||||||
class CliqueItem(Item):
|
class CliqueItem(Item):
|
||||||
@@ -13,7 +10,7 @@ class CliqueItem(Item):
|
|||||||
class CliqueItemData(NamedTuple):
|
class CliqueItemData(NamedTuple):
|
||||||
code: Optional[int] = None
|
code: Optional[int] = None
|
||||||
type: ItemClassification = ItemClassification.filler
|
type: ItemClassification = ItemClassification.filler
|
||||||
can_create: Callable[["CliqueWorld"], bool] = lambda world: True
|
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True
|
||||||
|
|
||||||
|
|
||||||
item_data_table: Dict[str, CliqueItemData] = {
|
item_data_table: Dict[str, CliqueItemData] = {
|
||||||
@@ -24,11 +21,11 @@ item_data_table: Dict[str, CliqueItemData] = {
|
|||||||
"Button Activation": CliqueItemData(
|
"Button Activation": CliqueItemData(
|
||||||
code=69696968,
|
code=69696968,
|
||||||
type=ItemClassification.progression,
|
type=ItemClassification.progression,
|
||||||
can_create=lambda world: world.options.hard_mode,
|
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]),
|
||||||
),
|
),
|
||||||
"A Cool Filler Item (No Satisfaction Guaranteed)": CliqueItemData(
|
"A Cool Filler Item (No Satisfaction Guaranteed)": CliqueItemData(
|
||||||
code=69696967,
|
code=69696967,
|
||||||
can_create=lambda world: False # Only created from `get_filler_item_name`.
|
can_create=lambda multiworld, player: False # Only created from `get_filler_item_name`.
|
||||||
),
|
),
|
||||||
"The Urge to Push": CliqueItemData(
|
"The Urge to Push": CliqueItemData(
|
||||||
type=ItemClassification.progression,
|
type=ItemClassification.progression,
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
from typing import Callable, Dict, NamedTuple, Optional, TYPE_CHECKING
|
from typing import Callable, Dict, NamedTuple, Optional
|
||||||
|
|
||||||
from BaseClasses import Location
|
from BaseClasses import Location, MultiWorld
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from . import CliqueWorld
|
|
||||||
|
|
||||||
|
|
||||||
class CliqueLocation(Location):
|
class CliqueLocation(Location):
|
||||||
@@ -13,7 +10,7 @@ class CliqueLocation(Location):
|
|||||||
class CliqueLocationData(NamedTuple):
|
class CliqueLocationData(NamedTuple):
|
||||||
region: str
|
region: str
|
||||||
address: Optional[int] = None
|
address: Optional[int] = None
|
||||||
can_create: Callable[["CliqueWorld"], bool] = lambda world: True
|
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True
|
||||||
locked_item: Optional[str] = None
|
locked_item: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -25,7 +22,7 @@ location_data_table: Dict[str, CliqueLocationData] = {
|
|||||||
"The Item on the Desk": CliqueLocationData(
|
"The Item on the Desk": CliqueLocationData(
|
||||||
region="The Button Realm",
|
region="The Button Realm",
|
||||||
address=69696968,
|
address=69696968,
|
||||||
can_create=lambda world: world.options.hard_mode,
|
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]),
|
||||||
),
|
),
|
||||||
"In the Player's Mind": CliqueLocationData(
|
"In the Player's Mind": CliqueLocationData(
|
||||||
region="The Button Realm",
|
region="The Button Realm",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from dataclasses import dataclass
|
from typing import Dict
|
||||||
from Options import Choice, Toggle, PerGameCommonOptions, StartInventoryPool
|
|
||||||
|
from Options import Choice, Option, Toggle
|
||||||
|
|
||||||
|
|
||||||
class HardMode(Toggle):
|
class HardMode(Toggle):
|
||||||
@@ -24,11 +25,10 @@ class ButtonColor(Choice):
|
|||||||
option_black = 11
|
option_black = 11
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
clique_options: Dict[str, type(Option)] = {
|
||||||
class CliqueOptions(PerGameCommonOptions):
|
"color": ButtonColor,
|
||||||
color: ButtonColor
|
"hard_mode": HardMode,
|
||||||
hard_mode: HardMode
|
|
||||||
start_inventory_from_pool: StartInventoryPool
|
|
||||||
|
|
||||||
# DeathLink is always on. Always.
|
# DeathLink is always on. Always.
|
||||||
# death_link: DeathLink
|
# "death_link": DeathLink,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
from typing import Callable, TYPE_CHECKING
|
from typing import Callable
|
||||||
|
|
||||||
from BaseClasses import CollectionState
|
from BaseClasses import CollectionState, MultiWorld
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from . import CliqueWorld
|
|
||||||
|
|
||||||
|
|
||||||
def get_button_rule(world: "CliqueWorld") -> Callable[[CollectionState], bool]:
|
def get_button_rule(multiworld: MultiWorld, player: int) -> Callable[[CollectionState], bool]:
|
||||||
if world.options.hard_mode:
|
if getattr(multiworld, "hard_mode")[player]:
|
||||||
return lambda state: state.has("Button Activation", world.player)
|
return lambda state: state.has("Button Activation", player)
|
||||||
|
|
||||||
return lambda state: True
|
return lambda state: True
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from typing import List, Dict, Any
|
from typing import List
|
||||||
|
|
||||||
from BaseClasses import Region, Tutorial
|
from BaseClasses import Region, Tutorial
|
||||||
from worlds.AutoWorld import WebWorld, World
|
from worlds.AutoWorld import WebWorld, World
|
||||||
from .Items import CliqueItem, item_data_table, item_table
|
from .Items import CliqueItem, item_data_table, item_table
|
||||||
from .Locations import CliqueLocation, location_data_table, location_table, locked_locations
|
from .Locations import CliqueLocation, location_data_table, location_table, locked_locations
|
||||||
from .Options import CliqueOptions
|
from .Options import clique_options
|
||||||
from .Regions import region_data_table
|
from .Regions import region_data_table
|
||||||
from .Rules import get_button_rule
|
from .Rules import get_button_rule
|
||||||
|
|
||||||
@@ -38,8 +38,7 @@ class CliqueWorld(World):
|
|||||||
|
|
||||||
game = "Clique"
|
game = "Clique"
|
||||||
web = CliqueWebWorld()
|
web = CliqueWebWorld()
|
||||||
options: CliqueOptions
|
option_definitions = clique_options
|
||||||
options_dataclass = CliqueOptions
|
|
||||||
location_name_to_id = location_table
|
location_name_to_id = location_table
|
||||||
item_name_to_id = item_table
|
item_name_to_id = item_table
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@ class CliqueWorld(World):
|
|||||||
def create_items(self) -> None:
|
def create_items(self) -> None:
|
||||||
item_pool: List[CliqueItem] = []
|
item_pool: List[CliqueItem] = []
|
||||||
for name, item in item_data_table.items():
|
for name, item in item_data_table.items():
|
||||||
if item.code and item.can_create(self):
|
if item.code and item.can_create(self.multiworld, self.player):
|
||||||
item_pool.append(self.create_item(name))
|
item_pool.append(self.create_item(name))
|
||||||
|
|
||||||
self.multiworld.itempool += item_pool
|
self.multiworld.itempool += item_pool
|
||||||
@@ -62,40 +61,41 @@ class CliqueWorld(World):
|
|||||||
|
|
||||||
# Create locations.
|
# Create locations.
|
||||||
for region_name, region_data in region_data_table.items():
|
for region_name, region_data in region_data_table.items():
|
||||||
region = self.get_region(region_name)
|
region = self.multiworld.get_region(region_name, self.player)
|
||||||
region.add_locations({
|
region.add_locations({
|
||||||
location_name: location_data.address for location_name, location_data in location_data_table.items()
|
location_name: location_data.address for location_name, location_data in location_data_table.items()
|
||||||
if location_data.region == region_name and location_data.can_create(self)
|
if location_data.region == region_name and location_data.can_create(self.multiworld, self.player)
|
||||||
}, CliqueLocation)
|
}, CliqueLocation)
|
||||||
region.add_exits(region_data_table[region_name].connecting_regions)
|
region.add_exits(region_data_table[region_name].connecting_regions)
|
||||||
|
|
||||||
# Place locked locations.
|
# Place locked locations.
|
||||||
for location_name, location_data in locked_locations.items():
|
for location_name, location_data in locked_locations.items():
|
||||||
# Ignore locations we never created.
|
# Ignore locations we never created.
|
||||||
if not location_data.can_create(self):
|
if not location_data.can_create(self.multiworld, self.player):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
locked_item = self.create_item(location_data_table[location_name].locked_item)
|
locked_item = self.create_item(location_data_table[location_name].locked_item)
|
||||||
self.get_location(location_name).place_locked_item(locked_item)
|
self.multiworld.get_location(location_name, self.player).place_locked_item(locked_item)
|
||||||
|
|
||||||
# Set priority location for the Big Red Button!
|
# Set priority location for the Big Red Button!
|
||||||
self.options.priority_locations.value.add("The Big Red Button")
|
self.multiworld.priority_locations[self.player].value.add("The Big Red Button")
|
||||||
|
|
||||||
def get_filler_item_name(self) -> str:
|
def get_filler_item_name(self) -> str:
|
||||||
return "A Cool Filler Item (No Satisfaction Guaranteed)"
|
return "A Cool Filler Item (No Satisfaction Guaranteed)"
|
||||||
|
|
||||||
def set_rules(self) -> None:
|
def set_rules(self) -> None:
|
||||||
button_rule = get_button_rule(self)
|
button_rule = get_button_rule(self.multiworld, self.player)
|
||||||
self.get_location("The Big Red Button").access_rule = button_rule
|
self.multiworld.get_location("The Big Red Button", self.player).access_rule = button_rule
|
||||||
self.get_location("In the Player's Mind").access_rule = button_rule
|
self.multiworld.get_location("In the Player's Mind", self.player).access_rule = button_rule
|
||||||
|
|
||||||
# Do not allow button activations on buttons.
|
# Do not allow button activations on buttons.
|
||||||
self.get_location("The Big Red Button").item_rule = lambda item: item.name != "Button Activation"
|
self.multiworld.get_location("The Big Red Button", self.player).item_rule =\
|
||||||
|
lambda item: item.name != "Button Activation"
|
||||||
|
|
||||||
# Completion condition.
|
# Completion condition.
|
||||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("The Urge to Push", self.player)
|
self.multiworld.completion_condition[self.player] = lambda state: state.has("The Urge to Push", self.player)
|
||||||
|
|
||||||
def fill_slot_data(self) -> Dict[str, Any]:
|
def fill_slot_data(self):
|
||||||
return {
|
return {
|
||||||
"color": self.options.color.current_key
|
"color": getattr(self.multiworld, "color")[self.player].current_key
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,267 +0,0 @@
|
|||||||
# In almost all cases, we leave boss and enemy randomization up to the static randomizer. But for
|
|
||||||
# Yhorm specifically we need to know where he ends up in order to ensure that the Storm Ruler is
|
|
||||||
# available before his fight.
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Set
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DS3BossInfo:
|
|
||||||
"""The set of locations a given boss location blocks access to."""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
"""The boss's name."""
|
|
||||||
|
|
||||||
id: int
|
|
||||||
"""The game's ID for this particular boss."""
|
|
||||||
|
|
||||||
dlc: bool = False
|
|
||||||
"""This boss appears in one of the game's DLCs."""
|
|
||||||
|
|
||||||
before_storm_ruler: bool = False
|
|
||||||
"""Whether this location appears before it's possible to get Storm Ruler in vanilla.
|
|
||||||
|
|
||||||
This is used to determine whether it's safe to place Yhorm here if weapons
|
|
||||||
aren't randomized.
|
|
||||||
"""
|
|
||||||
|
|
||||||
locations: Set[str] = field(default_factory=set)
|
|
||||||
"""Additional individual locations that can't be accessed until the boss is dead."""
|
|
||||||
|
|
||||||
|
|
||||||
# Note: the static randomizer splits up some bosses into separate fights for separate phases, each
|
|
||||||
# of which can be individually replaced by Yhorm.
|
|
||||||
all_bosses = [
|
|
||||||
DS3BossInfo("Iudex Gundyr", 4000800, before_storm_ruler = True, locations = {
|
|
||||||
"CA: Coiled Sword - boss drop"
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Vordt of the Boreal Valley", 3000800, before_storm_ruler = True, locations = {
|
|
||||||
"HWL: Soul of Boreal Valley Vordt"
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Curse-rotted Greatwood", 3100800, locations = {
|
|
||||||
"US: Soul of the Rotted Greatwood",
|
|
||||||
"US: Transposing Kiln - boss drop",
|
|
||||||
"US: Wargod Wooden Shield - Pit of Hollows",
|
|
||||||
"FS: Hawkwood's Shield - gravestone after Hawkwood leaves",
|
|
||||||
"FS: Sunset Shield - by grave after killing Hodrick w/Sirris",
|
|
||||||
"US: Sunset Helm - Pit of Hollows after killing Hodrick w/Sirris",
|
|
||||||
"US: Sunset Armor - pit of hollows after killing Hodrick w/Sirris",
|
|
||||||
"US: Sunset Gauntlets - pit of hollows after killing Hodrick w/Sirris",
|
|
||||||
"US: Sunset Leggings - pit of hollows after killing Hodrick w/Sirris",
|
|
||||||
"FS: Sunless Talisman - Sirris, kill GA boss",
|
|
||||||
"FS: Sunless Veil - shop, Sirris quest, kill GA boss",
|
|
||||||
"FS: Sunless Armor - shop, Sirris quest, kill GA boss",
|
|
||||||
"FS: Sunless Gauntlets - shop, Sirris quest, kill GA boss",
|
|
||||||
"FS: Sunless Leggings - shop, Sirris quest, kill GA boss",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Crystal Sage", 3300850, locations = {
|
|
||||||
"RS: Soul of a Crystal Sage",
|
|
||||||
"FS: Sage's Big Hat - shop after killing RS boss",
|
|
||||||
"FS: Hawkwood's Shield - gravestone after Hawkwood leaves",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Deacons of the Deep", 3500800, locations = {
|
|
||||||
"CD: Soul of the Deacons of the Deep",
|
|
||||||
"CD: Small Doll - boss drop",
|
|
||||||
"CD: Archdeacon White Crown - boss room after killing boss",
|
|
||||||
"CD: Archdeacon Holy Garb - boss room after killing boss",
|
|
||||||
"CD: Archdeacon Skirt - boss room after killing boss",
|
|
||||||
"FS: Hawkwood's Shield - gravestone after Hawkwood leaves",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Abyss Watchers", 3300801, before_storm_ruler = True, locations = {
|
|
||||||
"FK: Soul of the Blood of the Wolf",
|
|
||||||
"FK: Cinders of a Lord - Abyss Watcher",
|
|
||||||
"FS: Undead Legion Helm - shop after killing FK boss",
|
|
||||||
"FS: Undead Legion Armor - shop after killing FK boss",
|
|
||||||
"FS: Undead Legion Gauntlet - shop after killing FK boss",
|
|
||||||
"FS: Undead Legion Leggings - shop after killing FK boss",
|
|
||||||
"FS: Farron Ring - Hawkwood",
|
|
||||||
"FS: Hawkwood's Shield - gravestone after Hawkwood leaves",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("High Lord Wolnir", 3800800, before_storm_ruler = True, locations = {
|
|
||||||
"CC: Soul of High Lord Wolnir",
|
|
||||||
"FS: Wolnir's Crown - shop after killing CC boss",
|
|
||||||
"CC: Homeward Bone - Irithyll bridge",
|
|
||||||
"CC: Pontiff's Right Eye - Irithyll bridge, miniboss drop",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Pontiff Sulyvahn", 3700850, locations = {
|
|
||||||
"IBV: Soul of Pontiff Sulyvahn",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Old Demon King", 3800830, locations = {
|
|
||||||
"SL: Soul of the Old Demon King",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Aldrich, Devourer of Gods", 3700800, locations = {
|
|
||||||
"AL: Soul of Aldrich",
|
|
||||||
"AL: Cinders of a Lord - Aldrich",
|
|
||||||
"FS: Smough's Helm - shop after killing AL boss",
|
|
||||||
"FS: Smough's Armor - shop after killing AL boss",
|
|
||||||
"FS: Smough's Gauntlets - shop after killing AL boss",
|
|
||||||
"FS: Smough's Leggings - shop after killing AL boss",
|
|
||||||
"AL: Sun Princess Ring - dark cathedral, after boss",
|
|
||||||
"FS: Leonhard's Garb - shop after killing Leonhard",
|
|
||||||
"FS: Leonhard's Gauntlets - shop after killing Leonhard",
|
|
||||||
"FS: Leonhard's Trousers - shop after killing Leonhard",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Dancer of the Boreal Valley", 3000899, locations = {
|
|
||||||
"HWL: Soul of the Dancer",
|
|
||||||
"FS: Dancer's Crown - shop after killing LC entry boss",
|
|
||||||
"FS: Dancer's Armor - shop after killing LC entry boss",
|
|
||||||
"FS: Dancer's Gauntlets - shop after killing LC entry boss",
|
|
||||||
"FS: Dancer's Leggings - shop after killing LC entry boss",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Dragonslayer Armour", 3010800, locations = {
|
|
||||||
"LC: Soul of Dragonslayer Armour",
|
|
||||||
"FS: Morne's Helm - shop after killing Eygon or LC boss",
|
|
||||||
"FS: Morne's Armor - shop after killing Eygon or LC boss",
|
|
||||||
"FS: Morne's Gauntlets - shop after killing Eygon or LC boss",
|
|
||||||
"FS: Morne's Leggings - shop after killing Eygon or LC boss",
|
|
||||||
"LC: Titanite Chunk - down stairs after boss",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Consumed King Oceiros", 3000830, locations = {
|
|
||||||
"CKG: Soul of Consumed Oceiros",
|
|
||||||
"CKG: Titanite Scale - tomb, chest #1",
|
|
||||||
"CKG: Titanite Scale - tomb, chest #2",
|
|
||||||
"CKG: Drakeblood Helm - tomb, after killing AP mausoleum NPC",
|
|
||||||
"CKG: Drakeblood Armor - tomb, after killing AP mausoleum NPC",
|
|
||||||
"CKG: Drakeblood Gauntlets - tomb, after killing AP mausoleum NPC",
|
|
||||||
"CKG: Drakeblood Leggings - tomb, after killing AP mausoleum NPC",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Champion Gundyr", 4000830, locations = {
|
|
||||||
"UG: Soul of Champion Gundyr",
|
|
||||||
"FS: Gundyr's Helm - shop after killing UG boss",
|
|
||||||
"FS: Gundyr's Armor - shop after killing UG boss",
|
|
||||||
"FS: Gundyr's Gauntlets - shop after killing UG boss",
|
|
||||||
"FS: Gundyr's Leggings - shop after killing UG boss",
|
|
||||||
"UG: Hornet Ring - environs, right of main path after killing FK boss",
|
|
||||||
"UG: Chaos Blade - environs, left of shrine",
|
|
||||||
"UG: Blacksmith Hammer - shrine, Andre's room",
|
|
||||||
"UG: Eyes of a Fire Keeper - shrine, Irina's room",
|
|
||||||
"UG: Coiled Sword Fragment - shrine, dead bonfire",
|
|
||||||
"UG: Soul of a Crestfallen Knight - environs, above shrine entrance",
|
|
||||||
"UG: Life Ring+3 - shrine, behind big throne",
|
|
||||||
"UG: Ring of Steel Protection+1 - environs, behind bell tower",
|
|
||||||
"FS: Ring of Sacrifice - Yuria shop",
|
|
||||||
"UG: Ember - shop",
|
|
||||||
"UG: Priestess Ring - shop",
|
|
||||||
"UG: Wolf Knight Helm - shop after killing FK boss",
|
|
||||||
"UG: Wolf Knight Armor - shop after killing FK boss",
|
|
||||||
"UG: Wolf Knight Gauntlets - shop after killing FK boss",
|
|
||||||
"UG: Wolf Knight Leggings - shop after killing FK boss",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Ancient Wyvern", 3200800),
|
|
||||||
DS3BossInfo("King of the Storm", 3200850, locations = {
|
|
||||||
"AP: Soul of the Nameless King",
|
|
||||||
"FS: Golden Crown - shop after killing AP boss",
|
|
||||||
"FS: Dragonscale Armor - shop after killing AP boss",
|
|
||||||
"FS: Golden Bracelets - shop after killing AP boss",
|
|
||||||
"FS: Dragonscale Waistcloth - shop after killing AP boss",
|
|
||||||
"AP: Titanite Slab - plaza",
|
|
||||||
"AP: Covetous Gold Serpent Ring+2 - plaza",
|
|
||||||
"AP: Dragonslayer Helm - plaza",
|
|
||||||
"AP: Dragonslayer Armor - plaza",
|
|
||||||
"AP: Dragonslayer Gauntlets - plaza",
|
|
||||||
"AP: Dragonslayer Leggings - plaza",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Nameless King", 3200851, locations = {
|
|
||||||
"AP: Soul of the Nameless King",
|
|
||||||
"FS: Golden Crown - shop after killing AP boss",
|
|
||||||
"FS: Dragonscale Armor - shop after killing AP boss",
|
|
||||||
"FS: Golden Bracelets - shop after killing AP boss",
|
|
||||||
"FS: Dragonscale Waistcloth - shop after killing AP boss",
|
|
||||||
"AP: Titanite Slab - plaza",
|
|
||||||
"AP: Covetous Gold Serpent Ring+2 - plaza",
|
|
||||||
"AP: Dragonslayer Helm - plaza",
|
|
||||||
"AP: Dragonslayer Armor - plaza",
|
|
||||||
"AP: Dragonslayer Gauntlets - plaza",
|
|
||||||
"AP: Dragonslayer Leggings - plaza",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Lothric, Younger Prince", 3410830, locations = {
|
|
||||||
"GA: Soul of the Twin Princes",
|
|
||||||
"GA: Cinders of a Lord - Lothric Prince",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Lorian, Elder Prince", 3410832, locations = {
|
|
||||||
"GA: Soul of the Twin Princes",
|
|
||||||
"GA: Cinders of a Lord - Lothric Prince",
|
|
||||||
"FS: Lorian's Helm - shop after killing GA boss",
|
|
||||||
"FS: Lorian's Armor - shop after killing GA boss",
|
|
||||||
"FS: Lorian's Gauntlets - shop after killing GA boss",
|
|
||||||
"FS: Lorian's Leggings - shop after killing GA boss",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Champion's Gravetender and Gravetender Greatwolf", 4500860, dlc = True,
|
|
||||||
locations = {"PW1: Valorheart - boss drop"}),
|
|
||||||
DS3BossInfo("Sister Friede", 4500801, dlc = True, locations = {
|
|
||||||
"PW2: Soul of Sister Friede",
|
|
||||||
"PW2: Titanite Slab - boss drop",
|
|
||||||
"PW1: Titanite Slab - Corvian",
|
|
||||||
"FS: Ordained Hood - shop after killing PW2 boss",
|
|
||||||
"FS: Ordained Dress - shop after killing PW2 boss",
|
|
||||||
"FS: Ordained Trousers - shop after killing PW2 boss",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Blackflame Friede", 4500800, dlc = True, locations = {
|
|
||||||
"PW2: Soul of Sister Friede",
|
|
||||||
"PW1: Titanite Slab - Corvian",
|
|
||||||
"FS: Ordained Hood - shop after killing PW2 boss",
|
|
||||||
"FS: Ordained Dress - shop after killing PW2 boss",
|
|
||||||
"FS: Ordained Trousers - shop after killing PW2 boss",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Demon Prince", 5000801, dlc = True, locations = {
|
|
||||||
"DH: Soul of the Demon Prince",
|
|
||||||
"DH: Small Envoy Banner - boss drop",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Halflight, Spear of the Church", 5100800, dlc = True, locations = {
|
|
||||||
"RC: Titanite Slab - mid boss drop",
|
|
||||||
"RC: Titanite Slab - ashes, NPC drop",
|
|
||||||
"RC: Titanite Slab - ashes, mob drop",
|
|
||||||
"RC: Filianore's Spear Ornament - mid boss drop",
|
|
||||||
"RC: Crucifix of the Mad King - ashes, NPC drop",
|
|
||||||
"RC: Shira's Crown - Shira's room after killing ashes NPC",
|
|
||||||
"RC: Shira's Armor - Shira's room after killing ashes NPC",
|
|
||||||
"RC: Shira's Gloves - Shira's room after killing ashes NPC",
|
|
||||||
"RC: Shira's Trousers - Shira's room after killing ashes NPC",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Darkeater Midir", 5100850, dlc = True, locations = {
|
|
||||||
"RC: Soul of Darkeater Midir",
|
|
||||||
"RC: Spears of the Church - hidden boss drop",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Slave Knight Gael 1", 5110801, dlc = True, locations = {
|
|
||||||
"RC: Soul of Slave Knight Gael",
|
|
||||||
"RC: Blood of the Dark Soul - end boss drop",
|
|
||||||
# These are accessible before you trigger the boss, but once you do you
|
|
||||||
# have to beat it before getting them.
|
|
||||||
"RC: Titanite Slab - ashes, mob drop",
|
|
||||||
"RC: Titanite Slab - ashes, NPC drop",
|
|
||||||
"RC: Sacred Chime of Filianore - ashes, NPC drop",
|
|
||||||
"RC: Crucifix of the Mad King - ashes, NPC drop",
|
|
||||||
"RC: Shira's Crown - Shira's room after killing ashes NPC",
|
|
||||||
"RC: Shira's Armor - Shira's room after killing ashes NPC",
|
|
||||||
"RC: Shira's Gloves - Shira's room after killing ashes NPC",
|
|
||||||
"RC: Shira's Trousers - Shira's room after killing ashes NPC",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Slave Knight Gael 2", 5110800, dlc = True, locations = {
|
|
||||||
"RC: Soul of Slave Knight Gael",
|
|
||||||
"RC: Blood of the Dark Soul - end boss drop",
|
|
||||||
# These are accessible before you trigger the boss, but once you do you
|
|
||||||
# have to beat it before getting them.
|
|
||||||
"RC: Titanite Slab - ashes, mob drop",
|
|
||||||
"RC: Titanite Slab - ashes, NPC drop",
|
|
||||||
"RC: Sacred Chime of Filianore - ashes, NPC drop",
|
|
||||||
"RC: Crucifix of the Mad King - ashes, NPC drop",
|
|
||||||
"RC: Shira's Crown - Shira's room after killing ashes NPC",
|
|
||||||
"RC: Shira's Armor - Shira's room after killing ashes NPC",
|
|
||||||
"RC: Shira's Gloves - Shira's room after killing ashes NPC",
|
|
||||||
"RC: Shira's Trousers - Shira's room after killing ashes NPC",
|
|
||||||
}),
|
|
||||||
DS3BossInfo("Lords of Cinder", 4100800, locations = {
|
|
||||||
"KFF: Soul of the Lords",
|
|
||||||
"FS: Billed Mask - Yuria after killing KFF boss",
|
|
||||||
"FS: Black Dress - Yuria after killing KFF boss",
|
|
||||||
"FS: Black Gauntlets - Yuria after killing KFF boss",
|
|
||||||
"FS: Black Leggings - Yuria after killing KFF boss"
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
|
|
||||||
default_yhorm_location = DS3BossInfo("Yhorm the Giant", 3900800, locations = {
|
|
||||||
"PC: Soul of Yhorm the Giant",
|
|
||||||
"PC: Cinders of a Lord - Yhorm the Giant",
|
|
||||||
"PC: Siegbräu - Siegward after killing boss",
|
|
||||||
})
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,78 +1,80 @@
|
|||||||
from dataclasses import dataclass
|
import typing
|
||||||
import json
|
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
from Options import Choice, DeathLink, DefaultOnToggle, ExcludeLocations, NamedRange, OptionDict, \
|
from Options import Toggle, DefaultOnToggle, Option, Range, Choice, ItemDict, DeathLink
|
||||||
OptionGroup, PerGameCommonOptions, Range, Removed, Toggle
|
|
||||||
|
|
||||||
## Game Options
|
|
||||||
|
|
||||||
|
|
||||||
class EarlySmallLothricBanner(Choice):
|
class RandomizeWeaponLocations(DefaultOnToggle):
|
||||||
"""Force Small Lothric Banner into an early sphere in your world or across all worlds."""
|
"""Randomizes weapons (+76 locations)"""
|
||||||
display_name = "Early Small Lothric Banner"
|
display_name = "Randomize Weapon Locations"
|
||||||
option_off = 0
|
|
||||||
option_early_global = 1
|
|
||||||
option_early_local = 2
|
|
||||||
default = option_off
|
|
||||||
|
|
||||||
|
|
||||||
class LateBasinOfVowsOption(Choice):
|
class RandomizeShieldLocations(DefaultOnToggle):
|
||||||
"""Guarantee that you don't need to enter Lothric Castle until later in the run.
|
"""Randomizes shields (+24 locations)"""
|
||||||
|
display_name = "Randomize Shield Locations"
|
||||||
- **Off:** You may have to enter Lothric Castle and the areas beyond it immediately after High
|
|
||||||
Wall of Lothric.
|
|
||||||
- **After Small Lothric Banner:** You may have to enter Lothric Castle after Catacombs of
|
|
||||||
Carthus.
|
|
||||||
- **After Small Doll:** You won't have to enter Lothric Castle until after Irithyll of the
|
|
||||||
Boreal Valley.
|
|
||||||
"""
|
|
||||||
display_name = "Late Basin of Vows"
|
|
||||||
option_off = 0
|
|
||||||
alias_false = 0
|
|
||||||
option_after_small_lothric_banner = 1
|
|
||||||
alias_true = 1
|
|
||||||
option_after_small_doll = 2
|
|
||||||
|
|
||||||
|
|
||||||
class LateDLCOption(Choice):
|
class RandomizeArmorLocations(DefaultOnToggle):
|
||||||
"""Guarantee that you don't need to enter the DLC until later in the run.
|
"""Randomizes armor pieces (+97 locations)"""
|
||||||
|
display_name = "Randomize Armor Locations"
|
||||||
- **Off:** You may have to enter the DLC after Catacombs of Carthus.
|
|
||||||
- **After Small Doll:** You may have to enter the DLC after Irithyll of the Boreal Valley.
|
|
||||||
- **After Basin:** You won't have to enter the DLC until after Lothric Castle.
|
|
||||||
"""
|
|
||||||
display_name = "Late DLC"
|
|
||||||
option_off = 0
|
|
||||||
alias_false = 0
|
|
||||||
option_after_small_doll = 1
|
|
||||||
alias_true = 1
|
|
||||||
option_after_basin = 2
|
|
||||||
|
|
||||||
|
|
||||||
class EnableDLCOption(Toggle):
|
class RandomizeRingLocations(DefaultOnToggle):
|
||||||
"""Include DLC locations, items, and enemies in the randomized pools.
|
"""Randomizes rings (+49 locations)"""
|
||||||
|
display_name = "Randomize Ring Locations"
|
||||||
To use this option, you must own both the "Ashes of Ariandel" and the "Ringed City" DLCs.
|
|
||||||
"""
|
|
||||||
display_name = "Enable DLC"
|
|
||||||
|
|
||||||
|
|
||||||
class EnableNGPOption(Toggle):
|
class RandomizeSpellLocations(DefaultOnToggle):
|
||||||
"""Include items and locations exclusive to NG+ cycles."""
|
"""Randomizes spells (+18 locations)"""
|
||||||
display_name = "Enable NG+"
|
display_name = "Randomize Spell Locations"
|
||||||
|
|
||||||
|
|
||||||
## Equipment
|
class RandomizeKeyLocations(DefaultOnToggle):
|
||||||
|
"""Randomizes items which unlock doors or bypass barriers"""
|
||||||
class RandomizeStartingLoadout(DefaultOnToggle):
|
display_name = "Randomize Key Locations"
|
||||||
"""Randomizes the equipment characters begin with."""
|
|
||||||
display_name = "Randomize Starting Loadout"
|
|
||||||
|
|
||||||
|
|
||||||
class RequireOneHandedStartingWeapons(DefaultOnToggle):
|
class RandomizeBossSoulLocations(DefaultOnToggle):
|
||||||
"""Require starting equipment to be usable one-handed."""
|
"""Randomizes Boss Souls (+18 Locations)"""
|
||||||
display_name = "Require One-Handed Starting Weapons"
|
display_name = "Randomize Boss Soul Locations"
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeNPCLocations(Toggle):
|
||||||
|
"""Randomizes friendly NPC drops (meaning you will probably have to kill them) (+14 locations)"""
|
||||||
|
display_name = "Randomize NPC Locations"
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeMiscLocations(Toggle):
|
||||||
|
"""Randomizes miscellaneous items (ashes, tomes, scrolls, etc.) to the pool. (+36 locations)"""
|
||||||
|
display_name = "Randomize Miscellaneous Locations"
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeHealthLocations(Toggle):
|
||||||
|
"""Randomizes health upgrade items. (+21 locations)"""
|
||||||
|
display_name = "Randomize Health Upgrade Locations"
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeProgressiveLocationsOption(Toggle):
|
||||||
|
"""Randomizes upgrade materials and consumables such as the titanite shards, firebombs, resin, etc...
|
||||||
|
|
||||||
|
Instead of specific locations, these are progressive, so Titanite Shard #1 is the first titanite shard
|
||||||
|
you pick up, regardless of whether it's from an enemy drop late in the game or an item on the ground in the
|
||||||
|
first 5 minutes."""
|
||||||
|
display_name = "Randomize Progressive Locations"
|
||||||
|
|
||||||
|
|
||||||
|
class PoolTypeOption(Choice):
|
||||||
|
"""Changes which non-progression items you add to the pool
|
||||||
|
|
||||||
|
Shuffle: Items are picked from the locations being randomized
|
||||||
|
Various: Items are picked from a list of all items in the game, but are the same type of item they replace"""
|
||||||
|
display_name = "Pool Type"
|
||||||
|
option_shuffle = 0
|
||||||
|
option_various = 1
|
||||||
|
|
||||||
|
|
||||||
|
class GuaranteedItemsOption(ItemDict):
|
||||||
|
"""Guarantees that the specified items will be in the item pool"""
|
||||||
|
display_name = "Guaranteed Items"
|
||||||
|
|
||||||
|
|
||||||
class AutoEquipOption(Toggle):
|
class AutoEquipOption(Toggle):
|
||||||
@@ -81,56 +83,47 @@ class AutoEquipOption(Toggle):
|
|||||||
|
|
||||||
|
|
||||||
class LockEquipOption(Toggle):
|
class LockEquipOption(Toggle):
|
||||||
"""Lock the equipment slots so you cannot change your armor or your left/right weapons.
|
"""Lock the equipment slots so you cannot change your armor or your left/right weapons. Works great with the
|
||||||
|
Auto-equip option."""
|
||||||
Works great with the Auto-equip option.
|
|
||||||
"""
|
|
||||||
display_name = "Lock Equipment Slots"
|
display_name = "Lock Equipment Slots"
|
||||||
|
|
||||||
|
|
||||||
class NoEquipLoadOption(Toggle):
|
|
||||||
"""Disable the equip load constraint from the game."""
|
|
||||||
display_name = "No Equip Load"
|
|
||||||
|
|
||||||
|
|
||||||
class NoWeaponRequirementsOption(Toggle):
|
class NoWeaponRequirementsOption(Toggle):
|
||||||
"""Disable the weapon requirements by removing any movement or damage penalties, permitting you
|
"""Disable the weapon requirements by removing any movement or damage penalties.
|
||||||
to use any weapon early.
|
Permitting you to use any weapon early"""
|
||||||
"""
|
|
||||||
display_name = "No Weapon Requirements"
|
display_name = "No Weapon Requirements"
|
||||||
|
|
||||||
|
|
||||||
class NoSpellRequirementsOption(Toggle):
|
class NoSpellRequirementsOption(Toggle):
|
||||||
"""Disable the spell requirements permitting you to use any spell."""
|
"""Disable the spell requirements permitting you to use any spell"""
|
||||||
display_name = "No Spell Requirements"
|
display_name = "No Spell Requirements"
|
||||||
|
|
||||||
|
|
||||||
## Weapons
|
class NoEquipLoadOption(Toggle):
|
||||||
|
"""Disable the equip load constraint from the game"""
|
||||||
|
display_name = "No Equip Load"
|
||||||
|
|
||||||
|
|
||||||
class RandomizeInfusionOption(Toggle):
|
class RandomizeInfusionOption(Toggle):
|
||||||
"""Enable this option to infuse a percentage of the pool of weapons and shields."""
|
"""Enable this option to infuse a percentage of the pool of weapons and shields."""
|
||||||
display_name = "Randomize Infusion"
|
display_name = "Randomize Infusion"
|
||||||
|
|
||||||
|
|
||||||
class RandomizeInfusionPercentageOption(NamedRange):
|
class RandomizeInfusionPercentageOption(Range):
|
||||||
"""The percentage of weapons/shields in the pool to be infused if Randomize Infusion is toggled.
|
"""The percentage of weapons/shields in the pool to be infused if Randomize Infusion is toggled"""
|
||||||
"""
|
|
||||||
display_name = "Percentage of Infused Weapons"
|
display_name = "Percentage of Infused Weapons"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 100
|
range_end = 100
|
||||||
default = 33
|
default = 33
|
||||||
# 3/155 weapons are infused in the base game, or about 2%
|
|
||||||
special_range_names = {"similar to base game": 2}
|
|
||||||
|
|
||||||
|
|
||||||
class RandomizeWeaponLevelOption(Choice):
|
class RandomizeWeaponLevelOption(Choice):
|
||||||
"""Enable this option to upgrade a percentage of the pool of weapons to a random value between
|
"""Enable this option to upgrade a percentage of the pool of weapons to a random value between the minimum and
|
||||||
the minimum and maximum levels defined.
|
maximum levels defined.
|
||||||
|
|
||||||
- **All:** All weapons are eligible, both basic and epic
|
All: All weapons are eligible, both basic and epic
|
||||||
- **Basic:** Only weapons that can be upgraded to +10
|
Basic: Only weapons that can be upgraded to +10
|
||||||
- **Epic:** Only weapons that can be upgraded to +5
|
Epic: Only weapons that can be upgraded to +5"""
|
||||||
"""
|
|
||||||
display_name = "Randomize Weapon Level"
|
display_name = "Randomize Weapon Level"
|
||||||
option_none = 0
|
option_none = 0
|
||||||
option_all = 1
|
option_all = 1
|
||||||
@@ -139,7 +132,7 @@ class RandomizeWeaponLevelOption(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class RandomizeWeaponLevelPercentageOption(Range):
|
class RandomizeWeaponLevelPercentageOption(Range):
|
||||||
"""The percentage of weapons in the pool to be upgraded if randomize weapons level is toggled."""
|
"""The percentage of weapons in the pool to be upgraded if randomize weapons level is toggled"""
|
||||||
display_name = "Percentage of Randomized Weapons"
|
display_name = "Percentage of Randomized Weapons"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 100
|
range_end = 100
|
||||||
@@ -147,7 +140,7 @@ class RandomizeWeaponLevelPercentageOption(Range):
|
|||||||
|
|
||||||
|
|
||||||
class MinLevelsIn5WeaponPoolOption(Range):
|
class MinLevelsIn5WeaponPoolOption(Range):
|
||||||
"""The minimum upgraded value of a weapon in the pool of weapons that can only reach +5."""
|
"""The minimum upgraded value of a weapon in the pool of weapons that can only reach +5"""
|
||||||
display_name = "Minimum Level of +5 Weapons"
|
display_name = "Minimum Level of +5 Weapons"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 5
|
range_end = 5
|
||||||
@@ -155,7 +148,7 @@ class MinLevelsIn5WeaponPoolOption(Range):
|
|||||||
|
|
||||||
|
|
||||||
class MaxLevelsIn5WeaponPoolOption(Range):
|
class MaxLevelsIn5WeaponPoolOption(Range):
|
||||||
"""The maximum upgraded value of a weapon in the pool of weapons that can only reach +5."""
|
"""The maximum upgraded value of a weapon in the pool of weapons that can only reach +5"""
|
||||||
display_name = "Maximum Level of +5 Weapons"
|
display_name = "Maximum Level of +5 Weapons"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 5
|
range_end = 5
|
||||||
@@ -163,7 +156,7 @@ class MaxLevelsIn5WeaponPoolOption(Range):
|
|||||||
|
|
||||||
|
|
||||||
class MinLevelsIn10WeaponPoolOption(Range):
|
class MinLevelsIn10WeaponPoolOption(Range):
|
||||||
"""The minimum upgraded value of a weapon in the pool of weapons that can reach +10."""
|
"""The minimum upgraded value of a weapon in the pool of weapons that can reach +10"""
|
||||||
display_name = "Minimum Level of +10 Weapons"
|
display_name = "Minimum Level of +10 Weapons"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 10
|
range_end = 10
|
||||||
@@ -171,308 +164,72 @@ class MinLevelsIn10WeaponPoolOption(Range):
|
|||||||
|
|
||||||
|
|
||||||
class MaxLevelsIn10WeaponPoolOption(Range):
|
class MaxLevelsIn10WeaponPoolOption(Range):
|
||||||
"""The maximum upgraded value of a weapon in the pool of weapons that can reach +10."""
|
"""The maximum upgraded value of a weapon in the pool of weapons that can reach +10"""
|
||||||
display_name = "Maximum Level of +10 Weapons"
|
display_name = "Maximum Level of +10 Weapons"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 10
|
range_end = 10
|
||||||
default = 10
|
default = 10
|
||||||
|
|
||||||
|
|
||||||
## Item Smoothing
|
class EarlySmallLothricBanner(Choice):
|
||||||
|
"""This option makes it so the user can choose to force the Small Lothric Banner into an early sphere in their world or
|
||||||
class SmoothSoulItemsOption(DefaultOnToggle):
|
into an early sphere across all worlds."""
|
||||||
"""Distribute soul items in a similar order as the base game.
|
display_name = "Early Small Lothric Banner"
|
||||||
|
option_off = 0
|
||||||
By default, soul items will be distributed totally randomly. If this is set, less valuable soul
|
option_early_global = 1
|
||||||
items will generally appear in earlier spheres and more valuable ones will generally appear
|
option_early_local = 2
|
||||||
later.
|
default = option_off
|
||||||
"""
|
|
||||||
display_name = "Smooth Soul Items"
|
|
||||||
|
class LateBasinOfVowsOption(Toggle):
|
||||||
|
"""This option makes it so the Basin of Vows is still randomized, but guarantees you that you wont have to venture into
|
||||||
class SmoothUpgradeItemsOption(DefaultOnToggle):
|
Lothric Castle to find your Small Lothric Banner to get out of High Wall of Lothric. So you may find Basin of Vows early,
|
||||||
"""Distribute upgrade items in a similar order as the base game.
|
but you wont have to fight Dancer to find your Small Lothric Banner."""
|
||||||
|
display_name = "Late Basin of Vows"
|
||||||
By default, upgrade items will be distributed totally randomly. If this is set, lower-level
|
|
||||||
upgrade items will generally appear in earlier spheres and higher-level ones will generally
|
|
||||||
appear later.
|
class LateDLCOption(Toggle):
|
||||||
"""
|
"""This option makes it so you are guaranteed to find your Small Doll without having to venture off into the DLC,
|
||||||
display_name = "Smooth Upgrade Items"
|
effectively putting anything in the DLC in logic after finding both Contraption Key and Small Doll,
|
||||||
|
and being able to get into Irithyll of the Boreal Valley."""
|
||||||
|
display_name = "Late DLC"
|
||||||
class SmoothUpgradedWeaponsOption(DefaultOnToggle):
|
|
||||||
"""Distribute upgraded weapons in a similar order as the base game.
|
|
||||||
|
class EnableDLCOption(Toggle):
|
||||||
By default, upgraded weapons will be distributed totally randomly. If this is set, lower-level
|
"""To use this option, you must own both the ASHES OF ARIANDEL and the RINGED CITY DLC"""
|
||||||
weapons will generally appear in earlier spheres and higher-level ones will generally appear
|
display_name = "Enable DLC"
|
||||||
later.
|
|
||||||
"""
|
|
||||||
display_name = "Smooth Upgraded Weapons"
|
dark_souls_options: typing.Dict[str, Option] = {
|
||||||
|
"enable_weapon_locations": RandomizeWeaponLocations,
|
||||||
|
"enable_shield_locations": RandomizeShieldLocations,
|
||||||
### Enemies
|
"enable_armor_locations": RandomizeArmorLocations,
|
||||||
|
"enable_ring_locations": RandomizeRingLocations,
|
||||||
class RandomizeEnemiesOption(DefaultOnToggle):
|
"enable_spell_locations": RandomizeSpellLocations,
|
||||||
"""Randomize enemy and boss placements."""
|
"enable_key_locations": RandomizeKeyLocations,
|
||||||
display_name = "Randomize Enemies"
|
"enable_boss_locations": RandomizeBossSoulLocations,
|
||||||
|
"enable_npc_locations": RandomizeNPCLocations,
|
||||||
|
"enable_misc_locations": RandomizeMiscLocations,
|
||||||
class SimpleEarlyBossesOption(DefaultOnToggle):
|
"enable_health_upgrade_locations": RandomizeHealthLocations,
|
||||||
"""Avoid replacing Iudex Gundyr and Vordt with late bosses.
|
"enable_progressive_locations": RandomizeProgressiveLocationsOption,
|
||||||
|
"pool_type": PoolTypeOption,
|
||||||
This excludes all bosses after Dancer of the Boreal Valley from these two boss fights. Disable
|
"guaranteed_items": GuaranteedItemsOption,
|
||||||
it for a chance at a much harder early game.
|
"auto_equip": AutoEquipOption,
|
||||||
|
"lock_equip": LockEquipOption,
|
||||||
This is ignored unless enemies are randomized.
|
"no_weapon_requirements": NoWeaponRequirementsOption,
|
||||||
"""
|
"randomize_infusion": RandomizeInfusionOption,
|
||||||
display_name = "Simple Early Bosses"
|
"randomize_infusion_percentage": RandomizeInfusionPercentageOption,
|
||||||
|
"randomize_weapon_level": RandomizeWeaponLevelOption,
|
||||||
|
"randomize_weapon_level_percentage": RandomizeWeaponLevelPercentageOption,
|
||||||
class ScaleEnemiesOption(DefaultOnToggle):
|
"min_levels_in_5": MinLevelsIn5WeaponPoolOption,
|
||||||
"""Scale randomized enemy stats to match the areas in which they appear.
|
"max_levels_in_5": MaxLevelsIn5WeaponPoolOption,
|
||||||
|
"min_levels_in_10": MinLevelsIn10WeaponPoolOption,
|
||||||
Disabling this will tend to make the early game much more difficult and the late game much
|
"max_levels_in_10": MaxLevelsIn10WeaponPoolOption,
|
||||||
easier.
|
"early_banner": EarlySmallLothricBanner,
|
||||||
|
"late_basin_of_vows": LateBasinOfVowsOption,
|
||||||
This is ignored unless enemies are randomized.
|
"late_dlc": LateDLCOption,
|
||||||
"""
|
"no_spell_requirements": NoSpellRequirementsOption,
|
||||||
display_name = "Scale Enemies"
|
"no_equip_load": NoEquipLoadOption,
|
||||||
|
"death_link": DeathLink,
|
||||||
|
"enable_dlc": EnableDLCOption,
|
||||||
class RandomizeMimicsWithEnemiesOption(Toggle):
|
}
|
||||||
"""Mix Mimics into the main enemy pool.
|
|
||||||
|
|
||||||
If this is enabled, Mimics will be replaced by normal enemies who drop the Mimic rewards on
|
|
||||||
death, and Mimics will be placed randomly in place of normal enemies. It's recommended to enable
|
|
||||||
Impatient Mimics as well if you enable this.
|
|
||||||
|
|
||||||
This is ignored unless enemies are randomized.
|
|
||||||
"""
|
|
||||||
display_name = "Randomize Mimics With Enemies"
|
|
||||||
|
|
||||||
|
|
||||||
class RandomizeSmallCrystalLizardsWithEnemiesOption(Toggle):
|
|
||||||
"""Mix small Crystal Lizards into the main enemy pool.
|
|
||||||
|
|
||||||
If this is enabled, Crystal Lizards will be replaced by normal enemies who drop the Crystal
|
|
||||||
Lizard rewards on death, and Crystal Lizards will be placed randomly in place of normal enemies.
|
|
||||||
|
|
||||||
This is ignored unless enemies are randomized.
|
|
||||||
"""
|
|
||||||
display_name = "Randomize Small Crystal Lizards With Enemies"
|
|
||||||
|
|
||||||
|
|
||||||
class ReduceHarmlessEnemiesOption(Toggle):
|
|
||||||
"""Reduce the frequency that "harmless" enemies appear.
|
|
||||||
|
|
||||||
Enable this to add a bit of extra challenge. This severely limits the number of enemies that are
|
|
||||||
slow to aggro, slow to attack, and do very little damage that appear in the enemy pool.
|
|
||||||
|
|
||||||
This is ignored unless enemies are randomized.
|
|
||||||
"""
|
|
||||||
display_name = "Reduce Harmless Enemies"
|
|
||||||
|
|
||||||
|
|
||||||
class AllChestsAreMimicsOption(Toggle):
|
|
||||||
"""Replace all chests with mimics that drop the same items.
|
|
||||||
|
|
||||||
If "Randomize Mimics With Enemies" is set, these chests will instead be replaced with random
|
|
||||||
enemies that drop the same items.
|
|
||||||
|
|
||||||
This is ignored unless enemies are randomized.
|
|
||||||
"""
|
|
||||||
display_name = "All Chests Are Mimics"
|
|
||||||
|
|
||||||
|
|
||||||
class ImpatientMimicsOption(Toggle):
|
|
||||||
"""Mimics attack as soon as you get close instead of waiting for you to open them.
|
|
||||||
|
|
||||||
This is ignored unless enemies are randomized.
|
|
||||||
"""
|
|
||||||
display_name = "Impatient Mimics"
|
|
||||||
|
|
||||||
|
|
||||||
class RandomEnemyPresetOption(OptionDict):
|
|
||||||
"""The YAML preset for the static enemy randomizer.
|
|
||||||
|
|
||||||
See the static randomizer documentation in `randomizer\\presets\\README.txt` for details.
|
|
||||||
Include this as nested YAML. For example:
|
|
||||||
|
|
||||||
.. code-block:: YAML
|
|
||||||
|
|
||||||
random_enemy_preset:
|
|
||||||
RemoveSource: Ancient Wyvern; Darkeater Midir
|
|
||||||
DontRandomize: Iudex Gundyr
|
|
||||||
"""
|
|
||||||
display_name = "Random Enemy Preset"
|
|
||||||
supports_weighting = False
|
|
||||||
default = {}
|
|
||||||
|
|
||||||
valid_keys = ["Description", "RecommendFullRandomization", "RecommendNoEnemyProgression",
|
|
||||||
"OopsAll", "Boss", "Miniboss", "Basic", "BuffBasicEnemiesAsBosses",
|
|
||||||
"DontRandomize", "RemoveSource", "Enemies"]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_option_name(cls, value: Dict[str, Any]) -> str:
|
|
||||||
return json.dumps(value)
|
|
||||||
|
|
||||||
|
|
||||||
## Item & Location
|
|
||||||
|
|
||||||
class DS3ExcludeLocations(ExcludeLocations):
|
|
||||||
"""Prevent these locations from having an important item."""
|
|
||||||
default = frozenset({"Hidden", "Small Crystal Lizards", "Upgrade", "Small Souls", "Miscellaneous"})
|
|
||||||
|
|
||||||
|
|
||||||
class ExcludedLocationBehaviorOption(Choice):
|
|
||||||
"""How to choose items for excluded locations in DS3.
|
|
||||||
|
|
||||||
- **Allow Useful:** Excluded locations can't have progression items, but they can have useful
|
|
||||||
items.
|
|
||||||
- **Forbid Useful:** Neither progression items nor useful items can be placed in excluded
|
|
||||||
locations.
|
|
||||||
- **Do Not Randomize:** Excluded locations always contain the same item as in vanilla Dark Souls
|
|
||||||
III.
|
|
||||||
|
|
||||||
A "progression item" is anything that's required to unlock another location in some game. A
|
|
||||||
"useful item" is something each game defines individually, usually items that are quite
|
|
||||||
desirable but not strictly necessary.
|
|
||||||
"""
|
|
||||||
display_name = "Excluded Locations Behavior"
|
|
||||||
option_allow_useful = 1
|
|
||||||
option_forbid_useful = 2
|
|
||||||
option_do_not_randomize = 3
|
|
||||||
default = 2
|
|
||||||
|
|
||||||
|
|
||||||
class MissableLocationBehaviorOption(Choice):
|
|
||||||
"""Which items can be placed in locations that can be permanently missed.
|
|
||||||
|
|
||||||
- **Allow Useful:** Missable locations can't have progression items, but they can have useful
|
|
||||||
items.
|
|
||||||
- **Forbid Useful:** Neither progression items nor useful items can be placed in missable
|
|
||||||
locations.
|
|
||||||
- **Do Not Randomize:** Missable locations always contain the same item as in vanilla Dark Souls
|
|
||||||
III.
|
|
||||||
|
|
||||||
A "progression item" is anything that's required to unlock another location in some game. A
|
|
||||||
"useful item" is something each game defines individually, usually items that are quite
|
|
||||||
desirable but not strictly necessary.
|
|
||||||
"""
|
|
||||||
display_name = "Missable Locations Behavior"
|
|
||||||
option_allow_useful = 1
|
|
||||||
option_forbid_useful = 2
|
|
||||||
option_do_not_randomize = 3
|
|
||||||
default = 2
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DarkSouls3Options(PerGameCommonOptions):
|
|
||||||
# Game Options
|
|
||||||
early_banner: EarlySmallLothricBanner
|
|
||||||
late_basin_of_vows: LateBasinOfVowsOption
|
|
||||||
late_dlc: LateDLCOption
|
|
||||||
death_link: DeathLink
|
|
||||||
enable_dlc: EnableDLCOption
|
|
||||||
enable_ngp: EnableNGPOption
|
|
||||||
|
|
||||||
# Equipment
|
|
||||||
random_starting_loadout: RandomizeStartingLoadout
|
|
||||||
require_one_handed_starting_weapons: RequireOneHandedStartingWeapons
|
|
||||||
auto_equip: AutoEquipOption
|
|
||||||
lock_equip: LockEquipOption
|
|
||||||
no_equip_load: NoEquipLoadOption
|
|
||||||
no_weapon_requirements: NoWeaponRequirementsOption
|
|
||||||
no_spell_requirements: NoSpellRequirementsOption
|
|
||||||
|
|
||||||
# Weapons
|
|
||||||
randomize_infusion: RandomizeInfusionOption
|
|
||||||
randomize_infusion_percentage: RandomizeInfusionPercentageOption
|
|
||||||
randomize_weapon_level: RandomizeWeaponLevelOption
|
|
||||||
randomize_weapon_level_percentage: RandomizeWeaponLevelPercentageOption
|
|
||||||
min_levels_in_5: MinLevelsIn5WeaponPoolOption
|
|
||||||
max_levels_in_5: MaxLevelsIn5WeaponPoolOption
|
|
||||||
min_levels_in_10: MinLevelsIn10WeaponPoolOption
|
|
||||||
max_levels_in_10: MaxLevelsIn10WeaponPoolOption
|
|
||||||
|
|
||||||
# Item Smoothing
|
|
||||||
smooth_soul_items: SmoothSoulItemsOption
|
|
||||||
smooth_upgrade_items: SmoothUpgradeItemsOption
|
|
||||||
smooth_upgraded_weapons: SmoothUpgradedWeaponsOption
|
|
||||||
|
|
||||||
# Enemies
|
|
||||||
randomize_enemies: RandomizeEnemiesOption
|
|
||||||
simple_early_bosses: SimpleEarlyBossesOption
|
|
||||||
scale_enemies: ScaleEnemiesOption
|
|
||||||
randomize_mimics_with_enemies: RandomizeMimicsWithEnemiesOption
|
|
||||||
randomize_small_crystal_lizards_with_enemies: RandomizeSmallCrystalLizardsWithEnemiesOption
|
|
||||||
reduce_harmless_enemies: ReduceHarmlessEnemiesOption
|
|
||||||
all_chests_are_mimics: AllChestsAreMimicsOption
|
|
||||||
impatient_mimics: ImpatientMimicsOption
|
|
||||||
random_enemy_preset: RandomEnemyPresetOption
|
|
||||||
|
|
||||||
# Item & Location
|
|
||||||
exclude_locations: DS3ExcludeLocations
|
|
||||||
excluded_location_behavior: ExcludedLocationBehaviorOption
|
|
||||||
missable_location_behavior: MissableLocationBehaviorOption
|
|
||||||
|
|
||||||
# Removed
|
|
||||||
pool_type: Removed
|
|
||||||
enable_weapon_locations: Removed
|
|
||||||
enable_shield_locations: Removed
|
|
||||||
enable_armor_locations: Removed
|
|
||||||
enable_ring_locations: Removed
|
|
||||||
enable_spell_locations: Removed
|
|
||||||
enable_key_locations: Removed
|
|
||||||
enable_boss_locations: Removed
|
|
||||||
enable_npc_locations: Removed
|
|
||||||
enable_misc_locations: Removed
|
|
||||||
enable_health_upgrade_locations: Removed
|
|
||||||
enable_progressive_locations: Removed
|
|
||||||
guaranteed_items: Removed
|
|
||||||
excluded_locations: Removed
|
|
||||||
missable_locations: Removed
|
|
||||||
|
|
||||||
|
|
||||||
option_groups = [
|
|
||||||
OptionGroup("Equipment", [
|
|
||||||
RandomizeStartingLoadout,
|
|
||||||
RequireOneHandedStartingWeapons,
|
|
||||||
AutoEquipOption,
|
|
||||||
LockEquipOption,
|
|
||||||
NoEquipLoadOption,
|
|
||||||
NoWeaponRequirementsOption,
|
|
||||||
NoSpellRequirementsOption,
|
|
||||||
]),
|
|
||||||
OptionGroup("Weapons", [
|
|
||||||
RandomizeInfusionOption,
|
|
||||||
RandomizeInfusionPercentageOption,
|
|
||||||
RandomizeWeaponLevelOption,
|
|
||||||
RandomizeWeaponLevelPercentageOption,
|
|
||||||
MinLevelsIn5WeaponPoolOption,
|
|
||||||
MaxLevelsIn5WeaponPoolOption,
|
|
||||||
MinLevelsIn10WeaponPoolOption,
|
|
||||||
MaxLevelsIn10WeaponPoolOption,
|
|
||||||
]),
|
|
||||||
OptionGroup("Item Smoothing", [
|
|
||||||
SmoothSoulItemsOption,
|
|
||||||
SmoothUpgradeItemsOption,
|
|
||||||
SmoothUpgradedWeaponsOption,
|
|
||||||
]),
|
|
||||||
OptionGroup("Enemies", [
|
|
||||||
RandomizeEnemiesOption,
|
|
||||||
SimpleEarlyBossesOption,
|
|
||||||
ScaleEnemiesOption,
|
|
||||||
RandomizeMimicsWithEnemiesOption,
|
|
||||||
RandomizeSmallCrystalLizardsWithEnemiesOption,
|
|
||||||
ReduceHarmlessEnemiesOption,
|
|
||||||
AllChestsAreMimicsOption,
|
|
||||||
ImpatientMimicsOption,
|
|
||||||
RandomEnemyPresetOption,
|
|
||||||
]),
|
|
||||||
OptionGroup("Item & Location Options", [
|
|
||||||
DS3ExcludeLocations,
|
|
||||||
ExcludedLocationBehaviorOption,
|
|
||||||
MissableLocationBehaviorOption,
|
|
||||||
])
|
|
||||||
]
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user