mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-18 21:38:13 -07:00
Compare commits
231 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3678d78cda | ||
|
|
8e3877f72f | ||
|
|
b86e24c2df | ||
|
|
ce862a67c7 | ||
|
|
7d36c15ca0 | ||
|
|
69245cb501 | ||
|
|
ca2306f54f | ||
|
|
785b405184 | ||
|
|
44a0c7634d | ||
|
|
793817957d | ||
|
|
7621889b8b | ||
|
|
c9f1a21bd2 | ||
|
|
874392756b | ||
|
|
7ff201e32c | ||
|
|
170aedba8f | ||
|
|
09c7f5f909 | ||
|
|
4aab317665 | ||
|
|
e52ce0149a | ||
|
|
5a5162c9d3 | ||
|
|
cf375cbcc4 | ||
|
|
6d6d35d598 | ||
|
|
05b257adf9 | ||
|
|
cabfef669a | ||
|
|
e4a5ed1cc4 | ||
|
|
5021997df0 | ||
|
|
d90cf0db65 | ||
|
|
dad228cd4a | ||
|
|
a652108472 | ||
|
|
5348f693fe | ||
|
|
b8c2e14e8b | ||
|
|
430b71a092 | ||
|
|
a40744e6db | ||
|
|
d802f9652a | ||
|
|
cbdb4d7ce3 | ||
|
|
691ce6a248 | ||
|
|
f9fc6944d3 | ||
|
|
e984583e5e | ||
|
|
7e03a87608 | ||
|
|
456bc481a3 | ||
|
|
b4752cd32d | ||
|
|
ceec51b9e1 | ||
|
|
d3312287a8 | ||
|
|
d65863ffa2 | ||
|
|
b8d7ef24f7 | ||
|
|
b2949dfbe8 | ||
|
|
2aa0653b6d | ||
|
|
d63efa5846 | ||
|
|
765721888a | ||
|
|
73701292b5 | ||
|
|
3ab71daa8d | ||
|
|
6f46397185 | ||
|
|
1a41e1acc8 | ||
|
|
34a3b5f058 | ||
|
|
456b4adaa1 | ||
|
|
fc8462f4e9 | ||
|
|
499dad53b1 | ||
|
|
8a809be67a | ||
|
|
7e0219c214 | ||
|
|
b37bb60891 | ||
|
|
f81335d614 | ||
|
|
8ed466bf24 | ||
|
|
920cffda2d | ||
|
|
b1be597451 | ||
|
|
08dc7e522e | ||
|
|
0f64bd08e1 | ||
|
|
d52827ebd2 | ||
|
|
0e55ddc7cf | ||
|
|
ab5b986716 | ||
|
|
97c313c1c4 | ||
|
|
701a7faa71 | ||
|
|
9a4e84efdc | ||
|
|
906b23088c | ||
|
|
0fb69dce33 | ||
|
|
e99f027b42 | ||
|
|
dddffa1660 | ||
|
|
83367c6946 | ||
|
|
0fcca25870 | ||
|
|
d1a7fd7da1 | ||
|
|
5c5f2ffc94 | ||
|
|
6f617e302d | ||
|
|
35c9061c9c | ||
|
|
e61d521ba8 | ||
|
|
6efa065867 | ||
|
|
56dbba6a31 | ||
|
|
43cb9611fb | ||
|
|
64b654d42e | ||
|
|
74aab81f79 | ||
|
|
f390b33c17 | ||
|
|
31852801c9 | ||
|
|
e35addf5b2 | ||
|
|
3cdcb8c455 | ||
|
|
48c6a6fb4c | ||
|
|
eaa8156061 | ||
|
|
54a7bb5664 | ||
|
|
0e6e359747 | ||
|
|
c4e7b6ca82 | ||
|
|
f253dffc07 | ||
|
|
c010c8c938 | ||
|
|
1e8a8e7482 | ||
|
|
182f7e24e5 | ||
|
|
9277cb39ef | ||
|
|
28a9709516 | ||
|
|
49a5b52774 | ||
|
|
2b1802ccee | ||
|
|
f5218faea7 | ||
|
|
81092247c6 | ||
|
|
ca96e7e294 | ||
|
|
c014c5a54a | ||
|
|
e9c863dffd | ||
|
|
7eda4c47f8 | ||
|
|
474a3181c6 | ||
|
|
4af6927e23 | ||
|
|
06df072095 | ||
|
|
56aabe51b8 | ||
|
|
5e5f24cdd2 | ||
|
|
9fbaa6050f | ||
|
|
0af31c71e0 | ||
|
|
169da1b1e0 | ||
|
|
8e7ea06f39 | ||
|
|
96d48a923a | ||
|
|
dcaa2f7b97 | ||
|
|
50330cf32f | ||
|
|
67520adcea | ||
|
|
a3e54a951f | ||
|
|
ae0abd3821 | ||
|
|
21bbf5fb95 | ||
|
|
09e052c750 | ||
|
|
68a92b0c6f | ||
|
|
8e06ab4f68 | ||
|
|
9dba39b606 | ||
|
|
a6f376b02e | ||
|
|
c66a8605da | ||
|
|
ac7590e621 | ||
|
|
30f97dd7de | ||
|
|
6e41c60672 | ||
|
|
5efb3fd2b0 | ||
|
|
6803c373e5 | ||
|
|
575c338aa3 | ||
|
|
05ce29f7dc | ||
|
|
74697b679e | ||
|
|
cf6661439e | ||
|
|
6297a4efa5 | ||
|
|
8ddb49f071 | ||
|
|
90446ad175 | ||
|
|
98bb8517e1 | ||
|
|
203c8f4d89 | ||
|
|
c0ef02d6fa | ||
|
|
4620493828 | ||
|
|
75b8c7891c | ||
|
|
53bc4ffa52 | ||
|
|
91f7cf16de | ||
|
|
7c8ea34a02 | ||
|
|
a05dbac55f | ||
|
|
83521e99d9 | ||
|
|
1d19da0c76 | ||
|
|
77e3f9fbef | ||
|
|
954d728005 | ||
|
|
80daa092a7 | ||
|
|
fac72dbc20 | ||
|
|
e764da3dc6 | ||
|
|
ab0903679c | ||
|
|
67f329b96f | ||
|
|
b273852512 | ||
|
|
b77805e5ee | ||
|
|
34141f8de0 | ||
|
|
e38f5d0a61 | ||
|
|
35ed0d4e19 | ||
|
|
e5c9b8ad0c | ||
|
|
6994f863e5 | ||
|
|
9d36ad0df2 | ||
|
|
cc22161644 | ||
|
|
d030a698a6 | ||
|
|
b6e5223aa2 | ||
|
|
79843803cf | ||
|
|
5fb1ebdcfd | ||
|
|
b019485944 | ||
|
|
205ca7fa37 | ||
|
|
8949e21565 | ||
|
|
deae524e9b | ||
|
|
496f0e09af | ||
|
|
f34da74012 | ||
|
|
94e6e978f3 | ||
|
|
697f749518 | ||
|
|
2307694012 | ||
|
|
b23c120258 | ||
|
|
ea1bb8d927 | ||
|
|
e714d2e129 | ||
|
|
878d5141ce | ||
|
|
1852287c91 | ||
|
|
8756f48e46 | ||
|
|
ff680b26cc | ||
|
|
29a0b013cb | ||
|
|
e7dbfa7fcd | ||
|
|
ad5089b5a3 | ||
|
|
dc50444edd | ||
|
|
ed4ad386e8 | ||
|
|
5188375736 | ||
|
|
9c2933f803 | ||
|
|
b840c3fe1a | ||
|
|
c12d3dd6ad | ||
|
|
f7989780fa | ||
|
|
e59bec36ec | ||
|
|
48a0fb05a2 | ||
|
|
12f1ef873c | ||
|
|
d7d4565429 | ||
|
|
7039b17bf6 | ||
|
|
34e7748f23 | ||
|
|
e33a9991ef | ||
|
|
4d1507cd0e | ||
|
|
7b39b23f73 | ||
|
|
925e02dca7 | ||
|
|
e76d32e908 | ||
|
|
08a36ec223 | ||
|
|
48dc14421e | ||
|
|
948f50f35d | ||
|
|
187f9dac94 | ||
|
|
eaec41d885 | ||
|
|
1e3a4b6db5 | ||
|
|
8c86139066 | ||
|
|
c96c554dfa | ||
|
|
9b22458f44 | ||
|
|
f99ee77325 | ||
|
|
bfac100567 | ||
|
|
e7a8e195e6 | ||
|
|
4054a9f15f | ||
|
|
ca76628813 | ||
|
|
d4d0a3e945 | ||
|
|
315e0c89e2 | ||
|
|
f6735745b6 | ||
|
|
50f7a79ea7 | ||
|
|
95110c4787 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
worlds/blasphemous/region_data.py linguist-generated=true
|
||||||
7
.github/workflows/unittests.yml
vendored
7
.github/workflows/unittests.yml
vendored
@@ -37,12 +37,13 @@ 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.11'} # current
|
- python: {version: '3.12'} # current
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
- python: {version: '3.11'} # current
|
- python: {version: '3.12'} # current
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -70,7 +71,7 @@ jobs:
|
|||||||
os:
|
os:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
python:
|
python:
|
||||||
- {version: '3.11'} # current
|
- {version: '3.12'} # current
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -150,7 +150,7 @@ venv/
|
|||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
.code-workspace
|
*.code-workspace
|
||||||
shell.nix
|
shell.nix
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
|
|||||||
332
BaseClasses.py
332
BaseClasses.py
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import collections
|
||||||
import itertools
|
import itertools
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
@@ -11,8 +11,10 @@ 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 Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \
|
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
|
||||||
TypedDict, Union, Type, ClassVar
|
Optional, Protocol, Set, Tuple, Union, Type)
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired, TypedDict
|
||||||
|
|
||||||
import NetUtils
|
import NetUtils
|
||||||
import Options
|
import Options
|
||||||
@@ -22,16 +24,16 @@ if typing.TYPE_CHECKING:
|
|||||||
from worlds import AutoWorld
|
from worlds import AutoWorld
|
||||||
|
|
||||||
|
|
||||||
class Group(TypedDict, total=False):
|
class Group(TypedDict):
|
||||||
name: str
|
name: str
|
||||||
game: str
|
game: str
|
||||||
world: "AutoWorld.World"
|
world: "AutoWorld.World"
|
||||||
players: Set[int]
|
players: AbstractSet[int]
|
||||||
item_pool: Set[str]
|
item_pool: NotRequired[Set[str]]
|
||||||
replacement_items: Dict[int, Optional[str]]
|
replacement_items: NotRequired[Dict[int, Optional[str]]]
|
||||||
local_items: Set[str]
|
local_items: NotRequired[Set[str]]
|
||||||
non_local_items: Set[str]
|
non_local_items: NotRequired[Set[str]]
|
||||||
link_replacement: bool
|
link_replacement: NotRequired[bool]
|
||||||
|
|
||||||
|
|
||||||
class ThreadBarrierProxy:
|
class ThreadBarrierProxy:
|
||||||
@@ -48,6 +50,11 @@ 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]
|
||||||
@@ -63,7 +70,6 @@ class MultiWorld():
|
|||||||
state: CollectionState
|
state: CollectionState
|
||||||
|
|
||||||
plando_options: PlandoOptions
|
plando_options: PlandoOptions
|
||||||
accessibility: Dict[int, Options.Accessibility]
|
|
||||||
early_items: Dict[int, Dict[str, int]]
|
early_items: Dict[int, Dict[str, int]]
|
||||||
local_early_items: Dict[int, Dict[str, int]]
|
local_early_items: Dict[int, Dict[str, int]]
|
||||||
local_items: Dict[int, Options.LocalItems]
|
local_items: Dict[int, Options.LocalItems]
|
||||||
@@ -157,7 +163,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, val):
|
def set_player_attr(attr: str, val) -> None:
|
||||||
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', {})
|
||||||
@@ -166,13 +172,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: Set[int] = frozenset()) -> Tuple[int, Group]:
|
def add_group(self, name: str, game: str, players: AbstractSet[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
|
||||||
@@ -196,7 +202,7 @@ class MultiWorld():
|
|||||||
|
|
||||||
return new_id, new_group
|
return new_id, new_group
|
||||||
|
|
||||||
def get_player_groups(self, player) -> Set[int]:
|
def get_player_groups(self, player: int) -> 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):
|
||||||
@@ -259,7 +265,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()
|
||||||
@@ -288,6 +294,86 @@ class MultiWorld():
|
|||||||
group["non_local_items"] = item_link["non_local_items"]
|
group["non_local_items"] = item_link["non_local_items"]
|
||||||
group["link_replacement"] = replacement_prio[item_link["link_replacement"]]
|
group["link_replacement"] = replacement_prio[item_link["link_replacement"]]
|
||||||
|
|
||||||
|
def link_items(self) -> None:
|
||||||
|
"""Called to link together items in the itempool related to the registered item link groups."""
|
||||||
|
from worlds import AutoWorld
|
||||||
|
|
||||||
|
for group_id, group in self.groups.items():
|
||||||
|
def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
|
||||||
|
Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]]
|
||||||
|
]:
|
||||||
|
classifications: Dict[str, int] = collections.defaultdict(int)
|
||||||
|
counters = {player: {name: 0 for name in shared_pool} for player in players}
|
||||||
|
for item in self.itempool:
|
||||||
|
if item.player in counters and item.name in shared_pool:
|
||||||
|
counters[item.player][item.name] += 1
|
||||||
|
classifications[item.name] |= item.classification
|
||||||
|
|
||||||
|
for player in players.copy():
|
||||||
|
if all([counters[player][item] == 0 for item in shared_pool]):
|
||||||
|
players.remove(player)
|
||||||
|
del (counters[player])
|
||||||
|
|
||||||
|
if not players:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
for item in shared_pool:
|
||||||
|
count = min(counters[player][item] for player in players)
|
||||||
|
if count:
|
||||||
|
for player in players:
|
||||||
|
counters[player][item] = count
|
||||||
|
else:
|
||||||
|
for player in players:
|
||||||
|
del (counters[player][item])
|
||||||
|
return counters, classifications
|
||||||
|
|
||||||
|
common_item_count, classifications = find_common_pool(group["players"], group["item_pool"])
|
||||||
|
if not common_item_count:
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_itempool: List[Item] = []
|
||||||
|
for item_name, item_count in next(iter(common_item_count.values())).items():
|
||||||
|
for _ in range(item_count):
|
||||||
|
new_item = group["world"].create_item(item_name)
|
||||||
|
# mangle together all original classification bits
|
||||||
|
new_item.classification |= classifications[item_name]
|
||||||
|
new_itempool.append(new_item)
|
||||||
|
|
||||||
|
region = Region("Menu", group_id, self, "ItemLink")
|
||||||
|
self.regions.append(region)
|
||||||
|
locations = region.locations
|
||||||
|
for item in self.itempool:
|
||||||
|
count = common_item_count.get(item.player, {}).get(item.name, 0)
|
||||||
|
if count:
|
||||||
|
loc = Location(group_id, f"Item Link: {item.name} -> {self.player_name[item.player]} {count}",
|
||||||
|
None, region)
|
||||||
|
loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \
|
||||||
|
state.has(item_name, group_id_, count_)
|
||||||
|
|
||||||
|
locations.append(loc)
|
||||||
|
loc.place_locked_item(item)
|
||||||
|
common_item_count[item.player][item.name] -= 1
|
||||||
|
else:
|
||||||
|
new_itempool.append(item)
|
||||||
|
|
||||||
|
itemcount = len(self.itempool)
|
||||||
|
self.itempool = new_itempool
|
||||||
|
|
||||||
|
while itemcount > len(self.itempool):
|
||||||
|
items_to_add = []
|
||||||
|
for player in group["players"]:
|
||||||
|
if group["link_replacement"]:
|
||||||
|
item_player = group_id
|
||||||
|
else:
|
||||||
|
item_player = player
|
||||||
|
if group["replacement_items"][player]:
|
||||||
|
items_to_add.append(AutoWorld.call_single(self, "create_item", item_player,
|
||||||
|
group["replacement_items"][player]))
|
||||||
|
else:
|
||||||
|
items_to_add.append(AutoWorld.call_single(self, "create_filler", item_player))
|
||||||
|
self.random.shuffle(items_to_add)
|
||||||
|
self.itempool.extend(items_to_add[:itemcount - len(self.itempool)])
|
||||||
|
|
||||||
def secure(self):
|
def secure(self):
|
||||||
self.random = ThreadBarrierProxy(secrets.SystemRandom())
|
self.random = ThreadBarrierProxy(secrets.SystemRandom())
|
||||||
self.is_race = True
|
self.is_race = True
|
||||||
@@ -309,7 +395,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) -> str:
|
def get_name_string_for_object(self, obj: HasNameAndPlayer) -> 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:
|
||||||
@@ -351,7 +437,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_events()
|
ret.sweep_for_advancements()
|
||||||
|
|
||||||
if use_cache:
|
if use_cache:
|
||||||
self._all_state = ret
|
self._all_state = ret
|
||||||
@@ -360,7 +446,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, player: int, resolve_group_locations: bool = False) -> List[Location]:
|
def find_item_locations(self, item: str, 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
|
||||||
@@ -369,7 +455,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, player: int) -> Location:
|
def find_item(self, item: str, 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)
|
||||||
|
|
||||||
@@ -462,9 +548,9 @@ class MultiWorld():
|
|||||||
return True
|
return True
|
||||||
state = starting_state.copy()
|
state = starting_state.copy()
|
||||||
else:
|
else:
|
||||||
if self.has_beaten_game(self.state):
|
|
||||||
return True
|
|
||||||
state = CollectionState(self)
|
state = CollectionState(self)
|
||||||
|
if self.has_beaten_game(state):
|
||||||
|
return True
|
||||||
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}
|
||||||
|
|
||||||
@@ -523,26 +609,21 @@ class MultiWorld():
|
|||||||
players: Dict[str, Set[int]] = {
|
players: Dict[str, Set[int]] = {
|
||||||
"minimal": set(),
|
"minimal": set(),
|
||||||
"items": set(),
|
"items": set(),
|
||||||
"locations": set()
|
"full": set()
|
||||||
}
|
}
|
||||||
for player, access in self.accessibility.items():
|
for player, world in self.worlds.items():
|
||||||
players[access.current_key].add(player)
|
players[world.options.accessibility.current_key].add(player)
|
||||||
|
|
||||||
beatable_fulfilled = False
|
beatable_fulfilled = False
|
||||||
|
|
||||||
def location_condition(location: Location):
|
def location_condition(location: Location) -> bool:
|
||||||
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
|
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
|
||||||
if location.player in players["locations"] or (location.item and location.item.player not in
|
return location.player in players["full"] or \
|
||||||
players["minimal"]):
|
(location.item and location.item.player not in players["minimal"])
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def location_relevant(location: Location):
|
def location_relevant(location: Location) -> bool:
|
||||||
"""Determine if this location is relevant to sweep."""
|
"""Determine if this location is relevant to sweep."""
|
||||||
if location.progress_type != LocationProgressType.EXCLUDED \
|
return location.player in players["full"] or location.advancement
|
||||||
and (location.player in players["locations"] or location.advancement):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def all_done() -> bool:
|
def all_done() -> bool:
|
||||||
"""Check if all access rules are fulfilled"""
|
"""Check if all access rules are fulfilled"""
|
||||||
@@ -587,7 +668,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]]
|
||||||
events: Set[Location]
|
advancements: 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]
|
||||||
@@ -599,7 +680,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.events = set()
|
self.advancements = 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()}
|
||||||
@@ -611,17 +692,25 @@ 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 = self.multiworld.get_region("Menu", player)
|
start: Region = world.get_region(world.origin_region_name)
|
||||||
|
|
||||||
# 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)
|
||||||
blocked_connections.update(start.exits)
|
self.blocked_connections[player].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()
|
||||||
@@ -641,16 +730,39 @@ 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 = copy.deepcopy(self.prog_items)
|
ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()}
|
||||||
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in
|
ret.reachable_regions = {player: region_set.copy() for player, region_set in
|
||||||
self.reachable_regions}
|
self.reachable_regions.items()}
|
||||||
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in
|
ret.blocked_connections = {player: entrance_set.copy() for player, entrance_set in
|
||||||
self.blocked_connections}
|
self.blocked_connections.items()}
|
||||||
ret.events = copy.copy(self.events)
|
ret.advancements = self.advancements.copy()
|
||||||
ret.path = copy.copy(self.path)
|
ret.path = self.path.copy()
|
||||||
ret.locations_checked = copy.copy(self.locations_checked)
|
ret.locations_checked = self.locations_checked.copy()
|
||||||
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
|
||||||
@@ -680,20 +792,25 @@ class CollectionState():
|
|||||||
def can_reach_region(self, spot: str, player: int) -> bool:
|
def can_reach_region(self, spot: str, player: int) -> bool:
|
||||||
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, key_only: bool = False, 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_events = True
|
reachable_advancements = True
|
||||||
# since the loop has a good chance to run more than once, only filter the events once
|
# since the loop has a good chance to run more than once, only filter the advancements once
|
||||||
locations = {location for location in locations if location.advancement and location not in self.events and
|
locations = {location for location in locations if location.advancement and location not in self.advancements}
|
||||||
not key_only or getattr(location.item, "locked_dungeon_item", False)}
|
|
||||||
while reachable_events:
|
while reachable_advancements:
|
||||||
reachable_events = {location for location in locations if location.can_reach(self)}
|
reachable_advancements = {location for location in locations if location.can_reach(self)}
|
||||||
locations -= reachable_events
|
locations -= reachable_advancements
|
||||||
for event in reachable_events:
|
for advancement in reachable_advancements:
|
||||||
self.events.add(event)
|
self.advancements.add(advancement)
|
||||||
assert isinstance(event.item, Item), "tried to collect Event with no Item"
|
assert isinstance(advancement.item, Item), "tried to collect Event with no Item"
|
||||||
self.collect(event.item, True, event)
|
self.collect(advancement.item, True, advancement)
|
||||||
|
|
||||||
# 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:
|
||||||
@@ -727,7 +844,7 @@ class CollectionState():
|
|||||||
if found >= count:
|
if found >= count:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool:
|
def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool:
|
||||||
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
|
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
|
||||||
Ignores duplicates of the same item."""
|
Ignores duplicates of the same item."""
|
||||||
@@ -742,7 +859,7 @@ class CollectionState():
|
|||||||
def count_from_list(self, items: Iterable[str], player: int) -> int:
|
def count_from_list(self, items: Iterable[str], player: int) -> int:
|
||||||
"""Returns the cumulative count of items from a list present in state."""
|
"""Returns the cumulative count of items from a list present in state."""
|
||||||
return sum(self.prog_items[player][item_name] for item_name in items)
|
return sum(self.prog_items[player][item_name] for item_name in items)
|
||||||
|
|
||||||
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
|
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
|
||||||
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
|
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
|
||||||
return sum(self.prog_items[player][item_name] > 0 for item_name in items)
|
return sum(self.prog_items[player][item_name] > 0 for item_name in items)
|
||||||
@@ -788,20 +905,16 @@ class CollectionState():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Item related
|
# Item related
|
||||||
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
|
def collect(self, item: Item, prevent_sweep: 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 event:
|
if changed and not prevent_sweep:
|
||||||
self.sweep_for_events()
|
self.sweep_for_advancements()
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
@@ -825,7 +938,7 @@ class Entrance:
|
|||||||
addresses = None
|
addresses = None
|
||||||
target = None
|
target = None
|
||||||
|
|
||||||
def __init__(self, player: int, name: str = '', parent: Region = None):
|
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.parent_region = parent
|
self.parent_region = parent
|
||||||
self.player = player
|
self.player = player
|
||||||
@@ -845,9 +958,6 @@ 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})'
|
||||||
|
|
||||||
@@ -973,7 +1083,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_type:
|
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
|
||||||
"""
|
"""
|
||||||
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.
|
||||||
|
|
||||||
@@ -1013,9 +1123,6 @@ 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})'
|
||||||
|
|
||||||
|
|
||||||
@@ -1034,9 +1141,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 = staticmethod(lambda state, item: False)
|
always_allow: Callable[[CollectionState, Item], bool] = 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 = staticmethod(lambda item: True)
|
item_rule: Callable[[Item], bool] = 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):
|
||||||
@@ -1045,16 +1152,20 @@ 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=True) -> bool:
|
def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool:
|
||||||
return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items)
|
return ((
|
||||||
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
|
self.always_allow(state, item)
|
||||||
and self.item_rule(item)
|
and item.name not in state.multiworld.worlds[item.player].options.non_local_items
|
||||||
and (not check_access or self.can_reach(state))))
|
) or (
|
||||||
|
(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:
|
||||||
# self.access_rule computes faster on average, so placing it first for faster abort
|
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average
|
||||||
assert self.parent_region, "Can't reach location without region"
|
assert self.parent_region, "Can't reach location without region"
|
||||||
return self.access_rule(state) and self.parent_region.can_reach(state)
|
return self.parent_region.can_reach(state) and self.access_rule(state)
|
||||||
|
|
||||||
def place_locked_item(self, item: Item):
|
def place_locked_item(self, item: Item):
|
||||||
if self.item:
|
if self.item:
|
||||||
@@ -1064,9 +1175,6 @@ 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})'
|
||||||
|
|
||||||
@@ -1088,7 +1196,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 and self.item.game == self.game
|
return self.item is not None and self.item.game == self.game
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hint_text(self) -> str:
|
def hint_text(self) -> str:
|
||||||
@@ -1096,13 +1204,26 @@ class Location:
|
|||||||
|
|
||||||
|
|
||||||
class ItemClassification(IntFlag):
|
class ItemClassification(IntFlag):
|
||||||
filler = 0b0000 # aka trash, as in filler items like ammo, currency etc,
|
filler = 0b0000
|
||||||
progression = 0b0001 # Item that is logically relevant
|
""" aka trash, as in filler items like ammo, currency etc """
|
||||||
useful = 0b0010 # Item that is generally quite useful, but not required for anything logical
|
|
||||||
trap = 0b0100 # detrimental or entirely useless (nothing) item
|
progression = 0b0001
|
||||||
skip_balancing = 0b1000 # should technically never occur on its own
|
""" Item that is logically relevant.
|
||||||
# Item that is logically relevant, but progression balancing should not touch.
|
Protects this item from being placed on excluded or unreachable locations. """
|
||||||
# Typically currency or other counted items.
|
|
||||||
|
useful = 0b0010
|
||||||
|
""" Item that is especially useful.
|
||||||
|
Protects this item from being placed on excluded or unreachable locations.
|
||||||
|
When combined with another flag like "progression", it means "an especially useful progression item". """
|
||||||
|
|
||||||
|
trap = 0b0100
|
||||||
|
""" Item that is detrimental in some way. """
|
||||||
|
|
||||||
|
skip_balancing = 0b1000
|
||||||
|
""" should technically never occur on its own
|
||||||
|
Item that is logically relevant, but progression balancing should not touch.
|
||||||
|
Typically currency or other counted items. """
|
||||||
|
|
||||||
progression_skip_balancing = 0b1001 # only progression gets balanced
|
progression_skip_balancing = 0b1001 # only progression gets balanced
|
||||||
|
|
||||||
def as_flag(self) -> int:
|
def as_flag(self) -> int:
|
||||||
@@ -1171,9 +1292,6 @@ 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})"
|
||||||
@@ -1251,9 +1369,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 = {}
|
restore_later: Dict[Location, Item] = {}
|
||||||
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
|
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
|
||||||
to_delete = set()
|
to_delete: Set[Location] = 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,
|
||||||
@@ -1271,7 +1389,7 @@ class Spoiler:
|
|||||||
sphere -= to_delete
|
sphere -= to_delete
|
||||||
|
|
||||||
# second phase, sphere 0
|
# second phase, sphere 0
|
||||||
removed_precollected = []
|
removed_precollected: List[Item] = []
|
||||||
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)
|
||||||
@@ -1291,8 +1409,6 @@ class Spoiler:
|
|||||||
state = CollectionState(multiworld)
|
state = CollectionState(multiworld)
|
||||||
collection_spheres = []
|
collection_spheres = []
|
||||||
while required_locations:
|
while required_locations:
|
||||||
state.sweep_for_events(key_only=True)
|
|
||||||
|
|
||||||
sphere = set(filter(state.can_reach, required_locations))
|
sphere = set(filter(state.can_reach, required_locations))
|
||||||
|
|
||||||
for location in sphere:
|
for location in sphere:
|
||||||
@@ -1354,7 +1470,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.mode[player] != 'inverted':
|
if multiworld.worlds[player].options.mode != '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:
|
||||||
@@ -1426,9 +1542,9 @@ class Spoiler:
|
|||||||
|
|
||||||
if self.paths:
|
if self.paths:
|
||||||
outfile.write('\n\nPaths:\n\n')
|
outfile.write('\n\nPaths:\n\n')
|
||||||
path_listings = []
|
path_listings: List[str] = []
|
||||||
for location, path in sorted(self.paths.items()):
|
for location, path in sorted(self.paths.items()):
|
||||||
path_lines = []
|
path_lines: List[str] = []
|
||||||
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))
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class ClientCommandProcessor(CommandProcessor):
|
|||||||
if address:
|
if address:
|
||||||
self.ctx.server_address = None
|
self.ctx.server_address = None
|
||||||
self.ctx.username = None
|
self.ctx.username = None
|
||||||
|
self.ctx.password = None
|
||||||
elif not self.ctx.server_address:
|
elif not self.ctx.server_address:
|
||||||
self.output("Please specify an address.")
|
self.output("Please specify an address.")
|
||||||
return False
|
return False
|
||||||
@@ -251,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 = None
|
ui: typing.Optional["kvui.GameManager"] = 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
|
||||||
@@ -514,6 +515,7 @@ class CommonContext:
|
|||||||
async def shutdown(self):
|
async def shutdown(self):
|
||||||
self.server_address = ""
|
self.server_address = ""
|
||||||
self.username = None
|
self.username = None
|
||||||
|
self.password = None
|
||||||
self.cancel_autoreconnect()
|
self.cancel_autoreconnect()
|
||||||
if self.server and not self.server.socket.closed:
|
if self.server and not self.server.socket.closed:
|
||||||
await self.server.socket.close()
|
await self.server.socket.close()
|
||||||
@@ -660,17 +662,19 @@ 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 run_gui(self):
|
def make_gui(self) -> typing.Type["kvui.GameManager"]:
|
||||||
"""Import kivy UI system and start running it as self.ui_task."""
|
"""To return the Kivy App class needed for run_gui so it can be overridden before being built"""
|
||||||
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"
|
||||||
|
|
||||||
self.ui = TextManager(self)
|
return TextManager
|
||||||
|
|
||||||
|
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):
|
||||||
@@ -992,7 +996,7 @@ def get_base_parser(description: typing.Optional[str] = None):
|
|||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
def run_as_textclient():
|
def run_as_textclient(*args):
|
||||||
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"}
|
||||||
@@ -1031,15 +1035,18 @@ def run_as_textclient():
|
|||||||
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 = parser.parse_args(args)
|
||||||
|
|
||||||
if args.url:
|
if args.url:
|
||||||
url = urllib.parse.urlparse(args.url)
|
url = urllib.parse.urlparse(args.url)
|
||||||
args.connect = url.netloc
|
if url.scheme == "archipelago":
|
||||||
if url.username:
|
args.connect = url.netloc
|
||||||
args.name = urllib.parse.unquote(url.username)
|
if url.username:
|
||||||
if url.password:
|
args.name = urllib.parse.unquote(url.username)
|
||||||
args.password = urllib.parse.unquote(url.password)
|
if 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()
|
||||||
|
|
||||||
@@ -1049,4 +1056,4 @@ def run_as_textclient():
|
|||||||
|
|
||||||
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()
|
run_as_textclient(*sys.argv[1:]) # default value for parse_args
|
||||||
|
|||||||
34
Fill.py
34
Fill.py
@@ -12,7 +12,12 @@ from worlds.generic.Rules import add_item_rule
|
|||||||
|
|
||||||
|
|
||||||
class FillError(RuntimeError):
|
class FillError(RuntimeError):
|
||||||
pass
|
def __init__(self, *args: typing.Union[str, typing.Any], **kwargs) -> None:
|
||||||
|
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:
|
||||||
@@ -24,7 +29,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_events(locations=locations)
|
new_state.sweep_for_advancements(locations=locations)
|
||||||
return new_state
|
return new_state
|
||||||
|
|
||||||
|
|
||||||
@@ -212,7 +217,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)}")
|
f"{', '.join(str(place) for place in placements)}", multiworld=multiworld)
|
||||||
|
|
||||||
item_pool.extend(unplaced_items)
|
item_pool.extend(unplaced_items)
|
||||||
|
|
||||||
@@ -299,7 +304,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)}")
|
f"{', '.join(str(place) for place in placements)}", multiworld=multiworld)
|
||||||
|
|
||||||
itempool.extend(unplaced_items)
|
itempool.extend(unplaced_items)
|
||||||
|
|
||||||
@@ -324,8 +329,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.events:
|
if location in state.advancements:
|
||||||
state.events.remove(location)
|
state.advancements.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)
|
||||||
@@ -358,7 +363,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_events(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
|
base_state.sweep_for_advancements(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:
|
||||||
@@ -506,7 +511,8 @@ 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)
|
||||||
|
|
||||||
@@ -523,7 +529,8 @@ 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 excludable items.",
|
||||||
|
multiworld=multiworld,
|
||||||
)
|
)
|
||||||
|
|
||||||
restitempool = filleritempool + usefulitempool
|
restitempool = filleritempool + usefulitempool
|
||||||
@@ -551,7 +558,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_events()
|
multiworld.state.sweep_for_advancements()
|
||||||
|
|
||||||
# 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:
|
||||||
@@ -589,7 +596,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.')
|
raise FillError('No more progress items left to place.', multiworld=multiworld)
|
||||||
|
|
||||||
# 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()
|
||||||
@@ -646,7 +653,6 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
|||||||
|
|
||||||
def get_sphere_locations(sphere_state: CollectionState,
|
def get_sphere_locations(sphere_state: CollectionState,
|
||||||
locations: typing.Set[Location]) -> typing.Set[Location]:
|
locations: typing.Set[Location]) -> typing.Set[Location]:
|
||||||
sphere_state.sweep_for_events(key_only=True, locations=locations)
|
|
||||||
return {loc for loc in locations if sphere_state.can_reach(loc)}
|
return {loc for loc in locations if sphere_state.can_reach(loc)}
|
||||||
|
|
||||||
def item_percentage(player: int, num: int) -> float:
|
def item_percentage(player: int, num: int) -> float:
|
||||||
@@ -740,7 +746,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_events(locations=locations_to_test)
|
reducing_state.sweep_for_advancements(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):
|
||||||
@@ -823,7 +829,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_events()
|
swept_state.sweep_for_advancements()
|
||||||
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)
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ 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 = {}
|
||||||
|
|
||||||
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)
|
||||||
@@ -202,7 +203,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 not erargs.name[player]: # if name was not specified, generate it from filename
|
elif player not in erargs.name: # 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)
|
||||||
|
|
||||||
@@ -511,7 +512,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 = game_weights.get("plando_items", [])
|
ret.plando_items = copy.deepcopy(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)
|
||||||
|
|
||||||
|
|||||||
9
KH1Client.py
Normal file
9
KH1Client.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
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,10 +16,11 @@ 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, Sequence, Union, Optional
|
from typing import Callable, Optional, Sequence, Tuple, Union
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
import settings
|
import settings
|
||||||
@@ -107,7 +108,81 @@ components.extend([
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
def identify(path: Union[None, str]):
|
def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
||||||
|
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:
|
||||||
@@ -266,7 +341,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 {filename}")
|
logging.warning(f"unable to identify component for {file}")
|
||||||
|
|
||||||
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.
|
||||||
@@ -299,20 +374,24 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
|||||||
elif not args:
|
elif not args:
|
||||||
args = {}
|
args = {}
|
||||||
|
|
||||||
if args.get("Patch|Game|Component", None) is not None:
|
path = args.get("Patch|Game|Component|url", None)
|
||||||
file, component = identify(args["Patch|Game|Component"])
|
if path is not None:
|
||||||
|
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 {args['Patch|Game|Component']}")
|
logging.warning(f"Could not identify Component responsible for {path}")
|
||||||
|
|
||||||
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()
|
||||||
@@ -322,12 +401,16 @@ 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(description='Archipelago Launcher')
|
parser = argparse.ArgumentParser(
|
||||||
|
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", type=str, nargs="?",
|
run_group.add_argument("Patch|Game|Component|url", type=str, nargs="?",
|
||||||
help="Pass either a patch file, a generated game or the name of a component to run.")
|
help="Pass either a patch file, a generated game, the component name to run, or a url to "
|
||||||
|
"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())
|
||||||
|
|||||||
@@ -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, TOP, LabelFrame, \
|
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, BOTH, 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,7 +29,8 @@ 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):
|
||||||
@@ -242,16 +243,17 @@ 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)
|
bottomFrame2 = Frame(adjustWindow, padx=8, pady=2)
|
||||||
|
|
||||||
romFrame, romVar = get_rom_frame(adjustWindow)
|
romFrame, romVar = get_rom_frame(adjustWindow)
|
||||||
|
|
||||||
romDialogFrame = Frame(adjustWindow)
|
romDialogFrame = Frame(adjustWindow, padx=8, pady=2)
|
||||||
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)
|
||||||
@@ -261,9 +263,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=True, fill=X)
|
romDialogFrame.pack(side=TOP, expand=False, fill=X)
|
||||||
baseRomLabel2.pack(side=LEFT)
|
baseRomLabel2.pack(side=LEFT, expand=False, fill=X, padx=(0, 8))
|
||||||
romEntry2.pack(side=LEFT, expand=True, fill=X)
|
romEntry2.pack(side=LEFT, expand=True, fill=BOTH, pady=1)
|
||||||
romSelectButton2.pack(side=LEFT)
|
romSelectButton2.pack(side=LEFT)
|
||||||
|
|
||||||
def adjustRom():
|
def adjustRom():
|
||||||
@@ -331,12 +333,11 @@ 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)
|
rom_options_frame.pack(side=TOP, padx=8, pady=8, fill=BOTH, expand=True)
|
||||||
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)
|
||||||
@@ -576,7 +577,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)
|
romFrame = Frame(parent, padx=8, pady=8)
|
||||||
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)
|
||||||
@@ -596,20 +597,19 @@ 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=X)
|
romEntry.pack(side=LEFT, expand=True, fill=BOTH, pady=1)
|
||||||
romSelectButton.pack(side=LEFT)
|
romSelectButton.pack(side=LEFT)
|
||||||
romFrame.pack(side=TOP, expand=True, fill=X)
|
romFrame.pack(side=TOP, 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")
|
romOptionsFrame = LabelFrame(parent, text="Rom options", padx=8, pady=8)
|
||||||
romOptionsFrame.columnconfigure(0, weight=1)
|
|
||||||
romOptionsFrame.columnconfigure(1, weight=1)
|
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
romOptionsFrame.rowconfigure(i, weight=1)
|
romOptionsFrame.rowconfigure(i, weight=0, pad=4)
|
||||||
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)
|
spriteEntry.pack(side=LEFT, expand=True, fill=X)
|
||||||
spriteSelectButton.pack(side=LEFT)
|
spriteSelectButton.pack(side=LEFT)
|
||||||
|
|
||||||
oofDialogFrame = Frame(romOptionsFrame)
|
oofDialogFrame = Frame(romOptionsFrame)
|
||||||
|
|||||||
112
Main.py
112
Main.py
@@ -11,7 +11,8 @@ 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 balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
|
from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, distribute_planned, \
|
||||||
|
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
|
||||||
@@ -100,7 +101,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.early_local_items[player].get(item_name, 0)
|
local_early = multiworld.local_early_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
|
||||||
@@ -124,14 +125,19 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
for player in multiworld.player_ids:
|
for player in multiworld.player_ids:
|
||||||
exclusion_rules(multiworld, player, multiworld.worlds[player].options.exclude_locations.value)
|
exclusion_rules(multiworld, player, multiworld.worlds[player].options.exclude_locations.value)
|
||||||
multiworld.worlds[player].options.priority_locations.value -= multiworld.worlds[player].options.exclude_locations.value
|
multiworld.worlds[player].options.priority_locations.value -= multiworld.worlds[player].options.exclude_locations.value
|
||||||
|
world_excluded_locations = set()
|
||||||
for location_name in multiworld.worlds[player].options.priority_locations.value:
|
for location_name in multiworld.worlds[player].options.priority_locations.value:
|
||||||
try:
|
try:
|
||||||
location = multiworld.get_location(location_name, player)
|
location = multiworld.get_location(location_name, player)
|
||||||
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
|
except KeyError:
|
||||||
if location_name not in multiworld.worlds[player].location_name_to_id:
|
continue
|
||||||
raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e
|
|
||||||
else:
|
if location.progress_type != LocationProgressType.EXCLUDED:
|
||||||
location.progress_type = LocationProgressType.PRIORITY
|
location.progress_type = LocationProgressType.PRIORITY
|
||||||
|
else:
|
||||||
|
logger.warning(f"Unable to prioritize location \"{location_name}\" in player {player}'s world because the world excluded it.")
|
||||||
|
world_excluded_locations.add(location_name)
|
||||||
|
multiworld.worlds[player].options.priority_locations.value -= world_excluded_locations
|
||||||
|
|
||||||
# Set local and non-local item rules.
|
# Set local and non-local item rules.
|
||||||
if multiworld.players > 1:
|
if multiworld.players > 1:
|
||||||
@@ -146,6 +152,7 @@ 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",
|
||||||
@@ -164,97 +171,26 @@ 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:
|
||||||
new_items.extend(multiworld.itempool[i+1:])
|
old_items.extend(multiworld.itempool[i+1:])
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
new_items.append(item)
|
old_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:
|
||||||
raise Exception(f"{multiworld.get_player_name(player)}"
|
logger.warning(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}")
|
||||||
assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change."
|
# find all filler we generated for the current player and remove until it matches
|
||||||
multiworld.itempool[:] = new_items
|
removables = [item for item in new_items if item.player == player]
|
||||||
|
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
|
||||||
|
|
||||||
# temporary home for item links, should be moved out of Main
|
multiworld.link_items()
|
||||||
for group_id, group in multiworld.groups.items():
|
|
||||||
def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
|
|
||||||
Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]]
|
|
||||||
]:
|
|
||||||
classifications: Dict[str, int] = collections.defaultdict(int)
|
|
||||||
counters = {player: {name: 0 for name in shared_pool} for player in players}
|
|
||||||
for item in multiworld.itempool:
|
|
||||||
if item.player in counters and item.name in shared_pool:
|
|
||||||
counters[item.player][item.name] += 1
|
|
||||||
classifications[item.name] |= item.classification
|
|
||||||
|
|
||||||
for player in players.copy():
|
|
||||||
if all([counters[player][item] == 0 for item in shared_pool]):
|
|
||||||
players.remove(player)
|
|
||||||
del (counters[player])
|
|
||||||
|
|
||||||
if not players:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
for item in shared_pool:
|
|
||||||
count = min(counters[player][item] for player in players)
|
|
||||||
if count:
|
|
||||||
for player in players:
|
|
||||||
counters[player][item] = count
|
|
||||||
else:
|
|
||||||
for player in players:
|
|
||||||
del (counters[player][item])
|
|
||||||
return counters, classifications
|
|
||||||
|
|
||||||
common_item_count, classifications = find_common_pool(group["players"], group["item_pool"])
|
|
||||||
if not common_item_count:
|
|
||||||
continue
|
|
||||||
|
|
||||||
new_itempool: List[Item] = []
|
|
||||||
for item_name, item_count in next(iter(common_item_count.values())).items():
|
|
||||||
for _ in range(item_count):
|
|
||||||
new_item = group["world"].create_item(item_name)
|
|
||||||
# mangle together all original classification bits
|
|
||||||
new_item.classification |= classifications[item_name]
|
|
||||||
new_itempool.append(new_item)
|
|
||||||
|
|
||||||
region = Region("Menu", group_id, multiworld, "ItemLink")
|
|
||||||
multiworld.regions.append(region)
|
|
||||||
locations = region.locations
|
|
||||||
for item in multiworld.itempool:
|
|
||||||
count = common_item_count.get(item.player, {}).get(item.name, 0)
|
|
||||||
if count:
|
|
||||||
loc = Location(group_id, f"Item Link: {item.name} -> {multiworld.player_name[item.player]} {count}",
|
|
||||||
None, region)
|
|
||||||
loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \
|
|
||||||
state.has(item_name, group_id_, count_)
|
|
||||||
|
|
||||||
locations.append(loc)
|
|
||||||
loc.place_locked_item(item)
|
|
||||||
common_item_count[item.player][item.name] -= 1
|
|
||||||
else:
|
|
||||||
new_itempool.append(item)
|
|
||||||
|
|
||||||
itemcount = len(multiworld.itempool)
|
|
||||||
multiworld.itempool = new_itempool
|
|
||||||
|
|
||||||
while itemcount > len(multiworld.itempool):
|
|
||||||
items_to_add = []
|
|
||||||
for player in group["players"]:
|
|
||||||
if group["link_replacement"]:
|
|
||||||
item_player = group_id
|
|
||||||
else:
|
|
||||||
item_player = player
|
|
||||||
if group["replacement_items"][player]:
|
|
||||||
items_to_add.append(AutoWorld.call_single(multiworld, "create_item", item_player,
|
|
||||||
group["replacement_items"][player]))
|
|
||||||
else:
|
|
||||||
items_to_add.append(AutoWorld.call_single(multiworld, "create_filler", item_player))
|
|
||||||
multiworld.random.shuffle(items_to_add)
|
|
||||||
multiworld.itempool.extend(items_to_add[:itemcount - len(multiworld.itempool)])
|
|
||||||
|
|
||||||
if any(multiworld.item_links.values()):
|
if any(multiworld.item_links.values()):
|
||||||
multiworld._all_state = None
|
multiworld._all_state = None
|
||||||
@@ -411,7 +347,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 Exception("Game appears as unbeatable. Aborting.")
|
raise FillError("Game appears as unbeatable. Aborting.", multiworld=multiworld)
|
||||||
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,6 +67,21 @@ 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:
|
||||||
@@ -551,6 +566,9 @@ 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()
|
||||||
|
|
||||||
@@ -991,7 +1009,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[int]:
|
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[typing.Tuple[int, int]]:
|
||||||
return ctx.locations.get_remaining(ctx.location_checks, team, slot)
|
return ctx.locations.get_remaining(ctx.location_checks, team, slot)
|
||||||
|
|
||||||
|
|
||||||
@@ -1203,6 +1221,10 @@ 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
|
||||||
|
|
||||||
@@ -1350,10 +1372,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":
|
||||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||||
if remaining_item_ids:
|
if rest_locations:
|
||||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id]
|
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id]
|
||||||
for item_id in remaining_item_ids))
|
for slot, item_id in rest_locations))
|
||||||
else:
|
else:
|
||||||
self.output("No remaining items found.")
|
self.output("No remaining items found.")
|
||||||
return True
|
return True
|
||||||
@@ -1363,10 +1385,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:
|
||||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||||
if remaining_item_ids:
|
if rest_locations:
|
||||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id]
|
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id]
|
||||||
for item_id in remaining_item_ids))
|
for slot, item_id in rest_locations))
|
||||||
else:
|
else:
|
||||||
self.output("No remaining items found.")
|
self.output("No remaining items found.")
|
||||||
return True
|
return True
|
||||||
@@ -2039,6 +2061,8 @@ 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)
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -397,12 +398,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[int]:
|
) -> typing.List[typing.Tuple[int, int]]:
|
||||||
checked = state[team, slot]
|
checked = state[team, slot]
|
||||||
player_locations = self[slot]
|
player_locations = self[slot]
|
||||||
return sorted([player_locations[location_id][0] for
|
return sorted([(player_locations[location_id][1], 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
|
||||||
|
|||||||
110
Options.py
110
Options.py
@@ -786,17 +786,22 @@ class VerifyKeys(metaclass=FreezeValidKeys):
|
|||||||
verify_location_name: bool = False
|
verify_location_name: bool = False
|
||||||
value: typing.Any
|
value: typing.Any
|
||||||
|
|
||||||
@classmethod
|
def verify_keys(self) -> None:
|
||||||
def verify_keys(cls, data: typing.Iterable[str]) -> None:
|
if self.valid_keys:
|
||||||
if cls.valid_keys:
|
data = set(self.value)
|
||||||
data = set(data)
|
dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data)
|
||||||
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data)
|
extra = dataset - self._valid_keys
|
||||||
extra = dataset - cls._valid_keys
|
|
||||||
if extra:
|
if extra:
|
||||||
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
|
raise OptionError(
|
||||||
f"Allowed keys: {cls._valid_keys}.")
|
f"Found unexpected key {', '.join(extra)} in {getattr(self, 'display_name', self)}. "
|
||||||
|
f"Allowed keys: {self._valid_keys}."
|
||||||
|
)
|
||||||
|
|
||||||
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
||||||
|
try:
|
||||||
|
self.verify_keys()
|
||||||
|
except OptionError as validation_error:
|
||||||
|
raise OptionError(f"Player {player_name} has invalid option keys:\n{validation_error}")
|
||||||
if self.convert_name_groups and self.verify_item_name:
|
if self.convert_name_groups and self.verify_item_name:
|
||||||
new_value = type(self.value)() # empty container of whatever value is
|
new_value = type(self.value)() # empty container of whatever value is
|
||||||
for item_name in self.value:
|
for item_name in self.value:
|
||||||
@@ -833,7 +838,6 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
|
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
|
||||||
if type(data) == dict:
|
if type(data) == dict:
|
||||||
cls.verify_keys(data)
|
|
||||||
return cls(data)
|
return cls(data)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||||
@@ -879,7 +883,6 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: typing.Any):
|
def from_any(cls, data: typing.Any):
|
||||||
if is_iterable_except_str(data):
|
if is_iterable_except_str(data):
|
||||||
cls.verify_keys(data)
|
|
||||||
return cls(data)
|
return cls(data)
|
||||||
return cls.from_text(str(data))
|
return cls.from_text(str(data))
|
||||||
|
|
||||||
@@ -905,7 +908,6 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: typing.Any):
|
def from_any(cls, data: typing.Any):
|
||||||
if is_iterable_except_str(data):
|
if is_iterable_except_str(data):
|
||||||
cls.verify_keys(data)
|
|
||||||
return cls(data)
|
return cls(data)
|
||||||
return cls.from_text(str(data))
|
return cls.from_text(str(data))
|
||||||
|
|
||||||
@@ -948,6 +950,19 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
|
|||||||
self.value = []
|
self.value = []
|
||||||
logging.warning(f"The plando texts module is turned off, "
|
logging.warning(f"The plando texts module is turned off, "
|
||||||
f"so text for {player_name} will be ignored.")
|
f"so text for {player_name} will be ignored.")
|
||||||
|
else:
|
||||||
|
super().verify(world, player_name, plando_options)
|
||||||
|
|
||||||
|
def verify_keys(self) -> None:
|
||||||
|
if self.valid_keys:
|
||||||
|
data = set(text.at for text in self)
|
||||||
|
dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data)
|
||||||
|
extra = dataset - self._valid_keys
|
||||||
|
if extra:
|
||||||
|
raise OptionError(
|
||||||
|
f"Invalid \"at\" placement {', '.join(extra)} in {getattr(self, 'display_name', self)}. "
|
||||||
|
f"Allowed placements: {self._valid_keys}."
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: PlandoTextsFromAnyType) -> Self:
|
def from_any(cls, data: PlandoTextsFromAnyType) -> Self:
|
||||||
@@ -958,7 +973,19 @@ 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(
|
||||||
@@ -966,12 +993,13 @@ 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)
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}")
|
raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}")
|
||||||
cls.verify_keys([text.at for text in texts])
|
|
||||||
return cls(texts)
|
return cls(texts)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}")
|
raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}")
|
||||||
@@ -1144,18 +1172,35 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect
|
|||||||
|
|
||||||
|
|
||||||
class Accessibility(Choice):
|
class Accessibility(Choice):
|
||||||
"""Set rules for reachability of your items/locations.
|
"""
|
||||||
|
Set rules for reachability of your items/locations.
|
||||||
|
|
||||||
|
**Full:** ensure everything can be reached and acquired.
|
||||||
|
|
||||||
- **Locations:** ensure everything can be reached and acquired.
|
**Minimal:** ensure what is needed to reach your goal can be acquired.
|
||||||
- **Items:** ensure all logically relevant items can be acquired.
|
|
||||||
- **Minimal:** ensure what is needed to reach your goal can be acquired.
|
|
||||||
"""
|
"""
|
||||||
display_name = "Accessibility"
|
display_name = "Accessibility"
|
||||||
rich_text_doc = True
|
rich_text_doc = True
|
||||||
option_locations = 0
|
option_full = 0
|
||||||
option_items = 1
|
|
||||||
option_minimal = 2
|
option_minimal = 2
|
||||||
alias_none = 2
|
alias_none = 2
|
||||||
|
alias_locations = 0
|
||||||
|
alias_items = 0
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class ItemsAccessibility(Accessibility):
|
||||||
|
"""
|
||||||
|
Set rules for reachability of your items/locations.
|
||||||
|
|
||||||
|
**Full:** ensure everything can be reached and acquired.
|
||||||
|
|
||||||
|
**Minimal:** ensure what is needed to reach your goal can be acquired.
|
||||||
|
|
||||||
|
**Items:** ensure all logically relevant items can be acquired. Some items, such as keys, may be self-locking, and
|
||||||
|
some locations may be inaccessible.
|
||||||
|
"""
|
||||||
|
option_items = 1
|
||||||
default = 1
|
default = 1
|
||||||
|
|
||||||
|
|
||||||
@@ -1205,6 +1250,7 @@ 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:
|
||||||
@@ -1486,31 +1532,3 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
|||||||
|
|
||||||
with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f:
|
with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f:
|
||||||
f.write(res)
|
f.write(res)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
|
|
||||||
from worlds.alttp.Options import Logic
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
map_shuffle = Toggle
|
|
||||||
compass_shuffle = Toggle
|
|
||||||
key_shuffle = Toggle
|
|
||||||
big_key_shuffle = Toggle
|
|
||||||
hints = Toggle
|
|
||||||
test = argparse.Namespace()
|
|
||||||
test.logic = Logic.from_text("no_logic")
|
|
||||||
test.map_shuffle = map_shuffle.from_text("ON")
|
|
||||||
test.hints = hints.from_text('OFF')
|
|
||||||
try:
|
|
||||||
test.logic = Logic.from_text("overworld_glitches_typo")
|
|
||||||
except KeyError as e:
|
|
||||||
print(e)
|
|
||||||
try:
|
|
||||||
test.logic_owg = Logic.from_text("owg")
|
|
||||||
except KeyError as e:
|
|
||||||
print(e)
|
|
||||||
if test.map_shuffle:
|
|
||||||
print("map_shuffle is on")
|
|
||||||
print(f"Hints are {bool(test.hints)}")
|
|
||||||
print(test)
|
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ 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
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
|||||||
def _cmd_patch(self):
|
def _cmd_patch(self):
|
||||||
"""Patch the game. Only use this command if /auto_patch fails."""
|
"""Patch the game. Only use this command if /auto_patch fails."""
|
||||||
if isinstance(self.ctx, UndertaleContext):
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True)
|
os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True)
|
||||||
self.ctx.patch_game()
|
self.ctx.patch_game()
|
||||||
self.output("Patched.")
|
self.output("Patched.")
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
|||||||
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
|
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
|
||||||
"""Patch the game automatically."""
|
"""Patch the game automatically."""
|
||||||
if isinstance(self.ctx, UndertaleContext):
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True)
|
os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True)
|
||||||
tempInstall = steaminstall
|
tempInstall = steaminstall
|
||||||
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
||||||
tempInstall = None
|
tempInstall = None
|
||||||
@@ -62,7 +62,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
|||||||
for file_name in os.listdir(tempInstall):
|
for file_name in os.listdir(tempInstall):
|
||||||
if file_name != "steam_api.dll":
|
if file_name != "steam_api.dll":
|
||||||
shutil.copy(os.path.join(tempInstall, file_name),
|
shutil.copy(os.path.join(tempInstall, file_name),
|
||||||
os.path.join(os.getcwd(), "Undertale", file_name))
|
Utils.user_path("Undertale", file_name))
|
||||||
self.ctx.patch_game()
|
self.ctx.patch_game()
|
||||||
self.output("Patching successful!")
|
self.output("Patching successful!")
|
||||||
|
|
||||||
@@ -111,12 +111,12 @@ class UndertaleContext(CommonContext):
|
|||||||
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
||||||
|
|
||||||
def patch_game(self):
|
def patch_game(self):
|
||||||
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "rb") as f:
|
with open(Utils.user_path("Undertale", "data.win"), "rb") as f:
|
||||||
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
|
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
|
||||||
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "wb") as f:
|
with open(Utils.user_path("Undertale", "data.win"), "wb") as f:
|
||||||
f.write(patchedFile)
|
f.write(patchedFile)
|
||||||
os.makedirs(name=os.path.join(os.getcwd(), "Undertale", "Custom Sprites"), exist_ok=True)
|
os.makedirs(name=Utils.user_path("Undertale", "Custom Sprites"), exist_ok=True)
|
||||||
with open(os.path.expandvars(os.path.join(os.getcwd(), "Undertale", "Custom Sprites",
|
with open(os.path.expandvars(Utils.user_path("Undertale", "Custom Sprites",
|
||||||
"Which Character.txt")), "w") as f:
|
"Which Character.txt")), "w") as f:
|
||||||
f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only "
|
f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only "
|
||||||
"line other than this one.\n", "frisk"])
|
"line other than this one.\n", "frisk"])
|
||||||
|
|||||||
10
WebHost.py
10
WebHost.py
@@ -1,3 +1,4 @@
|
|||||||
|
import argparse
|
||||||
import os
|
import os
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import logging
|
import logging
|
||||||
@@ -31,6 +32,15 @@ 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()
|
||||||
|
|||||||
@@ -72,6 +72,14 @@ 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
|
||||||
@@ -249,6 +257,7 @@ 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)
|
||||||
@@ -279,6 +288,7 @@ 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
|
||||||
|
|
||||||
@@ -325,10 +335,12 @@ 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()
|
||||||
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)
|
||||||
logging.info(f"Starting room {next_room} on {name}.")
|
logging.info(f"Starting room {next_room} on {name}.")
|
||||||
|
del task # delete reference to task object
|
||||||
|
|
||||||
starter = Starter()
|
starter = Starter()
|
||||||
starter.daemon = True
|
starter.daemon = True
|
||||||
|
|||||||
@@ -231,6 +231,13 @@ def generate_yaml(game: str):
|
|||||||
|
|
||||||
del options[key]
|
del options[key]
|
||||||
|
|
||||||
|
# Detect keys which end with -range, indicating a NamedRange with a possible custom value
|
||||||
|
elif key_parts[-1].endswith("-range"):
|
||||||
|
if options[key_parts[-1][:-6]] == "custom":
|
||||||
|
options[key_parts[-1][:-6]] = val
|
||||||
|
|
||||||
|
del options[key]
|
||||||
|
|
||||||
# Detect random-* keys and set their options accordingly
|
# Detect random-* keys and set their options accordingly
|
||||||
for key, val in options.copy().items():
|
for key, val in options.copy().items():
|
||||||
if key.startswith("random-"):
|
if key.startswith("random-"):
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
flask>=3.0.3
|
flask>=3.0.3
|
||||||
werkzeug>=3.0.3
|
werkzeug>=3.0.4
|
||||||
pony>=0.7.17
|
pony>=0.7.19
|
||||||
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.7.0
|
Flask-Limiter>=3.8.0
|
||||||
bokeh>=3.1.1; python_version <= '3.8'
|
bokeh>=3.1.1; python_version <= '3.8'
|
||||||
bokeh>=3.4.1; python_version >= '3.9'
|
bokeh>=3.4.3; 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 **#archipelago-dev** channel on our Discord.
|
If you have more questions, feel free to ask in the **#ap-world-dev** channel on our Discord.
|
||||||
|
|||||||
@@ -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 TextClient"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></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>{{ patch.game }}</td>
|
<td>{{ patch.game }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if patch.data %}
|
{% if patch.data %}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
{% macro NamedRange(option_name, option) %}
|
{% macro NamedRange(option_name, option) %}
|
||||||
{{ OptionTitle(option_name, option) }}
|
{{ OptionTitle(option_name, option) }}
|
||||||
<div class="named-range-container">
|
<div class="named-range-container">
|
||||||
<select id="{{ option_name }}-select" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
<select id="{{ option_name }}-select" name="{{ option_name }}" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||||
{% for key, val in option.special_range_names.items() %}
|
{% for key, val in option.special_range_names.items() %}
|
||||||
{% if option.default == val %}
|
{% if option.default == val %}
|
||||||
<option value="{{ val }}" selected>{{ key|replace("_", " ")|title }} ({{ val }})</option>
|
<option value="{{ val }}" selected>{{ key|replace("_", " ")|title }} ({{ val }})</option>
|
||||||
@@ -64,17 +64,17 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
<option value="custom" hidden>Custom</option>
|
<option value="custom" hidden>Custom</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="named-range-wrapper">
|
<div class="named-range-wrapper js-required">
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
id="{{ option_name }}"
|
id="{{ option_name }}"
|
||||||
name="{{ option_name }}"
|
name="{{ option_name }}-range"
|
||||||
min="{{ option.range_start }}"
|
min="{{ option.range_start }}"
|
||||||
max="{{ option.range_end }}"
|
max="{{ option.range_end }}"
|
||||||
value="{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}"
|
value="{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}"
|
||||||
{{ "disabled" if option.default == "random" }}
|
{{ "disabled" if option.default == "random" }}
|
||||||
/>
|
/>
|
||||||
<span id="{{ option_name }}-value" class="range-value js-required">
|
<span id="{{ option_name }}-value" class="range-value">
|
||||||
{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}
|
{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}
|
||||||
</span>
|
</span>
|
||||||
{{ RandomizeButton(option_name, option) }}
|
{{ RandomizeButton(option_name, option) }}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<noscript>
|
<noscript>
|
||||||
<style>
|
<style>
|
||||||
.js-required{
|
.js-required{
|
||||||
display: none;
|
display: none !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
You have no generated any seeds yet!
|
You have not 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="{{ "checked" if key in option.default else "" }}"
|
{{ "checked" if key in option.default }}
|
||||||
/>
|
/>
|
||||||
<label for="{{ option_name }}-{{ key }}">
|
<label for="{{ option_name }}-{{ key }}">
|
||||||
{{ key }}
|
{{ key }}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class TrackerData:
|
|||||||
|
|
||||||
# Normal lookup tables as well.
|
# Normal lookup tables as well.
|
||||||
self.item_name_to_id[game] = game_package["item_name_to_id"]
|
self.item_name_to_id[game] = game_package["item_name_to_id"]
|
||||||
self.location_name_to_id[game] = game_package["item_name_to_id"]
|
self.location_name_to_id[game] = game_package["location_name_to_id"]
|
||||||
|
|
||||||
def get_seed_name(self) -> str:
|
def get_seed_name(self) -> str:
|
||||||
"""Retrieves the seed name."""
|
"""Retrieves the seed name."""
|
||||||
|
|||||||
@@ -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[int]:
|
def get_remaining(self, state: State, team: int, slot: int) -> List[Tuple[int, 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.item for
|
return sorted([(entry.receiver, 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)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Archipelago World Code Owners / Maintainers Document
|
# Archipelago World Code Owners / Maintainers Document
|
||||||
#
|
#
|
||||||
# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder. For any pull
|
# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder as well as
|
||||||
# requests that modify these worlds, a code owner must approve the PR in addition to a core maintainer. This is not to
|
# certain documentation. For any pull requests that modify these worlds/docs, a code owner must approve the PR in
|
||||||
# be used for files/folders outside the /worlds folder, those will always need sign off from a core maintainer.
|
# addition to a core maintainer. All other files and folders are owned and maintained by core maintainers directly.
|
||||||
#
|
#
|
||||||
# All usernames must be GitHub usernames (and are case sensitive).
|
# All usernames must be GitHub usernames (and are case sensitive).
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
/worlds/clique/ @ThePhar
|
/worlds/clique/ @ThePhar
|
||||||
|
|
||||||
# Dark Souls III
|
# Dark Souls III
|
||||||
/worlds/dark_souls_3/ @Marechal-L
|
/worlds/dark_souls_3/ @Marechal-L @nex3
|
||||||
|
|
||||||
# Donkey Kong Country 3
|
# Donkey Kong Country 3
|
||||||
/worlds/dkc3/ @PoryGone
|
/worlds/dkc3/ @PoryGone
|
||||||
@@ -78,6 +78,9 @@
|
|||||||
# 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
|
||||||
|
|
||||||
@@ -103,6 +106,9 @@
|
|||||||
# 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
|
||||||
|
|
||||||
@@ -112,8 +118,8 @@
|
|||||||
# Noita
|
# Noita
|
||||||
/worlds/noita/ @ScipioWright @heinermann
|
/worlds/noita/ @ScipioWright @heinermann
|
||||||
|
|
||||||
# Ocarina of Time
|
# Old School Runescape
|
||||||
/worlds/oot/ @espeon65536
|
/worlds/osrs @digiholic
|
||||||
|
|
||||||
# Overcooked! 2
|
# Overcooked! 2
|
||||||
/worlds/overcooked2/ @toasterparty
|
/worlds/overcooked2/ @toasterparty
|
||||||
@@ -193,6 +199,9 @@
|
|||||||
# 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
|
||||||
|
|
||||||
@@ -218,6 +227,9 @@
|
|||||||
# 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
|
||||||
@@ -226,3 +238,11 @@
|
|||||||
|
|
||||||
# Ori and the Blind Forest
|
# Ori and the Blind Forest
|
||||||
# /worlds_disabled/oribf/
|
# /worlds_disabled/oribf/
|
||||||
|
|
||||||
|
###################
|
||||||
|
## Documentation ##
|
||||||
|
###################
|
||||||
|
|
||||||
|
# Apworld Dev Faq
|
||||||
|
/docs/apworld_dev_faq.md @qwint @ScipioWright
|
||||||
|
|
||||||
|
|||||||
45
docs/apworld_dev_faq.md
Normal file
45
docs/apworld_dev_faq.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# APWorld Dev FAQ
|
||||||
|
|
||||||
|
This document is meant as a reference tool to show solutions to common problems when developing an apworld.
|
||||||
|
It is not intended to answer every question about Archipelago and it assumes you have read the other docs,
|
||||||
|
including [Contributing](contributing.md), [Adding Games](<adding games.md>), and [World API](<world api.md>).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### My game has a restrictive start that leads to fill errors
|
||||||
|
|
||||||
|
Hint to the Generator that an item needs to be in sphere one with local_early_items. Here, `1` represents the number of "Sword" items to attempt to place in sphere one.
|
||||||
|
```py
|
||||||
|
early_item_name = "Sword"
|
||||||
|
self.multiworld.local_early_items[self.player][early_item_name] = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Some alternative ways to try to fix this problem are:
|
||||||
|
* Add more locations to sphere one of your world, potentially only when there would be a restrictive start
|
||||||
|
* Pre-place items yourself, such as during `create_items`
|
||||||
|
* Put items into the player's starting inventory using `push_precollected`
|
||||||
|
* Raise an exception, such as an `OptionError` during `generate_early`, to disallow options that would lead to a restrictive start
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### I have multiple settings that change the item/location pool counts and need to balance them out
|
||||||
|
|
||||||
|
In an ideal situation your system for producing locations and items wouldn't leave any opportunity for them to be unbalanced. But in real, complex situations, that might be unfeasible.
|
||||||
|
|
||||||
|
If that's the case, you can create extra filler based on the difference between your unfilled locations and your itempool by comparing [get_unfilled_locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#:~:text=get_unfilled_locations) to your list of items to submit
|
||||||
|
|
||||||
|
Note: to use self.create_filler(), self.get_filler_item_name() should be defined to only return valid filler item names
|
||||||
|
```py
|
||||||
|
total_locations = len(self.multiworld.get_unfilled_locations(self.player))
|
||||||
|
item_pool = self.create_non_filler_items()
|
||||||
|
|
||||||
|
for _ in range(total_locations - len(item_pool)):
|
||||||
|
item_pool.append(self.create_filler())
|
||||||
|
|
||||||
|
self.multiworld.itempool += item_pool
|
||||||
|
```
|
||||||
|
|
||||||
|
A faster alternative to the `for` loop would be to use a [list comprehension](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions):
|
||||||
|
```py
|
||||||
|
item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))]
|
||||||
|
```
|
||||||
@@ -510,7 +510,7 @@ In JSON this may look like:
|
|||||||
| ----- | ----- |
|
| ----- | ----- |
|
||||||
| 0 | Nothing special about this item |
|
| 0 | Nothing special about this item |
|
||||||
| 0b001 | If set, indicates the item can unlock logical advancement |
|
| 0b001 | If set, indicates the item can unlock logical advancement |
|
||||||
| 0b010 | If set, indicates the item is important but not in a way that unlocks advancement |
|
| 0b010 | If set, indicates the item is especially useful |
|
||||||
| 0b100 | If set, indicates the item is a trap |
|
| 0b100 | If set, indicates the item is a trap |
|
||||||
|
|
||||||
### JSONMessagePart
|
### JSONMessagePart
|
||||||
@@ -702,14 +702,18 @@ 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. |
|
||||||
| 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. |
|
| HintGame | Indicates the client is a hint game, made to send hints instead of 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. |
|
| Tracker | Indicates the client is a tracker, made to track instead of sending locations. Special join/leave message,¹ `game` is optional.² |
|
||||||
|
| 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/Special)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
|
`(Named)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,6 +129,23 @@ 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 is currently unsupported**
|
* Python 3.12.x is currently the newest supported version
|
||||||
* 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)
|
||||||
* **Python 3.12 is currently unsupported**
|
* [read above](#General) which versions are supported
|
||||||
|
|
||||||
* **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 #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
|
[Discord in #ap-core-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
|
||||||
|
|||||||
@@ -248,7 +248,8 @@ will all have the same ID. Name must not be numeric (must contain at least 1 let
|
|||||||
Other classifications include:
|
Other classifications include:
|
||||||
|
|
||||||
* `filler`: a regular item or trash item
|
* `filler`: a regular item or trash item
|
||||||
* `useful`: generally quite useful, but not required for anything logical. Cannot be placed on excluded locations
|
* `useful`: item that is especially useful. Cannot be placed on excluded or unreachable locations. When combined with
|
||||||
|
another flag like "progression", it means "an especially useful progression item".
|
||||||
* `trap`: negative impact on the player
|
* `trap`: negative impact on the player
|
||||||
* `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be
|
* `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be
|
||||||
combined with `progression`; see below)
|
combined with `progression`; see below)
|
||||||
@@ -303,6 +304,31 @@ 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
|
||||||
@@ -630,7 +656,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#L9).
|
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10).
|
||||||
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).
|
||||||
|
|||||||
@@ -26,8 +26,17 @@ 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). You can define who is allowed to approve changes to your world
|
nominate someone else (i.e. there are multiple devs).
|
||||||
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
|
||||||
|
|
||||||
@@ -35,7 +44,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 #archipelago-dev.
|
Voting shall be conducted on Discord in #ap-core-dev.
|
||||||
|
|
||||||
## Dropping out
|
## Dropping out
|
||||||
|
|
||||||
@@ -51,7 +60,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 #archipelago-dev. Commits that are a direct result of the voting shall include
|
Voting shall be conducted on Discord in #ap-core-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,6 +186,11 @@ 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: "";
|
||||||
@@ -223,8 +228,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}\ArchipelagoTextClient.exe,0";
|
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoLauncher.exe,0";
|
||||||
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1""";
|
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1""";
|
||||||
|
|
||||||
[Code]
|
[Code]
|
||||||
// See: https://stackoverflow.com/a/51614652/2287576
|
// See: https://stackoverflow.com/a/51614652/2287576
|
||||||
|
|||||||
9
kvui.py
9
kvui.py
@@ -5,6 +5,8 @@ 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
|
||||||
|
|
||||||
@@ -595,8 +597,9 @@ class GameManager(App):
|
|||||||
"!help for server commands.")
|
"!help for server commands.")
|
||||||
|
|
||||||
def connect_button_action(self, button):
|
def connect_button_action(self, button):
|
||||||
|
self.ctx.username = None
|
||||||
|
self.ctx.password = None
|
||||||
if self.ctx.server:
|
if self.ctx.server:
|
||||||
self.ctx.username = None
|
|
||||||
async_start(self.ctx.disconnect())
|
async_start(self.ctx.disconnect())
|
||||||
else:
|
else:
|
||||||
async_start(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", "")))
|
async_start(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", "")))
|
||||||
@@ -836,6 +839,10 @@ class KivyJSONtoTextParser(JSONtoTextParser):
|
|||||||
return self._handle_text(node)
|
return self._handle_text(node)
|
||||||
|
|
||||||
def _handle_text(self, node: JSONMessagePart):
|
def _handle_text(self, node: JSONMessagePart):
|
||||||
|
# All other text goes through _handle_color, and we don't want to escape markup twice,
|
||||||
|
# or mess up text that already has intentional markup applied to it
|
||||||
|
if node.get("type", "text") == "text":
|
||||||
|
node["text"] = escape_markup(node["text"])
|
||||||
for ref in node.get("refs", []):
|
for ref in node.get("refs", []):
|
||||||
node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]"
|
node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]"
|
||||||
self.ref_count += 1
|
self.ref_count += 1
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
colorama>=0.4.6
|
colorama>=0.4.6
|
||||||
websockets>=12.0
|
websockets>=13.0.1
|
||||||
PyYAML>=6.0.1
|
PyYAML>=6.0.2
|
||||||
jellyfish>=1.0.3
|
jellyfish>=1.1.0
|
||||||
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.6.2
|
certifi>=2024.8.30
|
||||||
cython>=3.0.10
|
cython>=3.0.11
|
||||||
cymem>=2.0.8
|
cymem>=2.0.8
|
||||||
orjson>=3.10.3
|
orjson>=3.10.7
|
||||||
typing_extensions>=4.12.1
|
typing_extensions>=4.12.2
|
||||||
|
|||||||
18
settings.py
18
settings.py
@@ -3,6 +3,7 @@ Application settings / host.yaml interface using type hints.
|
|||||||
This is different from player options.
|
This is different from player options.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
@@ -11,7 +12,6 @@ import warnings
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import cast, Any, BinaryIO, ClassVar, Dict, Iterator, List, Optional, TextIO, Tuple, Union, TypeVar
|
from typing import cast, Any, BinaryIO, ClassVar, Dict, Iterator, List, Optional, TextIO, Tuple, Union, TypeVar
|
||||||
import os
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"get_settings", "fmt_doc", "no_gui",
|
"get_settings", "fmt_doc", "no_gui",
|
||||||
@@ -798,6 +798,7 @@ class Settings(Group):
|
|||||||
atexit.register(autosave)
|
atexit.register(autosave)
|
||||||
|
|
||||||
def save(self, location: Optional[str] = None) -> None: # as above
|
def save(self, location: Optional[str] = None) -> None: # as above
|
||||||
|
from Utils import parse_yaml
|
||||||
location = location or self._filename
|
location = location or self._filename
|
||||||
assert location, "No file specified"
|
assert location, "No file specified"
|
||||||
temp_location = location + ".tmp" # not using tempfile to test expected file access
|
temp_location = location + ".tmp" # not using tempfile to test expected file access
|
||||||
@@ -807,10 +808,18 @@ class Settings(Group):
|
|||||||
# can't use utf-8-sig because it breaks backward compat: pyyaml on Windows with bytes does not strip the BOM
|
# can't use utf-8-sig because it breaks backward compat: pyyaml on Windows with bytes does not strip the BOM
|
||||||
with open(temp_location, "w", encoding="utf-8") as f:
|
with open(temp_location, "w", encoding="utf-8") as f:
|
||||||
self.dump(f)
|
self.dump(f)
|
||||||
# replace old with new
|
f.flush()
|
||||||
if os.path.exists(location):
|
if hasattr(os, "fsync"):
|
||||||
|
os.fsync(f.fileno())
|
||||||
|
# validate new file is valid yaml
|
||||||
|
with open(temp_location, encoding="utf-8") as f:
|
||||||
|
parse_yaml(f.read())
|
||||||
|
# replace old with new, try atomic operation first
|
||||||
|
try:
|
||||||
|
os.rename(temp_location, location)
|
||||||
|
except (OSError, FileExistsError):
|
||||||
os.unlink(location)
|
os.unlink(location)
|
||||||
os.rename(temp_location, location)
|
os.rename(temp_location, location)
|
||||||
self._filename = location
|
self._filename = location
|
||||||
|
|
||||||
def dump(self, f: TextIO, level: int = 0) -> None:
|
def dump(self, f: TextIO, level: int = 0) -> None:
|
||||||
@@ -832,7 +841,6 @@ def get_settings() -> Settings:
|
|||||||
with _lock: # make sure we only have one instance
|
with _lock: # make sure we only have one instance
|
||||||
res = getattr(get_settings, "_cache", None)
|
res = getattr(get_settings, "_cache", None)
|
||||||
if not res:
|
if not res:
|
||||||
import os
|
|
||||||
from Utils import user_path, local_path
|
from Utils import user_path, local_path
|
||||||
filenames = ("options.yaml", "host.yaml")
|
filenames = ("options.yaml", "host.yaml")
|
||||||
locations: List[str] = []
|
locations: List[str] = []
|
||||||
|
|||||||
3
setup.py
3
setup.py
@@ -21,7 +21,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||||
try:
|
try:
|
||||||
requirement = 'cx-Freeze==7.0.0'
|
requirement = 'cx-Freeze==7.2.0'
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
try:
|
try:
|
||||||
pkg_resources.require(requirement)
|
pkg_resources.require(requirement)
|
||||||
@@ -66,7 +66,6 @@ non_apworlds: set = {
|
|||||||
"Adventure",
|
"Adventure",
|
||||||
"ArchipIDLE",
|
"ArchipIDLE",
|
||||||
"Archipelago",
|
"Archipelago",
|
||||||
"ChecksFinder",
|
|
||||||
"Clique",
|
"Clique",
|
||||||
"Final Fantasy",
|
"Final Fantasy",
|
||||||
"Lufia II Ancient Cave",
|
"Lufia II Ancient Cave",
|
||||||
|
|||||||
@@ -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, event=True)
|
state.collect(item, prevent_sweep=True)
|
||||||
state.sweep_for_events()
|
state.sweep_for_advancements()
|
||||||
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.events:
|
if item.location and item.advancement and item.location in self.multiworld.state.advancements:
|
||||||
self.multiworld.state.events.remove(item.location)
|
self.multiworld.state.advancements.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,13 +293,11 @@ 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():
|
||||||
if location.name not in excluded:
|
with self.subTest("Location should be reached", location=location.name):
|
||||||
with self.subTest("Location should be reached", location=location.name):
|
reachable = location.can_reach(state)
|
||||||
reachable = location.can_reach(state)
|
self.assertTrue(reachable, f"{location.name} unreachable")
|
||||||
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)
|
||||||
|
|||||||
@@ -174,8 +174,8 @@ class TestFillRestrictive(unittest.TestCase):
|
|||||||
player1 = generate_player_data(multiworld, 1, 3, 3)
|
player1 = generate_player_data(multiworld, 1, 3, 3)
|
||||||
player2 = generate_player_data(multiworld, 2, 3, 3)
|
player2 = generate_player_data(multiworld, 2, 3, 3)
|
||||||
|
|
||||||
multiworld.accessibility[player1.id].value = multiworld.accessibility[player1.id].option_minimal
|
multiworld.worlds[player1.id].options.accessibility.value = Accessibility.option_minimal
|
||||||
multiworld.accessibility[player2.id].value = multiworld.accessibility[player2.id].option_locations
|
multiworld.worlds[player2.id].options.accessibility.value = Accessibility.option_full
|
||||||
|
|
||||||
multiworld.completion_condition[player1.id] = lambda state: True
|
multiworld.completion_condition[player1.id] = lambda state: True
|
||||||
multiworld.completion_condition[player2.id] = lambda state: state.has(player2.prog_items[2].name, player2.id)
|
multiworld.completion_condition[player2.id] = lambda state: state.has(player2.prog_items[2].name, player2.id)
|
||||||
@@ -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_events() # collect everything
|
multiworld.state.sweep_for_advancements() # 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_events()
|
multiworld.state.sweep_for_advancements()
|
||||||
multiworld.state.sweep_for_events()
|
multiworld.state.sweep_for_advancements()
|
||||||
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")
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import os
|
import os
|
||||||
|
import os.path
|
||||||
import unittest
|
import unittest
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from tempfile import TemporaryFile
|
from tempfile import TemporaryDirectory, TemporaryFile
|
||||||
from typing import Any, Dict, List, cast
|
from typing import Any, Dict, List, cast
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
from settings import Settings, Group
|
from settings import Group, Settings, ServerOptions
|
||||||
|
|
||||||
|
|
||||||
class TestIDs(unittest.TestCase):
|
class TestIDs(unittest.TestCase):
|
||||||
@@ -80,3 +81,27 @@ class TestSettingsDumper(unittest.TestCase):
|
|||||||
self.assertEqual(value_spaces[2], value_spaces[0]) # start of sub-list
|
self.assertEqual(value_spaces[2], value_spaces[0]) # start of sub-list
|
||||||
self.assertGreater(value_spaces[3], value_spaces[0],
|
self.assertGreater(value_spaces[3], value_spaces[0],
|
||||||
f"{value_lines[3]} should have more indentation than {value_lines[0]} in {lines}")
|
f"{value_lines[3]} should have more indentation than {value_lines[0]} in {lines}")
|
||||||
|
|
||||||
|
|
||||||
|
class TestSettingsSave(unittest.TestCase):
|
||||||
|
def test_save(self) -> None:
|
||||||
|
"""Test that saving and updating works"""
|
||||||
|
with TemporaryDirectory() as d:
|
||||||
|
filename = os.path.join(d, "host.yaml")
|
||||||
|
new_release_mode = ServerOptions.ReleaseMode("enabled")
|
||||||
|
# create default host.yaml
|
||||||
|
settings = Settings(None)
|
||||||
|
settings.save(filename)
|
||||||
|
self.assertTrue(os.path.exists(filename),
|
||||||
|
"Default settings could not be saved")
|
||||||
|
self.assertNotEqual(settings.server_options.release_mode, new_release_mode,
|
||||||
|
"Unexpected default release mode")
|
||||||
|
# update host.yaml
|
||||||
|
settings.server_options.release_mode = new_release_mode
|
||||||
|
settings.save(filename)
|
||||||
|
self.assertFalse(os.path.exists(filename + ".tmp"),
|
||||||
|
"Temp file was not removed during save")
|
||||||
|
# read back host.yaml
|
||||||
|
settings = Settings(filename)
|
||||||
|
self.assertEqual(settings.server_options.release_mode, new_release_mode,
|
||||||
|
"Settings were not overwritten")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from BaseClasses import PlandoOptions
|
from BaseClasses import MultiWorld, PlandoOptions
|
||||||
from Options import ItemLinks
|
from Options import ItemLinks
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
@@ -47,3 +47,15 @@ class TestOptions(unittest.TestCase):
|
|||||||
self.assertIn("Bow", link.value[0]["item_pool"])
|
self.assertIn("Bow", link.value[0]["item_pool"])
|
||||||
|
|
||||||
# TODO test that the group created using these options has the items
|
# TODO test that the group created using these options has the items
|
||||||
|
|
||||||
|
def test_item_links_resolve(self):
|
||||||
|
"""Test item link option resolves correctly."""
|
||||||
|
item_link_group = [{
|
||||||
|
"name": "ItemLinkTest",
|
||||||
|
"item_pool": ["Everything"],
|
||||||
|
"link_replacement": False,
|
||||||
|
"replacement_item": None,
|
||||||
|
}]
|
||||||
|
item_links = {1: ItemLinks.from_any(item_link_group), 2: ItemLinks.from_any(item_link_group)}
|
||||||
|
for link in item_links.values():
|
||||||
|
self.assertEqual(link.value[0], item_link_group[0])
|
||||||
|
|||||||
@@ -14,6 +14,18 @@ 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
|
||||||
@@ -37,12 +49,10 @@ 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():
|
||||||
if location.name not in excluded:
|
with self.subTest("Location should be reached", location=location.name):
|
||||||
with self.subTest("Location should be reached", location=location.name):
|
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
|
||||||
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_locations
|
world.options.accessibility.value = Accessibility.option_full
|
||||||
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,10 +66,10 @@ 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 in AutoWorldRegister.world_types.values():
|
for world_type in AutoWorldRegister.world_types.values():
|
||||||
self.multiworld = setup_multiworld([world, world], ())
|
self.multiworld = setup_multiworld([world_type, world_type], ())
|
||||||
for world in self.multiworld.worlds.values():
|
for world in self.multiworld.worlds.values():
|
||||||
world.options.accessibility.value = Accessibility.option_locations
|
world.options.accessibility.value = Accessibility.option_full
|
||||||
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)
|
||||||
|
|||||||
@@ -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), [13, 21])
|
self.assertEqual(self.store.get_remaining(one_state, 0, 1), [(1, 13), (2, 21)])
|
||||||
self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [13, 21, 22])
|
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, 3), [99])
|
self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [(4, 99)])
|
||||||
|
|
||||||
def test_location_set_intersection(self) -> None:
|
def test_location_set_intersection(self) -> None:
|
||||||
locations = {10, 11, 12}
|
locations = {10, 11, 12}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import unittest
|
||||||
|
import typing
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask.testing import FlaskClient
|
||||||
|
|
||||||
|
|
||||||
|
class TestBase(unittest.TestCase):
|
||||||
|
app: typing.ClassVar[Flask]
|
||||||
|
client: FlaskClient
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
from WebHostLib import app as raw_app
|
||||||
|
from WebHost import get_app
|
||||||
|
|
||||||
|
raw_app.config["PONY"] = {
|
||||||
|
"provider": "sqlite",
|
||||||
|
"filename": ":memory:",
|
||||||
|
"create_db": True,
|
||||||
|
}
|
||||||
|
raw_app.config.update({
|
||||||
|
"TESTING": True,
|
||||||
|
"DEBUG": True,
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
cls.app = get_app()
|
||||||
|
except AssertionError as e:
|
||||||
|
# since we only have 1 global app object, this might fail, but luckily all tests use the same config
|
||||||
|
if "register_blueprint" not in e.args[0]:
|
||||||
|
raise
|
||||||
|
cls.app = raw_app
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.client = self.app.test_client()
|
||||||
|
|||||||
@@ -1,31 +1,16 @@
|
|||||||
import io
|
import io
|
||||||
import unittest
|
|
||||||
import json
|
import json
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from . import TestBase
|
||||||
|
|
||||||
class TestDocs(unittest.TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls) -> None:
|
|
||||||
from WebHostLib import app as raw_app
|
|
||||||
from WebHost import get_app
|
|
||||||
raw_app.config["PONY"] = {
|
|
||||||
"provider": "sqlite",
|
|
||||||
"filename": ":memory:",
|
|
||||||
"create_db": True,
|
|
||||||
}
|
|
||||||
raw_app.config.update({
|
|
||||||
"TESTING": True,
|
|
||||||
})
|
|
||||||
app = get_app()
|
|
||||||
|
|
||||||
cls.client = app.test_client()
|
class TestAPIGenerate(TestBase):
|
||||||
|
def test_correct_error_empty_request(self) -> None:
|
||||||
def test_correct_error_empty_request(self):
|
|
||||||
response = self.client.post("/api/generate")
|
response = self.client.post("/api/generate")
|
||||||
self.assertIn("No options found. Expected file attachment or json weights.", response.text)
|
self.assertIn("No options found. Expected file attachment or json weights.", response.text)
|
||||||
|
|
||||||
def test_generation_queued_weights(self):
|
def test_generation_queued_weights(self) -> None:
|
||||||
options = {
|
options = {
|
||||||
"Tester1":
|
"Tester1":
|
||||||
{
|
{
|
||||||
@@ -43,7 +28,7 @@ class TestDocs(unittest.TestCase):
|
|||||||
self.assertTrue(json_data["text"].startswith("Generation of seed "))
|
self.assertTrue(json_data["text"].startswith("Generation of seed "))
|
||||||
self.assertTrue(json_data["text"].endswith(" started successfully."))
|
self.assertTrue(json_data["text"].endswith(" started successfully."))
|
||||||
|
|
||||||
def test_generation_queued_file(self):
|
def test_generation_queued_file(self) -> None:
|
||||||
options = {
|
options = {
|
||||||
"game": "Archipelago",
|
"game": "Archipelago",
|
||||||
"name": "Tester",
|
"name": "Tester",
|
||||||
|
|||||||
192
test/webhost/test_host_room.py
Normal file
192
test/webhost/test_host_room.py
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import os
|
||||||
|
from uuid import UUID, uuid4, uuid5
|
||||||
|
|
||||||
|
from flask import url_for
|
||||||
|
|
||||||
|
from . import TestBase
|
||||||
|
|
||||||
|
|
||||||
|
class TestHostFakeRoom(TestBase):
|
||||||
|
room_id: UUID
|
||||||
|
log_filename: str
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
from pony.orm import db_session
|
||||||
|
from Utils import user_path
|
||||||
|
from WebHostLib.models import Room, Seed
|
||||||
|
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
with self.client.session_transaction() as session:
|
||||||
|
session["_id"] = uuid4()
|
||||||
|
with db_session:
|
||||||
|
# create an empty seed and a room from it
|
||||||
|
seed = Seed(multidata=b"", owner=session["_id"])
|
||||||
|
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
|
||||||
|
self.room_id = room.id
|
||||||
|
self.log_filename = user_path("logs", f"{self.room_id}.txt")
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
from pony.orm import db_session, select
|
||||||
|
from WebHostLib.models import Command, Room
|
||||||
|
|
||||||
|
with db_session:
|
||||||
|
for command in select(command for command in Command if command.room.id == self.room_id): # type: ignore
|
||||||
|
command.delete()
|
||||||
|
room: Room = Room.get(id=self.room_id)
|
||||||
|
room.seed.delete()
|
||||||
|
room.delete()
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.unlink(self.log_filename)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_display_log_missing_full(self) -> None:
|
||||||
|
"""
|
||||||
|
Verify that we get a 200 response even if log is missing.
|
||||||
|
This is required to not get an error for fetch.
|
||||||
|
"""
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
response = self.client.get(url_for("display_log", room=self.room_id))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_display_log_missing_range(self) -> None:
|
||||||
|
"""
|
||||||
|
Verify that we get a full response for missing log even if we asked for range.
|
||||||
|
This is required for the JS logic to differentiate between log update and log error message.
|
||||||
|
"""
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
response = self.client.get(url_for("display_log", room=self.room_id), headers={
|
||||||
|
"Range": "bytes=100-"
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_display_log_denied(self) -> None:
|
||||||
|
"""Verify that only the owner can see the log."""
|
||||||
|
other_client = self.app.test_client()
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
response = other_client.get(url_for("display_log", room=self.room_id))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_display_log_missing_room(self) -> None:
|
||||||
|
"""Verify log for missing room gives an error as opposed to missing log for existing room."""
|
||||||
|
missing_room_id = uuid5(uuid4(), "") # rooms are always uuid4, so this can't exist
|
||||||
|
other_client = self.app.test_client()
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
response = other_client.get(url_for("display_log", room=missing_room_id))
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_display_log_full(self) -> None:
|
||||||
|
"""Verify full log response."""
|
||||||
|
with open(self.log_filename, "w", encoding="utf-8") as f:
|
||||||
|
text = "x" * 200
|
||||||
|
f.write(text)
|
||||||
|
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
response = self.client.get(url_for("display_log", room=self.room_id))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.get_data(True), text)
|
||||||
|
|
||||||
|
def test_display_log_range(self) -> None:
|
||||||
|
"""Verify that Range header in request gives a range in response."""
|
||||||
|
with open(self.log_filename, "w", encoding="utf-8") as f:
|
||||||
|
f.write(" " * 100)
|
||||||
|
text = "x" * 100
|
||||||
|
f.write(text)
|
||||||
|
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
response = self.client.get(url_for("display_log", room=self.room_id), headers={
|
||||||
|
"Range": "bytes=100-"
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, 206)
|
||||||
|
self.assertEqual(response.get_data(True), text)
|
||||||
|
|
||||||
|
def test_display_log_range_bom(self) -> None:
|
||||||
|
"""Verify that a BOM in the log file is skipped for range."""
|
||||||
|
with open(self.log_filename, "w", encoding="utf-8-sig") as f:
|
||||||
|
f.write(" " * 100)
|
||||||
|
text = "x" * 100
|
||||||
|
f.write(text)
|
||||||
|
self.assertEqual(f.tell(), 203) # including BOM
|
||||||
|
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
response = self.client.get(url_for("display_log", room=self.room_id), headers={
|
||||||
|
"Range": "bytes=100-"
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, 206)
|
||||||
|
self.assertEqual(response.get_data(True), text)
|
||||||
|
|
||||||
|
def test_host_room_missing(self) -> None:
|
||||||
|
"""Verify that missing room gives a 404 response."""
|
||||||
|
missing_room_id = uuid5(uuid4(), "") # rooms are always uuid4, so this can't exist
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
response = self.client.get(url_for("host_room", room=missing_room_id))
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_host_room_own(self) -> None:
|
||||||
|
"""Verify that own room gives the full output."""
|
||||||
|
with open(self.log_filename, "w", encoding="utf-8-sig") as f:
|
||||||
|
text = "* should be visible *"
|
||||||
|
f.write(text)
|
||||||
|
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
response = self.client.get(url_for("host_room", room=self.room_id))
|
||||||
|
response_text = response.get_data(True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn("href=\"/seed/", response_text)
|
||||||
|
self.assertIn(text, response_text)
|
||||||
|
|
||||||
|
def test_host_room_other(self) -> None:
|
||||||
|
"""Verify that non-own room gives the reduced output."""
|
||||||
|
from pony.orm import db_session
|
||||||
|
from WebHostLib.models import Room
|
||||||
|
|
||||||
|
with db_session:
|
||||||
|
room: Room = Room.get(id=self.room_id)
|
||||||
|
room.last_port = 12345
|
||||||
|
|
||||||
|
with open(self.log_filename, "w", encoding="utf-8-sig") as f:
|
||||||
|
text = "* should not be visible *"
|
||||||
|
f.write(text)
|
||||||
|
|
||||||
|
other_client = self.app.test_client()
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
response = other_client.get(url_for("host_room", room=self.room_id))
|
||||||
|
response_text = response.get_data(True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertNotIn("href=\"/seed/", response_text)
|
||||||
|
self.assertNotIn(text, response_text)
|
||||||
|
self.assertIn("/connect ", response_text)
|
||||||
|
self.assertIn(":12345", response_text)
|
||||||
|
|
||||||
|
def test_host_room_own_post(self) -> None:
|
||||||
|
"""Verify command from owner gets queued for the server and response is redirect."""
|
||||||
|
from pony.orm import db_session, select
|
||||||
|
from WebHostLib.models import Command
|
||||||
|
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
response = self.client.post(url_for("host_room", room=self.room_id), data={
|
||||||
|
"cmd": "/help"
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, 302, response.text)\
|
||||||
|
|
||||||
|
with db_session:
|
||||||
|
commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore
|
||||||
|
self.assertIn("/help", (command.commandtext for command in commands))
|
||||||
|
|
||||||
|
def test_host_room_other_post(self) -> None:
|
||||||
|
"""Verify command from non-owner does not get queued for the server."""
|
||||||
|
from pony.orm import db_session, select
|
||||||
|
from WebHostLib.models import Command
|
||||||
|
|
||||||
|
other_client = self.app.test_client()
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
response = other_client.post(url_for("host_room", room=self.room_id), data={
|
||||||
|
"cmd": "/help"
|
||||||
|
})
|
||||||
|
self.assertLess(response.status_code, 500)
|
||||||
|
|
||||||
|
with db_session:
|
||||||
|
commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore
|
||||||
|
self.assertNotIn("/help", (command.commandtext for command in commands))
|
||||||
@@ -292,6 +292,14 @@ 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,10 +26,13 @@ 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
|
||||||
@@ -45,6 +48,8 @@ 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
|
||||||
@@ -56,10 +61,10 @@ class Component:
|
|||||||
processes = weakref.WeakSet()
|
processes = weakref.WeakSet()
|
||||||
|
|
||||||
|
|
||||||
def launch_subprocess(func: Callable, name: str = None):
|
def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None:
|
||||||
global processes
|
global processes
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
process = multiprocessing.Process(target=func, name=name)
|
process = multiprocessing.Process(target=func, name=name, args=args)
|
||||||
process.start()
|
process.start()
|
||||||
processes.add(process)
|
processes.add(process)
|
||||||
|
|
||||||
@@ -78,9 +83,9 @@ class SuffixIdentifier:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def launch_textclient():
|
def launch_textclient(*args):
|
||||||
import CommonClient
|
import CommonClient
|
||||||
launch_subprocess(CommonClient.run_as_textclient, name="TextClient")
|
launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args)
|
||||||
|
|
||||||
|
|
||||||
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
|
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
|
||||||
@@ -132,7 +137,8 @@ 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.")
|
"so a Launcher restart is required to use the new installation.\n"
|
||||||
|
"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,7 +73,12 @@ 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])
|
||||||
|
|
||||||
mod.__package__ = f"worlds.{mod.__package__}"
|
if mod.__package__ is not None:
|
||||||
|
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():
|
||||||
|
|||||||
@@ -59,14 +59,10 @@ class BizHawkClientContext(CommonContext):
|
|||||||
self.bizhawk_ctx = BizHawkContext()
|
self.bizhawk_ctx = BizHawkContext()
|
||||||
self.watcher_timeout = 0.5
|
self.watcher_timeout = 0.5
|
||||||
|
|
||||||
def run_gui(self):
|
def make_gui(self):
|
||||||
from kvui import GameManager
|
ui = super().make_gui()
|
||||||
|
ui.base_title = "Archipelago BizHawk Client"
|
||||||
class BizHawkManager(GameManager):
|
return ui
|
||||||
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":
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]:
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
if name == "Scooter Badge":
|
if name == "Scooter Badge":
|
||||||
if world.options.CTRLogic is CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE:
|
if world.options.CTRLogic == CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE:
|
||||||
item_type = ItemClassification.progression
|
item_type = ItemClassification.progression
|
||||||
elif name == "No Bonk Badge" and world.is_dw():
|
elif name == "No Bonk Badge" and world.is_dw():
|
||||||
item_type = ItemClassification.progression
|
item_type = ItemClassification.progression
|
||||||
|
|||||||
@@ -292,6 +292,9 @@ blacklisted_combos = {
|
|||||||
# See above comment
|
# See above comment
|
||||||
"Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations",
|
"Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations",
|
||||||
"Murder on the Owl Express"],
|
"Murder on the Owl Express"],
|
||||||
|
|
||||||
|
# was causing test failures
|
||||||
|
"Time Rift - Balcony": ["Alpine Free Roam"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -656,6 +659,10 @@ def is_valid_act_combo(world: "HatInTimeWorld", entrance_act: Region,
|
|||||||
if exit_act.name not in chapter_finales:
|
if exit_act.name not in chapter_finales:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
exit_chapter: str = act_chapters.get(exit_act.name)
|
||||||
|
# make sure that certain time rift combinations never happen
|
||||||
|
always_block: bool = exit_chapter != "Mafia Town" and exit_chapter != "Subcon Forest"
|
||||||
|
if not ignore_certain_rules or always_block:
|
||||||
if entrance_act.name in rift_access_regions and exit_act.name in rift_access_regions[entrance_act.name]:
|
if entrance_act.name in rift_access_regions and exit_act.name in rift_access_regions[entrance_act.name]:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -681,9 +688,12 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool:
|
|||||||
if act.name not in guaranteed_first_acts:
|
if act.name not in guaranteed_first_acts:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if world.options.ActRandomizer == ActRandomizer.option_light and "Time Rift" in act.name:
|
||||||
|
return False
|
||||||
|
|
||||||
# If there's only a single level in the starting chapter, only allow Mafia Town or Subcon Forest levels
|
# If there's only a single level in the starting chapter, only allow Mafia Town or Subcon Forest levels
|
||||||
start_chapter = world.options.StartingChapter
|
start_chapter = world.options.StartingChapter
|
||||||
if start_chapter is ChapterIndex.ALPINE or start_chapter is ChapterIndex.SUBCON:
|
if start_chapter == ChapterIndex.ALPINE or start_chapter == ChapterIndex.SUBCON:
|
||||||
if "Time Rift" in act.name:
|
if "Time Rift" in act.name:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -720,7 +730,8 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool:
|
|||||||
elif act.name == "Contractual Obligations" and world.options.ShuffleSubconPaintings:
|
elif act.name == "Contractual Obligations" and world.options.ShuffleSubconPaintings:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if world.options.ShuffleSubconPaintings and act_chapters.get(act.name, "") == "Subcon Forest":
|
if world.options.ShuffleSubconPaintings and "Time Rift" not in act.name \
|
||||||
|
and act_chapters.get(act.name, "") == "Subcon Forest":
|
||||||
# Only allow Subcon levels if painting skips are allowed
|
# Only allow Subcon levels if painting skips are allowed
|
||||||
if diff < Difficulty.MODERATE or world.options.NoPaintingSkips:
|
if diff < Difficulty.MODERATE or world.options.NoPaintingSkips:
|
||||||
return False
|
return False
|
||||||
@@ -957,40 +968,35 @@ 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
|
|
||||||
step = 0
|
thug_location_counts: Dict[str, int] = {}
|
||||||
old_name = ""
|
|
||||||
|
|
||||||
for key, data in shop_locations.items():
|
for key, data in shop_locations.items():
|
||||||
if data.nyakuza_thug == "":
|
thug_name = data.nyakuza_thug
|
||||||
|
if thug_name == "":
|
||||||
|
# Different shop type.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if old_name != "" and old_name == data.nyakuza_thug:
|
if thug_name not in world.nyakuza_thug_items:
|
||||||
|
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
|
||||||
|
|
||||||
try:
|
location_count = thug_location_counts.setdefault(thug_name, 0)
|
||||||
if world.nyakuza_thug_items[data.nyakuza_thug] <= 0:
|
if location_count >= shop_item_count:
|
||||||
continue
|
# Already created all the locations for this thug.
|
||||||
except KeyError:
|
continue
|
||||||
pass
|
|
||||||
|
|
||||||
if count == -1:
|
# Create the shop location.
|
||||||
count = world.random.randint(min_items, max_items)
|
region = world.multiworld.get_region(data.region, world.player)
|
||||||
world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count)
|
loc = HatInTimeLocation(world.player, key, data.id, region)
|
||||||
if count <= 0:
|
region.locations.append(loc)
|
||||||
continue
|
world.shop_locs.append(loc.name)
|
||||||
|
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:
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from worlds.AutoWorld import CollectionState
|
from worlds.AutoWorld import CollectionState
|
||||||
from worlds.generic.Rules import add_rule, set_rule
|
from worlds.generic.Rules import add_rule, set_rule
|
||||||
from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \
|
from .Locations import location_table, zipline_unlocks, is_location_valid, shop_locations, event_locs
|
||||||
shop_locations, event_locs
|
|
||||||
from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HitType
|
from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HitType
|
||||||
from BaseClasses import Location, Entrance, Region
|
from BaseClasses import Location, Entrance, Region
|
||||||
from typing import TYPE_CHECKING, List, Callable, Union, Dict
|
from typing import TYPE_CHECKING, List, Callable, Union, Dict
|
||||||
@@ -148,14 +147,14 @@ def set_rules(world: "HatInTimeWorld"):
|
|||||||
if world.is_dlc1():
|
if world.is_dlc1():
|
||||||
chapter_list.append(ChapterIndex.CRUISE)
|
chapter_list.append(ChapterIndex.CRUISE)
|
||||||
|
|
||||||
if world.is_dlc2() and final_chapter is not ChapterIndex.METRO:
|
if world.is_dlc2() and final_chapter != ChapterIndex.METRO:
|
||||||
chapter_list.append(ChapterIndex.METRO)
|
chapter_list.append(ChapterIndex.METRO)
|
||||||
|
|
||||||
chapter_list.remove(starting_chapter)
|
chapter_list.remove(starting_chapter)
|
||||||
world.random.shuffle(chapter_list)
|
world.random.shuffle(chapter_list)
|
||||||
|
|
||||||
# Make sure Alpine is unlocked before any DLC chapters are, as the Alpine door needs to be open to access them
|
# Make sure Alpine is unlocked before any DLC chapters are, as the Alpine door needs to be open to access them
|
||||||
if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()):
|
if starting_chapter != ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()):
|
||||||
index1 = 69
|
index1 = 69
|
||||||
index2 = 69
|
index2 = 69
|
||||||
pos: int
|
pos: int
|
||||||
@@ -165,7 +164,7 @@ def set_rules(world: "HatInTimeWorld"):
|
|||||||
if world.is_dlc1():
|
if world.is_dlc1():
|
||||||
index1 = chapter_list.index(ChapterIndex.CRUISE)
|
index1 = chapter_list.index(ChapterIndex.CRUISE)
|
||||||
|
|
||||||
if world.is_dlc2() and final_chapter is not ChapterIndex.METRO:
|
if world.is_dlc2() and final_chapter != ChapterIndex.METRO:
|
||||||
index2 = chapter_list.index(ChapterIndex.METRO)
|
index2 = chapter_list.index(ChapterIndex.METRO)
|
||||||
|
|
||||||
lowest_index = min(index1, index2)
|
lowest_index = min(index1, index2)
|
||||||
@@ -242,9 +241,6 @@ def set_rules(world: "HatInTimeWorld"):
|
|||||||
if not is_location_valid(world, key):
|
if not is_location_valid(world, key):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if key in contract_locations.keys():
|
|
||||||
continue
|
|
||||||
|
|
||||||
loc = world.multiworld.get_location(key, world.player)
|
loc = world.multiworld.get_location(key, world.player)
|
||||||
|
|
||||||
for hat in data.required_hats:
|
for hat in data.required_hats:
|
||||||
@@ -256,7 +252,7 @@ def set_rules(world: "HatInTimeWorld"):
|
|||||||
if data.paintings > 0 and world.options.ShuffleSubconPaintings:
|
if data.paintings > 0 and world.options.ShuffleSubconPaintings:
|
||||||
add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings))
|
add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings))
|
||||||
|
|
||||||
if data.hit_type is not HitType.none and world.options.UmbrellaLogic:
|
if data.hit_type != HitType.none and world.options.UmbrellaLogic:
|
||||||
if data.hit_type == HitType.umbrella:
|
if data.hit_type == HitType.umbrella:
|
||||||
add_rule(loc, lambda state: state.has("Umbrella", world.player))
|
add_rule(loc, lambda state: state.has("Umbrella", world.player))
|
||||||
|
|
||||||
@@ -385,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
|
||||||
add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
|
set_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
|
||||||
add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
|
set_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:
|
||||||
@@ -436,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
|
||||||
add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
|
set_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
|
||||||
add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
|
set_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),
|
||||||
@@ -518,7 +514,7 @@ def set_hard_rules(world: "HatInTimeWorld"):
|
|||||||
lambda state: can_use_hat(state, world, HatType.ICE))
|
lambda state: can_use_hat(state, world, HatType.ICE))
|
||||||
|
|
||||||
# Hard: clear Rush Hour with Brewing Hat only
|
# Hard: clear Rush Hour with Brewing Hat only
|
||||||
if world.options.NoTicketSkips is not NoTicketSkips.option_true:
|
if world.options.NoTicketSkips != NoTicketSkips.option_true:
|
||||||
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
|
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
|
||||||
lambda state: can_use_hat(state, world, HatType.BREWING))
|
lambda state: can_use_hat(state, world, HatType.BREWING))
|
||||||
else:
|
else:
|
||||||
@@ -859,10 +855,15 @@ 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:
|
||||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale"))
|
add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale"))
|
||||||
|
reg_act_connection(world, world.multiworld.get_entrance("The Arctic Cruise - Finale",
|
||||||
|
world.player).connected_region, entrance)
|
||||||
|
|
||||||
for entrance in regions["Time Rift - Deep Sea"].entrances:
|
for entrance in regions["Time Rift - Deep Sea"].entrances:
|
||||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake"))
|
add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake"))
|
||||||
@@ -935,10 +936,14 @@ 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:
|
||||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale"))
|
add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale"))
|
||||||
|
reg_act_connection(world, "Rock the Boat", entrance.name)
|
||||||
|
|
||||||
for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances:
|
for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances:
|
||||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake"))
|
add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake"))
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld
|
from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld
|
||||||
from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \
|
from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \
|
||||||
calculate_yarn_costs
|
calculate_yarn_costs, alps_hooks
|
||||||
from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region
|
from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region
|
||||||
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \
|
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \
|
||||||
get_total_locations
|
get_total_locations
|
||||||
from .Rules import set_rules
|
from .Rules import set_rules, has_paintings
|
||||||
from .Options import AHITOptions, slot_data_options, adjust_options, RandomizeHatOrder, EndGoal, create_option_groups
|
from .Options import AHITOptions, slot_data_options, adjust_options, RandomizeHatOrder, EndGoal, create_option_groups
|
||||||
from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item
|
from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item, Difficulty
|
||||||
from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes
|
from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes
|
||||||
from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses
|
from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses
|
||||||
from worlds.AutoWorld import World, WebWorld, CollectionState
|
from worlds.AutoWorld import World, WebWorld, CollectionState
|
||||||
|
from worlds.generic.Rules import add_rule
|
||||||
from typing import List, Dict, TextIO
|
from typing import List, Dict, TextIO
|
||||||
from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type
|
from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type
|
||||||
from Utils import local_path
|
from Utils import local_path
|
||||||
@@ -86,19 +87,27 @@ class HatInTimeWorld(World):
|
|||||||
if self.is_dw_only():
|
if self.is_dw_only():
|
||||||
return
|
return
|
||||||
|
|
||||||
# If our starting chapter is 4 and act rando isn't on, force hookshot into inventory
|
# Take care of some extremely restrictive starts in other chapters with act shuffle off
|
||||||
# If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock
|
if not self.options.ActRandomizer:
|
||||||
start_chapter: ChapterIndex = ChapterIndex(self.options.StartingChapter)
|
start_chapter = self.options.StartingChapter
|
||||||
|
if start_chapter == ChapterIndex.ALPINE:
|
||||||
|
self.multiworld.push_precollected(self.create_item("Hookshot Badge"))
|
||||||
|
if self.options.UmbrellaLogic:
|
||||||
|
self.multiworld.push_precollected(self.create_item("Umbrella"))
|
||||||
|
|
||||||
if start_chapter == ChapterIndex.ALPINE or start_chapter == ChapterIndex.SUBCON:
|
if self.options.ShuffleAlpineZiplines:
|
||||||
if not self.options.ActRandomizer:
|
ziplines = list(alps_hooks.keys())
|
||||||
if start_chapter == ChapterIndex.ALPINE:
|
ziplines.remove("Zipline Unlock - The Twilight Bell Path") # not enough checks from this one
|
||||||
self.multiworld.push_precollected(self.create_item("Hookshot Badge"))
|
self.multiworld.push_precollected(self.create_item(self.random.choice(ziplines)))
|
||||||
if self.options.UmbrellaLogic:
|
elif start_chapter == ChapterIndex.SUBCON:
|
||||||
self.multiworld.push_precollected(self.create_item("Umbrella"))
|
if self.options.ShuffleSubconPaintings:
|
||||||
|
|
||||||
if start_chapter == ChapterIndex.SUBCON and self.options.ShuffleSubconPaintings:
|
|
||||||
self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock"))
|
self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock"))
|
||||||
|
elif start_chapter == ChapterIndex.BIRDS:
|
||||||
|
if self.options.UmbrellaLogic:
|
||||||
|
if self.options.LogicDifficulty < Difficulty.EXPERT:
|
||||||
|
self.multiworld.push_precollected(self.create_item("Umbrella"))
|
||||||
|
elif self.options.LogicDifficulty < Difficulty.MODERATE:
|
||||||
|
self.multiworld.push_precollected(self.create_item("Umbrella"))
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
# noinspection PyClassVar
|
# noinspection PyClassVar
|
||||||
@@ -119,7 +128,10 @@ class HatInTimeWorld(World):
|
|||||||
# place vanilla contract locations if contract shuffle is off
|
# place vanilla contract locations if contract shuffle is off
|
||||||
if not self.options.ShuffleActContracts:
|
if not self.options.ShuffleActContracts:
|
||||||
for name in contract_locations.keys():
|
for name in contract_locations.keys():
|
||||||
self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name))
|
loc = self.get_location(name)
|
||||||
|
loc.place_locked_item(create_item(self, name))
|
||||||
|
if self.options.ShuffleSubconPaintings and loc.name != "Snatcher's Contract - The Subcon Well":
|
||||||
|
add_rule(loc, lambda state: has_paintings(state, self, 1))
|
||||||
|
|
||||||
def create_items(self):
|
def create_items(self):
|
||||||
if self.has_yarn():
|
if self.has_yarn():
|
||||||
@@ -317,7 +329,7 @@ class HatInTimeWorld(World):
|
|||||||
|
|
||||||
def remove(self, state: "CollectionState", item: "Item") -> bool:
|
def remove(self, state: "CollectionState", item: "Item") -> bool:
|
||||||
old_count: int = state.count(item.name, self.player)
|
old_count: int = state.count(item.name, self.player)
|
||||||
change = super().collect(state, item)
|
change = super().remove(state, item)
|
||||||
if change and old_count == 1:
|
if change and old_count == 1:
|
||||||
if "Stamp" in item.name:
|
if "Stamp" in item.name:
|
||||||
if "2 Stamp" in item.name:
|
if "2 Stamp" in item.name:
|
||||||
|
|||||||
@@ -12,41 +12,29 @@
|
|||||||
|
|
||||||
## Instructions
|
## Instructions
|
||||||
|
|
||||||
1. Have Steam running. Open the Steam console with this link: [steam://open/console](steam://open/console)
|
1. **BACK UP YOUR SAVE FILES IN YOUR MAIN INSTALL IF YOU CARE ABOUT THEM!!!**
|
||||||
This may not work for some browsers. If that's the case, and you're on Windows, open the Run dialog using Win+R,
|
Go to `steamapps/common/HatinTime/HatinTimeGame/SaveData/` and copy everything inside that folder over to a safe place.
|
||||||
paste the link into the box, and hit Enter.
|
**This is important! Changing the game version CAN and WILL break your existing save files!!!**
|
||||||
|
|
||||||
|
|
||||||
2. In the Steam console, enter the following command:
|
2. In your Steam library, right-click on **A Hat in Time** in the list of games and click on **Properties**.
|
||||||
`download_depot 253230 253232 7770543545116491859`. ***Wait for the console to say the download is finished!***
|
|
||||||
This can take a while to finish (30+ minutes) depending on your connection speed, so please be patient. Additionally,
|
|
||||||
**try to prevent your connection from being interrupted or slowed while Steam is downloading the depot,**
|
|
||||||
or else the download may potentially become corrupted (see first FAQ issue below).
|
|
||||||
|
|
||||||
|
|
||||||
3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder.
|
3. Click the **Betas** tab. In the **Beta Participation** dropdown, select `tcplink`.
|
||||||
|
While it downloads, you can subscribe to the [Archipelago workshop mod.]((https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601))
|
||||||
|
|
||||||
|
|
||||||
4. There should be a folder named `depot_253232`. Rename it to HatinTime_AP and move it to your `steamapps/common` folder.
|
4. Once the game finishes downloading, start it up.
|
||||||
|
In Game Settings, make sure **Enable Developer Console** is checked.
|
||||||
|
|
||||||
|
|
||||||
5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`.
|
5. You should now be good to go. See below for more details on how to use the mod and connect to an Archipelago game.
|
||||||
In this new text file, input the number **253230** on the first line.
|
|
||||||
|
|
||||||
|
|
||||||
6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like.
|
|
||||||
You will use this shortcut to open the Archipelago-compatible version of A Hat in Time.
|
|
||||||
|
|
||||||
|
|
||||||
7. Start up the game using your new shortcut. To confirm if you are on the correct version,
|
|
||||||
go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running
|
|
||||||
the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked.
|
|
||||||
|
|
||||||
|
|
||||||
## Connecting to the Archipelago server
|
## Connecting to the Archipelago server
|
||||||
|
|
||||||
To connect to the multiworld server, simply run the **ArchipelagoAHITClient**
|
To connect to the multiworld server, simply run the **Archipelago AHIT Client** from the Launcher
|
||||||
(or run it from the Launcher if you have the apworld installed) and connect it to the Archipelago server.
|
and connect it to the Archipelago server.
|
||||||
The game will connect to the client automatically when you create a new save file.
|
The game will connect to the client automatically when you create a new save file.
|
||||||
|
|
||||||
|
|
||||||
@@ -61,33 +49,8 @@ make sure ***Enable Developer Console*** is checked in Game Settings and press t
|
|||||||
|
|
||||||
|
|
||||||
## FAQ/Common Issues
|
## FAQ/Common Issues
|
||||||
### I followed the setup, but I receive an odd error message upon starting the game or creating a save file!
|
|
||||||
If you receive an error message such as
|
|
||||||
**"Failed to find default engine .ini to retrieve My Documents subdirectory to use. Force quitting."** or
|
|
||||||
**"Failed to load map "hub_spaceship"** after booting up the game or creating a save file respectively, then the depot
|
|
||||||
download was likely corrupted. The only way to fix this is to start the entire download all over again.
|
|
||||||
Unfortunately, this appears to be an underlying issue with Steam's depot downloader. The only way to really prevent this
|
|
||||||
from happening is to ensure that your connection is not interrupted or slowed while downloading.
|
|
||||||
|
|
||||||
### The game keeps crashing on startup after the splash screen!
|
### The game is not connecting when starting a new save!
|
||||||
This issue is unfortunately very hard to fix, and the underlying cause is not known. If it does happen however,
|
|
||||||
try the following:
|
|
||||||
|
|
||||||
- Close Steam **entirely**.
|
|
||||||
- Open the downpatched version of the game (with Steam closed) and allow it to load to the titlescreen.
|
|
||||||
- Close the game, and then open Steam again.
|
|
||||||
- After launching the game, the issue should hopefully disappear. If not, repeat the above steps until it does.
|
|
||||||
|
|
||||||
### I followed the setup, but "Live Game Events" still shows up in the options menu!
|
|
||||||
The most common cause of this is the `steam_appid.txt` file. If you're on Windows 10, file extensions are hidden by
|
|
||||||
default (thanks Microsoft). You likely made the mistake of still naming the file `steam_appid.txt`, which, since file
|
|
||||||
extensions are hidden, would result in the file being named `steam_appid.txt.txt`, which is incorrect.
|
|
||||||
To show file extensions in Windows 10, open any folder, click the View tab at the top, and check
|
|
||||||
"File name extensions". Then you can correct the name of the file. If the name of the file is correct,
|
|
||||||
and you're still running into the issue, re-read the setup guide again in case you missed a step.
|
|
||||||
If you still can't get it to work, ask for help in the Discord thread.
|
|
||||||
|
|
||||||
### The game is running on the older version, but it's not connecting when starting a new save!
|
|
||||||
For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu
|
For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu
|
||||||
(rocket icon) in-game, and re-enable the mod.
|
(rocket icon) in-game, and re-enable the mod.
|
||||||
|
|
||||||
|
|||||||
@@ -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_events()
|
all_state_base.sweep_for_advancements()
|
||||||
|
|
||||||
# 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.events:
|
if loc in all_state_base.advancements:
|
||||||
all_state_base.events.remove(loc)
|
all_state_base.advancements.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_available[player].value
|
treasure_hunt_total = (world.triforce_pieces_required[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
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import typing
|
import typing
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, \
|
from Options import Choice, Range, DeathLink, DefaultOnToggle, FreeText, ItemsAccessibility, Option, \
|
||||||
StartInventoryPool, PlandoBosses, PlandoConnections, PlandoTexts, FreeText, Removed
|
PlandoBosses, PlandoConnections, PlandoTexts, Removed, StartInventoryPool, Toggle
|
||||||
from .EntranceShuffle import default_connections, default_dungeon_connections, \
|
from .EntranceShuffle import default_connections, default_dungeon_connections, \
|
||||||
inverted_default_connections, inverted_default_dungeon_connections
|
inverted_default_connections, inverted_default_dungeon_connections
|
||||||
from .Text import TextTable
|
from .Text import TextTable
|
||||||
@@ -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[1] for connection in (
|
exits = 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)])
|
||||||
|
|
||||||
@@ -743,6 +743,7 @@ class ALttPPlandoTexts(PlandoTexts):
|
|||||||
|
|
||||||
|
|
||||||
alttp_options: typing.Dict[str, type(Option)] = {
|
alttp_options: typing.Dict[str, type(Option)] = {
|
||||||
|
"accessibility": ItemsAccessibility,
|
||||||
"plando_connections": ALttPPlandoConnections,
|
"plando_connections": ALttPPlandoConnections,
|
||||||
"plando_texts": ALttPPlandoTexts,
|
"plando_texts": ALttPPlandoTexts,
|
||||||
"start_inventory_from_pool": StartInventoryPool,
|
"start_inventory_from_pool": StartInventoryPool,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import collections
|
|||||||
import logging
|
import logging
|
||||||
from typing import Iterator, Set
|
from typing import Iterator, Set
|
||||||
|
|
||||||
|
from Options import ItemsAccessibility
|
||||||
from BaseClasses import Entrance, MultiWorld
|
from BaseClasses import Entrance, MultiWorld
|
||||||
from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item,
|
from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item,
|
||||||
item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items)
|
item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items)
|
||||||
@@ -39,7 +40,7 @@ def set_rules(world):
|
|||||||
else:
|
else:
|
||||||
# Set access rules according to max glitches for multiworld progression.
|
# Set access rules according to max glitches for multiworld progression.
|
||||||
# Set accessibility to none, and shuffle assuming the no logic players can always win
|
# Set accessibility to none, and shuffle assuming the no logic players can always win
|
||||||
world.accessibility[player] = world.accessibility[player].from_text("minimal")
|
world.accessibility[player].value = ItemsAccessibility.option_minimal
|
||||||
world.progression_balancing[player].value = 0
|
world.progression_balancing[player].value = 0
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -377,7 +378,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
or state.has("Cane of Somaria", player)))
|
or state.has("Cane of Somaria", player)))
|
||||||
set_rule(multiworld.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
set_rule(multiworld.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
||||||
set_rule(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player))
|
set_rule(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player))
|
||||||
if multiworld.accessibility[player] != 'locations':
|
if multiworld.accessibility[player] != 'full':
|
||||||
set_always_allow(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
|
set_always_allow(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
|
||||||
|
|
||||||
set_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
|
set_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
|
||||||
@@ -393,7 +394,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
if state.has('Hookshot', player)
|
if state.has('Hookshot', player)
|
||||||
else state._lttp_has_key('Small Key (Swamp Palace)', player, 4))
|
else state._lttp_has_key('Small Key (Swamp Palace)', player, 4))
|
||||||
set_rule(multiworld.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
|
set_rule(multiworld.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
|
||||||
if multiworld.accessibility[player] != 'locations':
|
if multiworld.accessibility[player] != 'full':
|
||||||
allow_self_locking_items(multiworld.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
|
allow_self_locking_items(multiworld.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
|
||||||
set_rule(multiworld.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5))
|
set_rule(multiworld.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5))
|
||||||
if not multiworld.small_key_shuffle[player] and multiworld.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']:
|
if not multiworld.small_key_shuffle[player] and multiworld.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']:
|
||||||
@@ -411,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] != 'locations' and not multiworld.key_drop_shuffle[player]:
|
if multiworld.accessibility[player] != 'full' 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),
|
||||||
@@ -423,7 +424,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
set_rule(multiworld.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
set_rule(multiworld.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||||
set_rule(multiworld.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
set_rule(multiworld.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||||
set_rule(multiworld.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) and can_use_bombs(state, player))
|
set_rule(multiworld.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) and can_use_bombs(state, player))
|
||||||
if multiworld.accessibility[player] != 'locations':
|
if multiworld.accessibility[player] != 'full':
|
||||||
allow_self_locking_items(multiworld.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)')
|
allow_self_locking_items(multiworld.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)')
|
||||||
set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
|
set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
|
||||||
add_rule(multiworld.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
add_rule(multiworld.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||||
@@ -488,7 +489,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
set_rule(multiworld.get_location('Turtle Rock - Roller Room - Right', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
|
set_rule(multiworld.get_location('Turtle Rock - Roller Room - Right', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
|
||||||
set_rule(multiworld.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player)))
|
set_rule(multiworld.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player)))
|
||||||
set_rule(multiworld.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
|
set_rule(multiworld.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
|
||||||
set_rule(multiworld.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10))
|
set_rule(multiworld.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10) and can_bomb_or_bonk(state, player))
|
||||||
set_rule(multiworld.get_location('Turtle Rock - Chain Chomps', player), lambda state: can_use_bombs(state, player) or can_shoot_arrows(state, player)
|
set_rule(multiworld.get_location('Turtle Rock - Chain Chomps', player), lambda state: can_use_bombs(state, player) or can_shoot_arrows(state, player)
|
||||||
or has_beam_sword(state, player) or state.has_any(["Blue Boomerang", "Red Boomerang", "Hookshot", "Cane of Somaria", "Fire Rod", "Ice Rod"], player))
|
or has_beam_sword(state, player) or state.has_any(["Blue Boomerang", "Red Boomerang", "Hookshot", "Cane of Somaria", "Fire Rod", "Ice Rod"], player))
|
||||||
set_rule(multiworld.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player))
|
set_rule(multiworld.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player))
|
||||||
@@ -522,12 +523,12 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
|
|
||||||
set_rule(multiworld.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
set_rule(multiworld.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||||
location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3))))
|
location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3))))
|
||||||
if multiworld.accessibility[player] != 'locations':
|
if multiworld.accessibility[player] != 'full':
|
||||||
set_always_allow(multiworld.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
set_always_allow(multiworld.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||||
|
|
||||||
set_rule(multiworld.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
set_rule(multiworld.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||||
location_item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
|
location_item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
|
||||||
if multiworld.accessibility[player] != 'locations':
|
if multiworld.accessibility[player] != 'full':
|
||||||
set_always_allow(multiworld.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
set_always_allow(multiworld.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||||
|
|
||||||
set_rule(multiworld.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
|
set_rule(multiworld.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
|
||||||
@@ -546,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] != 'locations':
|
# if world.accessibility[player] != 'full':
|
||||||
# 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.
|
||||||
@@ -1200,7 +1201,7 @@ def set_trock_key_rules(world, player):
|
|||||||
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
|
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
|
||||||
forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
|
forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
|
||||||
forbid_item(world.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
|
forbid_item(world.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
|
||||||
if world.accessibility[player] == 'locations':
|
if world.accessibility[player] == 'full':
|
||||||
if world.big_key_shuffle[player] and can_reach_big_chest:
|
if world.big_key_shuffle[player] and can_reach_big_chest:
|
||||||
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
|
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
|
||||||
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
|
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
|
||||||
@@ -1214,7 +1215,7 @@ def set_trock_key_rules(world, player):
|
|||||||
location.place_locked_item(item)
|
location.place_locked_item(item)
|
||||||
toss_junk_item(world, player)
|
toss_junk_item(world, player)
|
||||||
|
|
||||||
if world.accessibility[player] != 'locations':
|
if world.accessibility[player] != 'full':
|
||||||
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
|
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
|
||||||
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
|
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
|
||||||
|
|
||||||
|
|||||||
@@ -76,10 +76,6 @@ class ALttPItem(Item):
|
|||||||
if self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
|
if self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
|
||||||
return self.type
|
return self.type
|
||||||
|
|
||||||
@property
|
|
||||||
def locked_dungeon_item(self):
|
|
||||||
return self.location.locked and self.dungeon_item
|
|
||||||
|
|
||||||
|
|
||||||
class LTTPRegionType(IntEnum):
|
class LTTPRegionType(IntEnum):
|
||||||
LightWorld = 1
|
LightWorld = 1
|
||||||
|
|||||||
@@ -356,6 +356,8 @@ 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. Plando features have to be enabled first, before they can be used (opt-in).
|
1. All plando options are enabled by default, except for "items plando" which has to be enabled before it can be used (opt-in).
|
||||||
2. To do so, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml
|
2. To enable it, 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,6 +66,7 @@ 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
|
||||||
@@ -132,17 +133,15 @@ 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
|
||||||
@@ -162,7 +161,6 @@ 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, event=True) # event=True prevents running sweep_for_events() and picking up
|
state.collect(item, prevent_sweep=True) # prevent_sweep=True prevents running sweep_for_advancements() and picking up
|
||||||
state.sweep_for_events() # key drop keys repeatedly
|
state.sweep_for_advancements() # 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,11 +1,11 @@
|
|||||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
from worlds.alttp.Dungeons import get_dungeon_item_pool
|
||||||
from worlds.alttp.EntranceShuffle import link_inverted_entrances
|
from worlds.alttp.EntranceShuffle import link_inverted_entrances
|
||||||
from worlds.alttp.InvertedRegions import create_inverted_regions
|
from worlds.alttp.InvertedRegions import create_inverted_regions
|
||||||
from worlds.alttp.ItemPool import difficulties
|
from worlds.alttp.ItemPool import difficulties
|
||||||
from worlds.alttp.Items import item_factory
|
from worlds.alttp.Items import item_factory
|
||||||
from worlds.alttp.Regions import mark_light_world_regions
|
from worlds.alttp.Regions import mark_light_world_regions
|
||||||
from worlds.alttp.Shops import create_shops
|
from worlds.alttp.Shops import create_shops
|
||||||
from test.TestBase import TestBase
|
from test.bases import TestBase
|
||||||
|
|
||||||
from worlds.alttp.test import LTTPTestBase
|
from worlds.alttp.test import LTTPTestBase
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from worlds.alttp.Items import item_factory
|
|||||||
from worlds.alttp.Options import GlitchesRequired
|
from worlds.alttp.Options import GlitchesRequired
|
||||||
from worlds.alttp.Regions import mark_light_world_regions
|
from worlds.alttp.Regions import mark_light_world_regions
|
||||||
from worlds.alttp.Shops import create_shops
|
from worlds.alttp.Shops import create_shops
|
||||||
from test.TestBase import TestBase
|
from test.bases import TestBase
|
||||||
|
|
||||||
from worlds.alttp.test import LTTPTestBase
|
from worlds.alttp.test import LTTPTestBase
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from worlds.alttp.Items import item_factory
|
|||||||
from worlds.alttp.Options import GlitchesRequired
|
from worlds.alttp.Options import GlitchesRequired
|
||||||
from worlds.alttp.Regions import mark_light_world_regions
|
from worlds.alttp.Regions import mark_light_world_regions
|
||||||
from worlds.alttp.Shops import create_shops
|
from worlds.alttp.Shops import create_shops
|
||||||
from test.TestBase import TestBase
|
from test.bases import TestBase
|
||||||
|
|
||||||
from worlds.alttp.test import LTTPTestBase
|
from worlds.alttp.test import LTTPTestBase
|
||||||
|
|
||||||
|
|||||||
60
worlds/alttp/test/options/test_dungeon_fill.py
Normal file
60
worlds/alttp/test/options/test_dungeon_fill.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
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 = "games/Sudoku/info/en"
|
options_page = False
|
||||||
theme = 'partyTime'
|
theme = 'partyTime'
|
||||||
|
|
||||||
setup_en = Tutorial(
|
setup_en = Tutorial(
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
# APSudoku Setup Guide
|
# APSudoku Setup Guide
|
||||||
|
|
||||||
## Required Software
|
## Required Software
|
||||||
- [APSudoku](https://github.com/EmilyV99/APSudoku)
|
- [APSudoku](https://github.com/APSudoku/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
|
||||||
|
|
||||||
@@ -13,25 +11,33 @@ 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/EmilyV99/APSudoku/releases). Download and extract the `APSudoku.zip` file.
|
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.
|
||||||
|
|
||||||
## Joining a MultiWorld Game
|
## Joining a MultiWorld Game
|
||||||
|
|
||||||
1. Run APSudoku.exe
|
1. Run the APSudoku executable.
|
||||||
2. Under the 'Archipelago' tab at the top-right:
|
2. Under `Settings` → `Connection` at the top-right:
|
||||||
- Enter the server url & port number
|
- Enter the server address and 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`
|
||||||
3. Go back to the 'Sudoku' tab
|
4. Under the `Sudoku` tab
|
||||||
- Click the various '?' buttons for information on how to play / control
|
- Choose puzzle difficulty
|
||||||
4. Choose puzzle difficulty
|
- Click `Start` to generate a puzzle
|
||||||
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 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 if you quit a puzzle without solving it (including disconnecting).
|
||||||
- Life count customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
|
- Your life count is 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.NORMAL, ItemGroup.UTILITY), # collectible_seahorse_costume
|
"Arnassi Armor": ItemData(698023, 1, ItemType.PROGRESSION, 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
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class AquariaLocations:
|
|||||||
|
|
||||||
locations_verse_cave_r = {
|
locations_verse_cave_r = {
|
||||||
"Verse Cave, bulb in the skeleton room": 698107,
|
"Verse Cave, bulb in the skeleton room": 698107,
|
||||||
"Verse Cave, bulb in the path left of the skeleton room": 698108,
|
"Verse Cave, bulb in the path right of the skeleton room": 698108,
|
||||||
"Verse Cave right area, Big Seed": 698175,
|
"Verse Cave right area, Big Seed": 698175,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 left path from the Verse Cave": 698061,
|
"Home Water, bulb in the end of the path close to 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 left part": 698071,
|
"Song Cave, bulb in the top right 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,
|
||||||
@@ -122,6 +122,7 @@ class AquariaLocations:
|
|||||||
"Open Water top right area, second urn in the Mithalas exit": 698149,
|
"Open Water top right area, second urn in the Mithalas exit": 698149,
|
||||||
"Open Water top right area, third urn in the Mithalas exit": 698150,
|
"Open Water top right area, third urn in the Mithalas exit": 698150,
|
||||||
}
|
}
|
||||||
|
|
||||||
locations_openwater_tr_turtle = {
|
locations_openwater_tr_turtle = {
|
||||||
"Open Water top right area, bulb in the turtle room": 698009,
|
"Open Water top right area, bulb in the turtle room": 698009,
|
||||||
"Open Water top right area, Transturtle": 698211,
|
"Open Water top right area, Transturtle": 698211,
|
||||||
@@ -151,6 +152,9 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +199,7 @@ class AquariaLocations:
|
|||||||
|
|
||||||
locations_cathedral_l = {
|
locations_cathedral_l = {
|
||||||
"Mithalas City Castle, bulb in the flesh hole": 698042,
|
"Mithalas City Castle, bulb in the flesh hole": 698042,
|
||||||
"Mithalas City Castle, Blue banner": 698165,
|
"Mithalas City Castle, Blue Banner": 698165,
|
||||||
"Mithalas City Castle, urn in the bedroom": 698130,
|
"Mithalas City Castle, urn in the bedroom": 698130,
|
||||||
"Mithalas City Castle, first urn of the single lamp path": 698131,
|
"Mithalas City Castle, first urn of the single lamp path": 698131,
|
||||||
"Mithalas City Castle, second urn of the single lamp path": 698132,
|
"Mithalas City Castle, second urn of the single lamp path": 698132,
|
||||||
@@ -226,7 +230,7 @@ class AquariaLocations:
|
|||||||
"Mithalas Cathedral, third urn in the path behind the flesh vein": 698146,
|
"Mithalas Cathedral, third urn in the path behind the flesh vein": 698146,
|
||||||
"Mithalas Cathedral, fourth urn in the top right room": 698147,
|
"Mithalas Cathedral, fourth urn in the top right room": 698147,
|
||||||
"Mithalas Cathedral, Mithalan Dress": 698189,
|
"Mithalas Cathedral, Mithalan Dress": 698189,
|
||||||
"Mithalas Cathedral right area, urn below the left entrance": 698198,
|
"Mithalas Cathedral, urn below the left entrance": 698198,
|
||||||
}
|
}
|
||||||
|
|
||||||
locations_cathedral_underground = {
|
locations_cathedral_underground = {
|
||||||
@@ -239,7 +243,7 @@ class AquariaLocations:
|
|||||||
}
|
}
|
||||||
|
|
||||||
locations_cathedral_boss = {
|
locations_cathedral_boss = {
|
||||||
"Cathedral boss area, beating Mithalan God": 698202,
|
"Mithalas boss area, beating Mithalan God": 698202,
|
||||||
}
|
}
|
||||||
|
|
||||||
locations_forest_tl = {
|
locations_forest_tl = {
|
||||||
@@ -268,11 +272,14 @@ class AquariaLocations:
|
|||||||
}
|
}
|
||||||
|
|
||||||
locations_forest_bl = {
|
locations_forest_bl = {
|
||||||
"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, Transturtle": 698212,
|
"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, Walker Baby": 698186,
|
||||||
|
}
|
||||||
|
|
||||||
locations_forest_br = {
|
locations_forest_br = {
|
||||||
"Kelp Forest bottom right area, Odd Container": 698168,
|
"Kelp Forest bottom right area, Odd Container": 698168,
|
||||||
}
|
}
|
||||||
@@ -369,7 +376,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 left part": 698092,
|
"Sun Temple, bulb on the right 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,
|
||||||
}
|
}
|
||||||
@@ -401,6 +408,9 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,7 +461,7 @@ class AquariaLocations:
|
|||||||
|
|
||||||
locations_body_c = {
|
locations_body_c = {
|
||||||
"The Body center area, breaking Li's cage": 698201,
|
"The Body center area, breaking Li's cage": 698201,
|
||||||
"The Body main area, bulb on the main path blocking tube": 698097,
|
"The Body center area, bulb on the main path blocking tube": 698097,
|
||||||
}
|
}
|
||||||
|
|
||||||
locations_body_l = {
|
locations_body_l = {
|
||||||
@@ -498,6 +508,7 @@ 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,
|
||||||
@@ -508,6 +519,7 @@ 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,
|
||||||
@@ -529,6 +541,7 @@ 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,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Description: Manage options in the Aquaria game multiworld randomizer
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from Options import Toggle, Choice, Range, DeathLink, PerGameCommonOptions, DefaultOnToggle, StartInventoryPool
|
from Options import Toggle, Choice, Range, PerGameCommonOptions, DefaultOnToggle, StartInventoryPool
|
||||||
|
|
||||||
|
|
||||||
class IngredientRandomizer(Choice):
|
class IngredientRandomizer(Choice):
|
||||||
@@ -111,6 +111,14 @@ class BindSongNeededToGetUnderRockBulb(Toggle):
|
|||||||
display_name = "Bind song needed to get sing bulbs under rocks"
|
display_name = "Bind song needed to get sing bulbs under rocks"
|
||||||
|
|
||||||
|
|
||||||
|
class BlindGoal(Toggle):
|
||||||
|
"""
|
||||||
|
Hide the goal's requirements from the help page so that you have to go to the last boss door to know
|
||||||
|
what is needed to access the boss.
|
||||||
|
"""
|
||||||
|
display_name = "Hide the goal's requirements"
|
||||||
|
|
||||||
|
|
||||||
class UnconfineHomeWater(Choice):
|
class UnconfineHomeWater(Choice):
|
||||||
"""
|
"""
|
||||||
Open the way out of the Home Water area so that Naija can go to open water and beyond without the bind song.
|
Open the way out of the Home Water area so that Naija can go to open water and beyond without the bind song.
|
||||||
@@ -142,4 +150,4 @@ class AquariaOptions(PerGameCommonOptions):
|
|||||||
dish_randomizer: DishRandomizer
|
dish_randomizer: DishRandomizer
|
||||||
aquarian_translation: AquarianTranslation
|
aquarian_translation: AquarianTranslation
|
||||||
skip_first_vision: SkipFirstVision
|
skip_first_vision: SkipFirstVision
|
||||||
death_link: DeathLink
|
blind_goal: BlindGoal
|
||||||
|
|||||||
@@ -14,97 +14,112 @@ 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("Hot soup", player)
|
return state.has_any({"Hot soup", "Hot soup x 2"}, 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_shield_song(state:CollectionState, player: int) -> bool:
|
def _has_energy_attack_item(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_nature_form(state:CollectionState, player: int) -> bool:
|
def _has_beast_and_soup_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:
|
||||||
@@ -134,6 +149,7 @@ 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
|
||||||
@@ -152,6 +168,7 @@ 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
|
||||||
@@ -179,6 +196,7 @@ 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
|
||||||
@@ -213,7 +231,7 @@ class AquariaRegions:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __add_region(self, hint: str,
|
def __add_region(self, hint: str,
|
||||||
locations: Optional[Dict[str, Optional[int]]]) -> Region:
|
locations: Optional[Dict[str, 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`
|
||||||
@@ -236,7 +254,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)
|
||||||
|
|
||||||
@@ -280,6 +298,8 @@ 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)
|
||||||
|
|
||||||
@@ -300,11 +320,11 @@ class AquariaRegions:
|
|||||||
AquariaLocations.locations_cathedral_l_sc)
|
AquariaLocations.locations_cathedral_l_sc)
|
||||||
self.cathedral_r = self.__add_region("Mithalas Cathedral",
|
self.cathedral_r = self.__add_region("Mithalas Cathedral",
|
||||||
AquariaLocations.locations_cathedral_r)
|
AquariaLocations.locations_cathedral_r)
|
||||||
self.cathedral_underground = self.__add_region("Mithalas Cathedral Underground area",
|
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",
|
self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, Mithalan God room", None)
|
||||||
|
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:
|
||||||
"""
|
"""
|
||||||
@@ -320,6 +340,8 @@ 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",
|
||||||
@@ -375,9 +397,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:
|
||||||
"""
|
"""
|
||||||
@@ -388,6 +410,8 @@ 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)
|
||||||
@@ -407,7 +431,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:
|
||||||
"""
|
"""
|
||||||
@@ -427,7 +451,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,
|
||||||
@@ -455,8 +479,8 @@ class AquariaRegions:
|
|||||||
"""
|
"""
|
||||||
Connect entrances of the different regions around `home_water`
|
Connect entrances of the different regions around `home_water`
|
||||||
"""
|
"""
|
||||||
self.__connect_regions("Menu", "Verse Cave right area",
|
self.__connect_one_way_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)
|
||||||
@@ -464,7 +488,8 @@ 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_form(state, self.player) and _has_bind_song(state, self.player))
|
lambda state: _has_energy_attack_item(state, self.player) and
|
||||||
|
_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",
|
||||||
@@ -472,7 +497,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_form(state, self.player) and
|
lambda state: _has_energy_attack_item(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,
|
||||||
@@ -482,28 +507,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_form(state, self.player))
|
lambda state: _has_energy_attack_item(state, self.player) and
|
||||||
|
_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_form(state, self.player))
|
_has_energy_attack_item(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_form(state, self.player))
|
lambda state: _has_energy_attack_item(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_bind_song(state, self.player) and
|
lambda state: _has_energy_form(state, self.player))
|
||||||
_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_form(state, self.player))
|
_has_energy_attack_item(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_form(state, self.player) and
|
_has_energy_attack_item(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)
|
||||||
@@ -520,7 +545,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(state, self.player))
|
lambda state: _has_beast_form_or_arnassi_armor(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",
|
||||||
@@ -529,10 +554,9 @@ 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(state, self.player))
|
lambda state: _has_beast_form_or_arnassi_armor(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",
|
||||||
@@ -551,10 +575,14 @@ 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(state, self.player) and
|
lambda state: _has_beast_form_or_arnassi_armor(state, self.player) and
|
||||||
_has_energy_form(state, self.player))
|
(_has_energy_attack_item(state, self.player) or
|
||||||
|
_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)
|
||||||
|
|
||||||
@@ -564,61 +592,62 @@ 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(state, self.player))
|
lambda state: _has_beast_form_or_arnassi_armor(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_form(state, self.player))
|
_has_energy_attack_item(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_beast_form(state, self.player) and
|
lambda state: _has_nature_form(state, self.player))
|
||||||
_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_regions("Mithalas castle", "Cathedral boss left area",
|
self.__connect_one_way_regions("Mithalas castle", "Cathedral boss right area",
|
||||||
self.cathedral_l, self.cathedral_boss_l,
|
self.cathedral_l, self.cathedral_boss_r,
|
||||||
lambda state: _has_beast_form(state, self.player) and
|
lambda state: _has_beast_form(state, self.player))
|
||||||
_has_energy_form(state, self.player) and
|
self.__connect_one_way_regions("Cathedral boss left area", "Mithalas castle",
|
||||||
_has_bind_song(state, self.player))
|
self.cathedral_boss_l, self.cathedral_l,
|
||||||
self.__connect_regions("Mithalas castle", "Cathedral underground",
|
lambda state: _has_beast_form(state, self.player))
|
||||||
|
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) and
|
lambda state: _has_beast_form(state, self.player))
|
||||||
_has_bind_song(state, self.player))
|
self.__connect_one_way_regions("Mithalas castle", "Mithalas Cathedral",
|
||||||
self.__connect_regions("Mithalas castle", "Cathedral right area",
|
self.cathedral_l, self.cathedral_r,
|
||||||
self.cathedral_l, self.cathedral_r,
|
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("Mithalas Cathedral", "Mithalas Cathedral underground",
|
||||||
self.__connect_regions("Cathedral right area", "Cathedral underground",
|
self.cathedral_r, self.cathedral_underground)
|
||||||
self.cathedral_r, self.cathedral_underground,
|
self.__connect_one_way_regions("Mithalas Cathedral underground", "Mithalas Cathedral",
|
||||||
lambda state: _has_energy_form(state, self.player))
|
self.cathedral_underground, self.cathedral_r,
|
||||||
self.__connect_one_way_regions("Cathedral underground", "Cathedral boss left area",
|
lambda state: _has_beast_form(state, self.player) and
|
||||||
self.cathedral_underground, self.cathedral_boss_r,
|
_has_energy_attack_item(state, self.player))
|
||||||
lambda state: _has_energy_form(state, self.player) and
|
self.__connect_one_way_regions("Mithalas Cathedral underground", "Cathedral boss right area",
|
||||||
_has_bind_song(state, self.player))
|
self.cathedral_underground, self.cathedral_boss_r)
|
||||||
self.__connect_one_way_regions("Cathedral boss left area", "Cathedral underground",
|
self.__connect_one_way_regions("Cathedral boss right 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_regions("Cathedral boss right area", "Cathedral boss left area",
|
self.__connect_one_way_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_form(state, self.player))
|
_has_energy_attack_item(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:
|
||||||
"""
|
"""
|
||||||
@@ -628,6 +657,12 @@ 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",
|
||||||
@@ -641,7 +676,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_form(state, self.player) and
|
_has_energy_attack_item(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)
|
||||||
@@ -649,7 +684,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_form(state, self.player))
|
lambda state: _has_energy_attack_item(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))
|
||||||
@@ -663,7 +698,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_form(state, self.player))
|
_has_energy_attack_item(state, self.player))
|
||||||
|
|
||||||
def __connect_veil_regions(self) -> None:
|
def __connect_veil_regions(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -681,8 +716,7 @@ 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))
|
||||||
@@ -691,20 +725,25 @@ 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_regions("Sun Temple right area", "Sun Temple left area",
|
self.__connect_one_way_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))
|
lambda state: _has_bind_song(state, self.player) or
|
||||||
|
_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_form(state, self.player))
|
lambda state: _has_energy_attack_item(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",
|
||||||
@@ -712,7 +751,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_form(state, self.player))
|
_has_energy_attack_item(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))
|
||||||
@@ -728,16 +767,22 @@ 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))
|
_has_beast_form(state, self.player)) or
|
||||||
|
_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
|
||||||
@@ -747,13 +792,14 @@ 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_form(state, self.player))
|
_has_energy_attack_item(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("Abyss right area", "Bubble Cave",
|
self.__connect_regions("Ice cave", "Bubble Cave",
|
||||||
self.ice_cave, self.bubble_cave,
|
self.ice_cave, self.bubble_cave,
|
||||||
lambda state: _has_beast_form(state, self.player))
|
lambda state: _has_beast_form(state, self.player) or
|
||||||
|
_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)
|
||||||
@@ -772,7 +818,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_form(state, self.player) and
|
_has_energy_attack_item(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:
|
||||||
@@ -780,11 +826,13 @@ 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))
|
||||||
@@ -803,22 +851,12 @@ 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, region_target: Region,
|
def __connect_transturtle(self, item_source: str, item_target: str, region_source: Region,
|
||||||
rule=None) -> None:
|
region_target: Region) -> 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:
|
||||||
if rule is None:
|
self.__connect_one_way_regions(item_source, item_target, region_source, region_target,
|
||||||
self.__connect_one_way_regions(item_source, item_target, region_source, region_target,
|
lambda state: state.has(item_target, self.player))
|
||||||
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"""
|
||||||
@@ -827,24 +865,10 @@ 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)
|
self.__connect_transturtle(item, "Transturtle Abyss right", region, self.abyss_r_transturtle)
|
||||||
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_path,
|
self.__connect_transturtle(item, "Transturtle Arnassi Ruins", region, self.arnassi_cave_transturtle)
|
||||||
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"""
|
||||||
@@ -853,10 +877,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)
|
self._connect_transturtle_to_other("Transturtle Abyss right", self.abyss_r_transturtle)
|
||||||
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_arnassi_path_transturtle_to_other("Transturtle Arnassi Ruins", self.arnassi_path)
|
self._connect_transturtle_to_other("Transturtle Arnassi Ruins", self.arnassi_cave_transturtle)
|
||||||
|
|
||||||
def connect_regions(self) -> None:
|
def connect_regions(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -893,7 +917,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_r,
|
self.__add_event_location(self.cathedral_boss_l,
|
||||||
"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,
|
||||||
@@ -970,8 +994,9 @@ 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(self.multiworld.get_location("Open Water top right area, second urn in the Mithalas exit", self.player),
|
add_rule(
|
||||||
lambda state: _has_damaging_item(state, 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))
|
||||||
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),
|
||||||
@@ -1019,66 +1044,46 @@ 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) and _has_beast_form(state, self.player))
|
lambda state: _has_hot_soup(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_hot_soup(state, self.player) and _has_beast_form(state, self.player))
|
lambda state: _has_beast_and_soup_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),
|
||||||
@@ -1097,12 +1102,14 @@ 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(self.multiworld.get_location("Open Water bottom left area, bulb inside the lowest fish pass", self.player),
|
add_rule(
|
||||||
lambda state: _has_fish_form(state, self.player))
|
self.multiworld.get_location("Open Water bottom left area, bulb inside the lowest fish pass", self.player),
|
||||||
add_rule(self.multiworld.get_location("Kelp Forest bottom left area, Walker baby", 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),
|
||||||
lambda state: _has_spirit_form(state, self.player))
|
lambda state: _has_spirit_form(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("The Veil top left area, bulb hidden behind the blocking rock", self.player),
|
add_rule(
|
||||||
lambda state: _has_bind_song(state, 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))
|
||||||
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),
|
||||||
@@ -1114,103 +1121,119 @@ 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(state, self.player))
|
lambda state: _has_beast_form_or_arnassi_armor(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_energy_form(state, self.player))
|
self.player), lambda state: _has_bind_song(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_form(state, self.player))
|
lambda state: _has_energy_attack_item(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) and
|
lambda state: _has_fish_form(state, self.player) or
|
||||||
_has_spirit_form(state, self.player))
|
_has_beast_and_soup_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("Cathedral 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()
|
||||||
@@ -1234,7 +1257,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_form(state, self.player))
|
lambda state: _has_bind_song(state, self.player) and _has_energy_attack_item(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
|
||||||
|
|
||||||
@@ -1274,6 +1297,7 @@ 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:
|
||||||
@@ -1300,6 +1324,7 @@ 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)
|
||||||
@@ -1337,6 +1362,7 @@ 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)
|
||||||
|
|||||||
@@ -204,7 +204,8 @@ class AquariaWorld(World):
|
|||||||
|
|
||||||
def fill_slot_data(self) -> Dict[str, Any]:
|
def fill_slot_data(self) -> Dict[str, Any]:
|
||||||
return {"ingredientReplacement": self.ingredients_substitution,
|
return {"ingredientReplacement": self.ingredients_substitution,
|
||||||
"aquarianTranslate": bool(self.options.aquarian_translation.value),
|
"aquarian_translate": bool(self.options.aquarian_translation.value),
|
||||||
|
"blind_goal": bool(self.options.blind_goal.value),
|
||||||
"secret_needed": self.options.objective.value > 0,
|
"secret_needed": self.options.objective.value > 0,
|
||||||
"minibosses_to_kill": self.options.mini_bosses_to_beat.value,
|
"minibosses_to_kill": self.options.mini_bosses_to_beat.value,
|
||||||
"bigbosses_to_kill": self.options.big_bosses_to_beat.value,
|
"bigbosses_to_kill": self.options.big_bosses_to_beat.value,
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ after_home_water_locations = [
|
|||||||
"Mithalas City, Doll",
|
"Mithalas City, Doll",
|
||||||
"Mithalas City, urn inside a home fish pass",
|
"Mithalas City, urn inside a home fish pass",
|
||||||
"Mithalas City Castle, bulb in the flesh hole",
|
"Mithalas City Castle, bulb in the flesh hole",
|
||||||
"Mithalas City Castle, Blue banner",
|
"Mithalas City Castle, Blue Banner",
|
||||||
"Mithalas City Castle, urn in the bedroom",
|
"Mithalas City Castle, urn in the bedroom",
|
||||||
"Mithalas City Castle, first urn of the single lamp path",
|
"Mithalas City Castle, first urn of the single lamp path",
|
||||||
"Mithalas City Castle, second urn of the single lamp path",
|
"Mithalas City Castle, second urn of the single lamp path",
|
||||||
@@ -82,14 +82,14 @@ after_home_water_locations = [
|
|||||||
"Mithalas Cathedral, third 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, fourth urn in the top right room",
|
||||||
"Mithalas Cathedral, Mithalan Dress",
|
"Mithalas Cathedral, Mithalan Dress",
|
||||||
"Mithalas Cathedral right area, urn below the left entrance",
|
"Mithalas Cathedral, urn below the left entrance",
|
||||||
"Cathedral Underground, bulb in the center part",
|
"Cathedral Underground, bulb in the center part",
|
||||||
"Cathedral Underground, first bulb in the top left part",
|
"Cathedral Underground, first bulb in the top left part",
|
||||||
"Cathedral Underground, second bulb in the top left part",
|
"Cathedral Underground, second bulb in the top left part",
|
||||||
"Cathedral Underground, third bulb in the top left part",
|
"Cathedral Underground, third bulb in the top left part",
|
||||||
"Cathedral Underground, bulb close to the save crystal",
|
"Cathedral Underground, bulb close to the save crystal",
|
||||||
"Cathedral Underground, bulb in the bottom right path",
|
"Cathedral Underground, bulb in the bottom right path",
|
||||||
"Cathedral boss area, beating Mithalan God",
|
"Mithalas boss area, beating Mithalan God",
|
||||||
"Kelp Forest top left area, bulb in the bottom left clearing",
|
"Kelp Forest top left area, bulb in the bottom left clearing",
|
||||||
"Kelp Forest top left area, bulb in the path down from the top left clearing",
|
"Kelp Forest top left area, bulb in the path down from the top left clearing",
|
||||||
"Kelp Forest top left area, bulb in the top left clearing",
|
"Kelp Forest top left area, bulb in the top left clearing",
|
||||||
@@ -104,7 +104,7 @@ after_home_water_locations = [
|
|||||||
"Kelp Forest top right area, Black Pearl",
|
"Kelp Forest top right area, Black Pearl",
|
||||||
"Kelp Forest top right area, bulb in the top fish pass",
|
"Kelp Forest top right area, bulb in the top fish pass",
|
||||||
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
||||||
"Kelp Forest bottom left area, Walker baby",
|
"Kelp Forest bottom left area, Walker Baby",
|
||||||
"Kelp Forest bottom left area, Transturtle",
|
"Kelp Forest bottom left area, Transturtle",
|
||||||
"Kelp Forest bottom right area, Odd Container",
|
"Kelp Forest bottom right area, Odd Container",
|
||||||
"Kelp Forest boss area, beating Drunian God",
|
"Kelp Forest boss area, beating Drunian God",
|
||||||
@@ -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 left part",
|
"Sun Temple, bulb on the right 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",
|
||||||
@@ -175,7 +175,7 @@ after_home_water_locations = [
|
|||||||
"Sunken City left area, Girl Costume",
|
"Sunken City left area, Girl Costume",
|
||||||
"Sunken City, bulb on top of the boss area",
|
"Sunken City, bulb on top of the boss area",
|
||||||
"The Body center area, breaking Li's cage",
|
"The Body center area, breaking Li's cage",
|
||||||
"The Body main area, bulb on the main path blocking tube",
|
"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, first bulb in the top face room",
|
||||||
"The Body left area, second 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 below the water stream",
|
||||||
|
|||||||
@@ -13,36 +13,16 @@ 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",
|
||||||
"Beating Mantis Shrimp Prime",
|
"Sunken City cleared",
|
||||||
"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)
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""
|
||||||
|
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,55 +17,16 @@ 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",
|
||||||
"Energy Temple boss area, Fallen God Tooth",
|
"The Body left area, first bulb in the top face room",
|
||||||
"Energy Temple blaster room, Blaster Egg",
|
"The Body left area, second bulb in the top face room",
|
||||||
"Mithalas City Castle, beating the Priests",
|
"The Body left area, bulb below the water stream",
|
||||||
"Mithalas Cathedral, first urn in the top right room",
|
"The Body left area, bulb in the top path to the top face room",
|
||||||
"Mithalas Cathedral, second urn in the top right room",
|
"The Body left area, bulb in the bottom face room",
|
||||||
"Mithalas Cathedral, third urn in the top right room",
|
"The Body right area, bulb in the top path to the bottom face room",
|
||||||
"Mithalas Cathedral, urn in the flesh room with fleas",
|
"The Body right area, bulb in the bottom face room",
|
||||||
"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 right area, urn below the left entrance",
|
|
||||||
"Cathedral 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"]]
|
||||||
|
|||||||
92
worlds/aquaria/test/test_energy_form_or_dual_form_access.py
Normal file
92
worlds/aquaria/test/test_energy_form_or_dual_form_access.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
"""
|
||||||
|
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,6 +17,7 @@ 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",
|
||||||
@@ -30,8 +31,7 @@ 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)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class LiAccessTest(AquariaTestBase):
|
|||||||
"Sunken City left area, Girl Costume",
|
"Sunken City left area, Girl Costume",
|
||||||
"Sunken City, bulb on top of the boss area",
|
"Sunken City, bulb on top of the boss area",
|
||||||
"The Body center area, breaking Li's cage",
|
"The Body center area, breaking Li's cage",
|
||||||
"The Body main area, bulb on the main path blocking tube",
|
"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, first bulb in the top face room",
|
||||||
"The Body left area, second 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 below the water stream",
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ 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",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class NatureFormAccessTest(AquariaTestBase):
|
|||||||
"Beating the Golem",
|
"Beating the Golem",
|
||||||
"Sunken City cleared",
|
"Sunken City cleared",
|
||||||
"The Body center area, breaking Li's cage",
|
"The Body center area, breaking Li's cage",
|
||||||
"The Body main area, bulb on the main path blocking tube",
|
"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, first bulb in the top face room",
|
||||||
"The Body left area, second 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 below the water stream",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
|||||||
|
|
||||||
unfillable_locations = [
|
unfillable_locations = [
|
||||||
"Energy Temple boss area, Fallen God Tooth",
|
"Energy Temple boss area, Fallen God Tooth",
|
||||||
"Cathedral boss area, beating Mithalan God",
|
"Mithalas boss area, beating Mithalan God",
|
||||||
"Kelp Forest boss area, beating Drunian God",
|
"Kelp Forest boss area, beating Drunian God",
|
||||||
"Sun Temple boss area, beating Sun God",
|
"Sun Temple boss area, beating Sun God",
|
||||||
"Sunken City, bulb on top of the boss area",
|
"Sunken City, bulb on top of the boss area",
|
||||||
@@ -35,7 +35,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
|||||||
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||||
"Bubble Cave, Verse Egg",
|
"Bubble Cave, Verse Egg",
|
||||||
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
||||||
"Kelp Forest bottom left area, Walker baby",
|
"Kelp Forest bottom left area, Walker Baby",
|
||||||
"Sun Temple, Sun Key",
|
"Sun Temple, Sun Key",
|
||||||
"The Body bottom area, Mutant Costume",
|
"The Body bottom area, Mutant Costume",
|
||||||
"Sun Temple, bulb in the hidden room of the right part",
|
"Sun Temple, bulb in the hidden room of the right part",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
|||||||
|
|
||||||
unfillable_locations = [
|
unfillable_locations = [
|
||||||
"Energy Temple boss area, Fallen God Tooth",
|
"Energy Temple boss area, Fallen God Tooth",
|
||||||
"Cathedral boss area, beating Mithalan God",
|
"Mithalas boss area, beating Mithalan God",
|
||||||
"Kelp Forest boss area, beating Drunian God",
|
"Kelp Forest boss area, beating Drunian God",
|
||||||
"Sun Temple boss area, beating Sun God",
|
"Sun Temple boss area, beating Sun God",
|
||||||
"Sunken City, bulb on top of the boss area",
|
"Sunken City, bulb on top of the boss area",
|
||||||
@@ -34,7 +34,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
|||||||
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||||
"Bubble Cave, Verse Egg",
|
"Bubble Cave, Verse Egg",
|
||||||
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
||||||
"Kelp Forest bottom left area, Walker baby",
|
"Kelp Forest bottom left area, Walker Baby",
|
||||||
"Sun Temple, Sun Key",
|
"Sun Temple, Sun Key",
|
||||||
"The Body bottom area, Mutant Costume",
|
"The Body bottom area, Mutant Costume",
|
||||||
"Sun Temple, bulb in the hidden room of the right part",
|
"Sun Temple, bulb in the hidden room of the right part",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class SpiritFormAccessTest(AquariaTestBase):
|
|||||||
"The Veil bottom area, bulb in the spirit path",
|
"The Veil bottom area, bulb in the spirit path",
|
||||||
"Mithalas City Castle, Trident Head",
|
"Mithalas City Castle, Trident Head",
|
||||||
"Open Water skeleton path, King Skull",
|
"Open Water skeleton path, King Skull",
|
||||||
"Kelp Forest bottom left area, Walker baby",
|
"Kelp Forest bottom left area, Walker Baby",
|
||||||
"Abyss right area, bulb behind the rock in the whale room",
|
"Abyss right area, bulb behind the rock in the whale room",
|
||||||
"The Whale, Verse Egg",
|
"The Whale, Verse Egg",
|
||||||
"Ice Cave, bulb in the room to the right",
|
"Ice Cave, bulb in the room to the right",
|
||||||
@@ -30,7 +30,6 @@ 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)
|
||||||
|
|||||||
19
worlds/blasphemous/ExtractorConfig.json
Normal file
19
worlds/blasphemous/ExtractorConfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"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,52 +637,35 @@ 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",
|
||||||
@@ -725,14 +708,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",
|
||||||
@@ -746,10 +729,17 @@ 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_set: Set[str] = [
|
tears_list: List[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)",
|
||||||
@@ -772,16 +762,16 @@ tears_set: Set[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,4 +1,5 @@
|
|||||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool
|
from dataclasses import dataclass
|
||||||
|
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, OptionGroup
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
|
||||||
@@ -20,23 +21,30 @@ 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
|
"""
|
||||||
and advanced movement tricks or glitches."""
|
Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses and advanced movement tricks or glitches.
|
||||||
|
"""
|
||||||
display_name = "Difficulty"
|
display_name = "Difficulty"
|
||||||
option_easy = 0
|
option_easy = 0
|
||||||
option_normal = 1
|
option_normal = 1
|
||||||
@@ -45,15 +53,18 @@ 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
|
"""
|
||||||
other options.
|
Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain other options.
|
||||||
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."""
|
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.
|
||||||
|
"""
|
||||||
display_name = "Starting Location"
|
display_name = "Starting Location"
|
||||||
option_brotherhood = 0
|
option_brotherhood = 0
|
||||||
option_albero = 1
|
option_albero = 1
|
||||||
@@ -66,10 +77,15 @@ 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
|
||||||
@@ -78,14 +94,18 @@ class Ending(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class SkipLongQuests(Toggle):
|
class SkipLongQuests(Toggle):
|
||||||
"""Ensures that the rewards for long quests will be filler items.
|
"""
|
||||||
Affected locations: \"Albero: Donate 50000 Tears\", \"Ossuary: 11th reward\", \"AtTotS: Miriam's gift\",
|
Ensures that the rewards for long quests will be filler items.
|
||||||
\"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
|
||||||
@@ -94,50 +114,68 @@ 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
|
"""
|
||||||
and survive.
|
Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes 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
|
"""
|
||||||
a second time in mid-air.
|
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.
|
||||||
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.
|
"""
|
||||||
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in
|
Randomizes the enemies that appear in each room.
|
||||||
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
|
||||||
@@ -146,43 +184,75 @@ 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.
|
"""
|
||||||
Has no effect if Enemy Randomizer is disabled."""
|
Randomized enemies will have their stats increased or decreased depending on the area they appear in.
|
||||||
|
|
||||||
|
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.
|
"""
|
||||||
Note that Guilt Fragments will not appear when killed by Death Link."""
|
When you die, everyone dies. The reverse is also true.
|
||||||
|
|
||||||
|
Note that Guilt Fragments will not appear when killed by Death Link.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
blasphemous_options = {
|
@dataclass
|
||||||
"prie_dieu_warp": PrieDieuWarp,
|
class BlasphemousOptions(PerGameCommonOptions):
|
||||||
"skip_cutscenes": SkipCutscenes,
|
prie_dieu_warp: PrieDieuWarp
|
||||||
"corpse_hints": CorpseHints,
|
skip_cutscenes: SkipCutscenes
|
||||||
"difficulty": Difficulty,
|
corpse_hints: CorpseHints
|
||||||
"penitence": Penitence,
|
difficulty: Difficulty
|
||||||
"starting_location": StartingLocation,
|
penitence: Penitence
|
||||||
"ending": Ending,
|
starting_location: StartingLocation
|
||||||
"skip_long_quests": SkipLongQuests,
|
ending: Ending
|
||||||
"thorn_shuffle" : ThornShuffle,
|
skip_long_quests: SkipLongQuests
|
||||||
"dash_shuffle": DashShuffle,
|
thorn_shuffle: ThornShuffle
|
||||||
"wall_climb_shuffle": WallClimbShuffle,
|
dash_shuffle: DashShuffle
|
||||||
"reliquary_shuffle": ReliquaryShuffle,
|
wall_climb_shuffle: WallClimbShuffle
|
||||||
"boots_of_pleading": CustomItem1,
|
reliquary_shuffle: ReliquaryShuffle
|
||||||
"purified_hand": CustomItem2,
|
boots_of_pleading: CustomItem1
|
||||||
"start_wheel": StartWheel,
|
purified_hand: CustomItem2
|
||||||
"skill_randomizer": SkillRando,
|
start_wheel: StartWheel
|
||||||
"enemy_randomizer": EnemyRando,
|
skill_randomizer: SkillRando
|
||||||
"enemy_groups": EnemyGroups,
|
enemy_randomizer: EnemyRando
|
||||||
"enemy_scaling": EnemyScaling,
|
enemy_groups: EnemyGroups
|
||||||
"death_link": BlasphemousDeathLink,
|
enemy_scaling: EnemyScaling
|
||||||
"start_inventory": StartInventoryPool
|
death_link: BlasphemousDeathLink
|
||||||
}
|
|
||||||
|
|
||||||
|
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
|
||||||
|
])
|
||||||
|
]
|
||||||
|
|||||||
582
worlds/blasphemous/Preprocessor.py
Normal file
582
worlds/blasphemous/Preprocessor.py
Normal file
@@ -0,0 +1,582 @@
|
|||||||
|
# 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())
|
||||||
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] = {
|
||||||
@@ -44,4 +44,4 @@ skill_dict: Dict[str, str] = {
|
|||||||
"Skill 5, Tier 1": "Lunge Skill",
|
"Skill 5, Tier 1": "Lunge Skill",
|
||||||
"Skill 5, Tier 2": "Lunge Skill",
|
"Skill 5, Tier 2": "Lunge Skill",
|
||||||
"Skill 5, Tier 3": "Lunge Skill",
|
"Skill 5, Tier 3": "Lunge Skill",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, Entrance, Location, Item, Tutorial, ItemClassification
|
from BaseClasses import Region, 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_set, reliquary_set, event_table
|
from .Items import base_id, item_table, group_table, tears_list, reliquary_set
|
||||||
from .Locations import location_table
|
from .Locations import location_names
|
||||||
from .Rooms import room_table, door_table
|
from .Rules import BlasRules
|
||||||
from .Rules import rules
|
from worlds.generic.Rules import set_rule
|
||||||
from worlds.generic.Rules import set_rule, add_rule
|
from .Options import BlasphemousOptions, blas_option_groups
|
||||||
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,39 +21,33 @@ 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 it's terrible fate in your quest to break
|
in an endless cycle of death and rebirth, and free the world from its terrible fate in your quest to break
|
||||||
your eternal damnation!
|
your eternal damnation!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
game: str = "Blasphemous"
|
game = "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["name"]: (base_id + index) for index, loc in enumerate(location_table)}
|
location_name_to_id = {loc: (base_id + index) for index, loc in enumerate(location_names.values())}
|
||||||
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
|
||||||
option_definitions = blasphemous_options
|
options_dataclass = BlasphemousOptions
|
||||||
|
options: BlasphemousOptions
|
||||||
|
|
||||||
required_client_version = (0, 4, 2)
|
required_client_version = (0, 4, 7)
|
||||||
|
|
||||||
|
|
||||||
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.door_connections: Dict[str, str] = {}
|
self.disabled_locations: List[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":
|
||||||
@@ -68,64 +62,56 @@ class BlasphemousWorld(World):
|
|||||||
|
|
||||||
|
|
||||||
def get_filler_item_name(self) -> str:
|
def get_filler_item_name(self) -> str:
|
||||||
return self.multiworld.random.choice(tears_set)
|
return self.random.choice(tears_list)
|
||||||
|
|
||||||
|
|
||||||
def generate_early(self):
|
def generate_early(self):
|
||||||
world = self.multiworld
|
if not self.options.starting_location.randomized:
|
||||||
player = self.player
|
if self.options.starting_location == "mourning_havoc" and self.options.difficulty < 2:
|
||||||
|
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
|
||||||
|
f"{self.options.starting_location} cannot be chosen if Difficulty is lower than Hard.")
|
||||||
|
|
||||||
if not world.starting_location[player].randomized:
|
if (self.options.starting_location == "brotherhood" or self.options.starting_location == "mourning_havoc") \
|
||||||
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
and self.options.dash_shuffle:
|
||||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
|
||||||
" cannot be chosen if Difficulty is lower than Hard.")
|
f"{self.options.starting_location} cannot be chosen if Shuffle Dash is enabled.")
|
||||||
|
|
||||||
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
|
|
||||||
and world.dash_shuffle[player]:
|
|
||||||
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]:
|
if self.options.starting_location == "grievance" and self.options.wall_climb_shuffle:
|
||||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
|
||||||
" cannot be chosen if Shuffle Wall Climb is enabled.")
|
f"{self.options.starting_location} 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 world.difficulty[player].value < 2:
|
if self.options.difficulty < 2:
|
||||||
locations.remove(6)
|
locations.remove(6)
|
||||||
|
|
||||||
if world.dash_shuffle[player]:
|
if self.options.dash_shuffle:
|
||||||
locations.remove(0)
|
locations.remove(0)
|
||||||
if 6 in locations:
|
if 6 in locations:
|
||||||
locations.remove(6)
|
locations.remove(6)
|
||||||
|
|
||||||
if world.wall_climb_shuffle[player]:
|
if self.options.wall_climb_shuffle:
|
||||||
locations.remove(3)
|
locations.remove(3)
|
||||||
|
|
||||||
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
if self.options.starting_location.value not in locations:
|
||||||
invalid = True
|
self.options.starting_location.value = self.random.choice(locations)
|
||||||
|
|
||||||
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 world.dash_shuffle[player]:
|
if not self.options.dash_shuffle:
|
||||||
world.push_precollected(self.create_item("Dash Ability"))
|
self.multiworld.push_precollected(self.create_item("Dash Ability"))
|
||||||
|
|
||||||
if not world.wall_climb_shuffle[player]:
|
if not self.options.wall_climb_shuffle:
|
||||||
world.push_precollected(self.create_item("Wall Climb Ability"))
|
self.multiworld.push_precollected(self.create_item("Wall Climb Ability"))
|
||||||
|
|
||||||
if world.skip_long_quests[player]:
|
if not self.options.boots_of_pleading:
|
||||||
|
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:
|
||||||
world.exclude_locations[player].value.add(loc)
|
self.options.exclude_locations.value.add(loc)
|
||||||
|
|
||||||
start_rooms: Dict[int, str] = {
|
start_rooms: Dict[int, str] = {
|
||||||
0: "D17Z01S01",
|
0: "D17Z01S01",
|
||||||
@@ -137,13 +123,10 @@ class BlasphemousWorld(World):
|
|||||||
6: "D20Z02S09"
|
6: "D20Z02S09"
|
||||||
}
|
}
|
||||||
|
|
||||||
self.start_room = start_rooms[world.starting_location[player].value]
|
self.start_room = start_rooms[self.options.starting_location.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)",
|
||||||
@@ -156,46 +139,46 @@ class BlasphemousWorld(World):
|
|||||||
skipped_items = []
|
skipped_items = []
|
||||||
junk: int = 0
|
junk: int = 0
|
||||||
|
|
||||||
for item, count in world.start_inventory[player].value.items():
|
for item, count in self.options.start_inventory.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 world.thorn_shuffle[player] == 2:
|
if self.options.thorn_shuffle == "vanilla":
|
||||||
for i in range(8):
|
for _ in range(8):
|
||||||
skipped_items.append("Thorn Upgrade")
|
skipped_items.append("Thorn Upgrade")
|
||||||
|
|
||||||
if world.dash_shuffle[player]:
|
if self.options.dash_shuffle:
|
||||||
skipped_items.append(to_remove[removed])
|
skipped_items.append(to_remove[removed])
|
||||||
removed += 1
|
removed += 1
|
||||||
elif not world.dash_shuffle[player]:
|
elif not self.options.dash_shuffle:
|
||||||
skipped_items.append("Dash Ability")
|
skipped_items.append("Dash Ability")
|
||||||
|
|
||||||
if world.wall_climb_shuffle[player]:
|
if self.options.wall_climb_shuffle:
|
||||||
skipped_items.append(to_remove[removed])
|
skipped_items.append(to_remove[removed])
|
||||||
removed += 1
|
removed += 1
|
||||||
elif not world.wall_climb_shuffle[player]:
|
elif not self.options.wall_climb_shuffle:
|
||||||
skipped_items.append("Wall Climb Ability")
|
skipped_items.append("Wall Climb Ability")
|
||||||
|
|
||||||
if not world.reliquary_shuffle[player]:
|
if not self.options.reliquary_shuffle:
|
||||||
skipped_items.extend(reliquary_set)
|
skipped_items.extend(reliquary_set)
|
||||||
elif world.reliquary_shuffle[player]:
|
elif self.options.reliquary_shuffle:
|
||||||
for i in range(3):
|
for _ in range(3):
|
||||||
skipped_items.append(to_remove[removed])
|
skipped_items.append(to_remove[removed])
|
||||||
removed += 1
|
removed += 1
|
||||||
|
|
||||||
if not world.boots_of_pleading[player]:
|
if not self.options.boots_of_pleading:
|
||||||
skipped_items.append("Boots of Pleading")
|
skipped_items.append("Boots of Pleading")
|
||||||
|
|
||||||
if not world.purified_hand[player]:
|
if not self.options.purified_hand:
|
||||||
skipped_items.append("Purified Hand of the Nun")
|
skipped_items.append("Purified Hand of the Nun")
|
||||||
|
|
||||||
if world.start_wheel[player]:
|
if self.options.start_wheel:
|
||||||
skipped_items.append("The Young Mason's Wheel")
|
skipped_items.append("The Young Mason's Wheel")
|
||||||
|
|
||||||
if not world.skill_randomizer[player]:
|
if not self.options.skill_randomizer:
|
||||||
skipped_items.extend(skill_dict.values())
|
skipped_items.extend(skill_dict.values())
|
||||||
|
|
||||||
counter = Counter(skipped_items)
|
counter = Counter(skipped_items)
|
||||||
@@ -208,184 +191,140 @@ class BlasphemousWorld(World):
|
|||||||
if count <= 0:
|
if count <= 0:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
for i in range(count):
|
for _ 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()))
|
||||||
|
|
||||||
world.itempool += pool
|
self.multiworld.itempool += pool
|
||||||
|
|
||||||
|
|
||||||
def pre_fill(self):
|
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 world.thorn_shuffle[player] == 2:
|
if self.options.thorn_shuffle == "vanilla":
|
||||||
self.place_items_from_set(thorn_set, "Thorn Upgrade")
|
self.place_items_from_set(thorn_set, "Thorn Upgrade")
|
||||||
|
|
||||||
if world.start_wheel[player]:
|
if self.options.start_wheel:
|
||||||
world.get_location("Beginning gift", player)\
|
self.get_location("Beginning gift").place_locked_item(self.create_item("The Young Mason's Wheel"))
|
||||||
.place_locked_item(self.create_item("The Young Mason's Wheel"))
|
|
||||||
|
|
||||||
if not world.skill_randomizer[player]:
|
if not self.options.skill_randomizer:
|
||||||
self.place_items_from_dict(skill_dict)
|
self.place_items_from_dict(skill_dict)
|
||||||
|
|
||||||
if world.thorn_shuffle[player] == 1:
|
if self.options.thorn_shuffle == "local_only":
|
||||||
world.local_items[player].value.add("Thorn Upgrade")
|
self.options.local_items.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.multiworld.get_location(loc, self.player)\
|
self.get_location(loc).place_locked_item(self.create_item(name))
|
||||||
.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.multiworld.get_location(loc, self.player)\
|
self.get_location(loc).place_locked_item(self.create_item(item))
|
||||||
.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] = []
|
||||||
|
|
||||||
|
for r in regions:
|
||||||
|
multiworld.regions.append(Region(r["name"], player, multiworld))
|
||||||
|
created_regions.append(r["name"])
|
||||||
|
|
||||||
|
self.get_region("Menu").add_exits({self.start_room: "New Game"})
|
||||||
|
|
||||||
|
blas_logic = BlasRules(self)
|
||||||
|
|
||||||
|
for r in regions:
|
||||||
|
region = self.get_region(r["name"])
|
||||||
|
|
||||||
|
for e in r["exits"]:
|
||||||
|
region.add_exits({e["target"]}, {e["target"]: blas_logic.load_rule(True, r["name"], e)})
|
||||||
|
|
||||||
|
for l in [l for l in r["locations"] if l not in self.disabled_locations]:
|
||||||
|
region.add_locations({location_names[l]: self.location_name_to_id[location_names[l]]}, BlasphemousLocation)
|
||||||
|
|
||||||
|
for t in r["transitions"]:
|
||||||
|
if t == r["name"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if t in created_regions:
|
||||||
|
region.add_exits({t})
|
||||||
|
else:
|
||||||
|
multiworld.regions.append(Region(t, player, multiworld))
|
||||||
|
created_regions.append(t)
|
||||||
|
region.add_exits({t})
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
menu_region = Region("Menu", player, world)
|
victory = Location(player, "His Holiness Escribar", None, self.get_region("D07Z01S03[W]"))
|
||||||
misc_region = Region("Misc", player, world)
|
|
||||||
world.regions += [menu_region, misc_region]
|
|
||||||
|
|
||||||
for room in room_table:
|
|
||||||
region = Region(room, player, world)
|
|
||||||
world.regions.append(region)
|
|
||||||
|
|
||||||
menu_region.add_exits({self.start_room: "New Game"})
|
|
||||||
world.get_region(self.start_room, player).add_exits({"Misc": "Misc"})
|
|
||||||
|
|
||||||
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"]
|
|
||||||
|
|
||||||
parent_region: Region = self.get_room_from_door(door["Id"])
|
|
||||||
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 index, loc in enumerate(location_table):
|
|
||||||
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
|
|
||||||
|
|
||||||
region: Region = world.get_region(loc["room"], player)
|
|
||||||
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 e, r in event_table.items():
|
|
||||||
region: Region = world.get_region(r, player)
|
|
||||||
event = BlasphemousLocation(player, e, None, region)
|
|
||||||
event.show_in_spoiler = False
|
|
||||||
event.place_locked_item(self.create_event(e))
|
|
||||||
region.locations.append(event)
|
|
||||||
|
|
||||||
for door in door_table:
|
|
||||||
region: Region = self.get_room_from_door(self.door_connections[door["Id"]])
|
|
||||||
event = BlasphemousLocation(player, door["Id"], None, region)
|
|
||||||
event.show_in_spoiler = False
|
|
||||||
event.place_locked_item(self.create_event(door["Id"]))
|
|
||||||
region.locations.append(event)
|
|
||||||
|
|
||||||
victory = Location(player, "His Holiness Escribar", None, world.get_region("D07Z01S03", player))
|
|
||||||
victory.place_locked_item(self.create_event("Victory"))
|
victory.place_locked_item(self.create_event("Victory"))
|
||||||
world.get_region("D07Z01S03", player).locations.append(victory)
|
self.get_region("D07Z01S03[W]").locations.append(victory)
|
||||||
|
|
||||||
if world.ending[self.player].value == 1:
|
if self.options.ending == "ending_a":
|
||||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
|
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
|
||||||
elif world.ending[self.player].value == 2:
|
elif self.options.ending == "ending_c":
|
||||||
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))
|
||||||
|
|
||||||
world.completion_condition[self.player] = lambda state: state.has("Victory", player)
|
multiworld.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 world.thorn_shuffle[player].value == 2:
|
if self.options.thorn_shuffle == "vanilla":
|
||||||
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": world.difficulty[player].value,
|
"LogicDifficulty": self.options.difficulty.value,
|
||||||
"StartingLocation": world.starting_location[player].value,
|
"StartingLocation": self.options.starting_location.value,
|
||||||
"VersionCreated": "AP",
|
"VersionCreated": "AP",
|
||||||
|
|
||||||
"UnlockTeleportation": bool(world.prie_dieu_warp[player].value),
|
"UnlockTeleportation": bool(self.options.prie_dieu_warp.value),
|
||||||
"AllowHints": bool(world.corpse_hints[player].value),
|
"AllowHints": bool(self.options.corpse_hints.value),
|
||||||
"AllowPenitence": bool(world.penitence[player].value),
|
"AllowPenitence": bool(self.options.penitence.value),
|
||||||
|
|
||||||
"ShuffleReliquaries": bool(world.reliquary_shuffle[player].value),
|
"ShuffleReliquaries": bool(self.options.reliquary_shuffle.value),
|
||||||
"ShuffleBootsOfPleading": bool(world.boots_of_pleading[player].value),
|
"ShuffleBootsOfPleading": bool(self.options.boots_of_pleading.value),
|
||||||
"ShufflePurifiedHand": bool(world.purified_hand[player].value),
|
"ShufflePurifiedHand": bool(self.options.purified_hand.value),
|
||||||
"ShuffleDash": bool(world.dash_shuffle[player].value),
|
"ShuffleDash": bool(self.options.dash_shuffle.value),
|
||||||
"ShuffleWallClimb": bool(world.wall_climb_shuffle[player].value),
|
"ShuffleWallClimb": bool(self.options.wall_climb_shuffle.value),
|
||||||
|
|
||||||
"ShuffleSwordSkills": bool(world.skill_randomizer[player].value),
|
"ShuffleSwordSkills": bool(self.options.wall_climb_shuffle.value),
|
||||||
"ShuffleThorns": thorns,
|
"ShuffleThorns": thorns,
|
||||||
"JunkLongQuests": bool(world.skip_long_quests[player].value),
|
"JunkLongQuests": bool(self.options.skip_long_quests.value),
|
||||||
"StartWithWheel": bool(world.start_wheel[player].value),
|
"StartWithWheel": bool(self.options.start_wheel.value),
|
||||||
|
|
||||||
"EnemyShuffleType": world.enemy_randomizer[player].value,
|
"EnemyShuffleType": self.options.enemy_randomizer.value,
|
||||||
"MaintainClass": bool(world.enemy_groups[player].value),
|
"MaintainClass": bool(self.options.enemy_groups.value),
|
||||||
"AreaScaling": bool(world.enemy_scaling[player].value),
|
"AreaScaling": bool(self.options.enemy_scaling.value),
|
||||||
|
|
||||||
"BossShuffleType": 0,
|
"BossShuffleType": 0,
|
||||||
"DoorShuffleType": 0
|
"DoorShuffleType": 0
|
||||||
}
|
}
|
||||||
|
|
||||||
slot_data = {
|
slot_data = {
|
||||||
"locations": locations,
|
"locationinfo": [{"gameId": loc, "apId": (base_id + index)} for index, loc in enumerate(location_names)],
|
||||||
"doors": doors,
|
"doors": doors,
|
||||||
"cfg": config,
|
"cfg": config,
|
||||||
"ending": world.ending[self.player].value,
|
"ending": self.options.ending.value,
|
||||||
"death_link": bool(world.death_link[self.player].value)
|
"death_link": bool(self.options.death_link.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return slot_data
|
return slot_data
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user