mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-20 14:28:18 -07:00
Compare commits
74 Commits
0-6-0-rc1
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ead8efbc24 | ||
|
|
aa1180e0aa | ||
|
|
34795b598a | ||
|
|
efd5004330 | ||
|
|
c799531105 | ||
|
|
5c1ded1fe9 | ||
|
|
b2162bb8e6 | ||
|
|
f1769a8d00 | ||
|
|
f520c1d9f2 | ||
|
|
910369a7f8 | ||
|
|
dbf6b6f935 | ||
|
|
e9c463c897 | ||
|
|
f4e43ca9e0 | ||
|
|
a298be9c41 | ||
|
|
18bcaa85a2 | ||
|
|
359f45d50f | ||
|
|
f5c574c37a | ||
|
|
f75a1ae117 | ||
|
|
768ccffe72 | ||
|
|
f6668997e6 | ||
|
|
db11c620a7 | ||
|
|
da48af60dc | ||
|
|
19faaa4104 | ||
|
|
628252896e | ||
|
|
f28aff6f9a | ||
|
|
894732be47 | ||
|
|
051518e72a | ||
|
|
b7b78dead3 | ||
|
|
d1167027f4 | ||
|
|
445c9b22d6 | ||
|
|
67e8877143 | ||
|
|
1fe8024b43 | ||
|
|
8e14e463e4 | ||
|
|
b8666b2562 | ||
|
|
57afdfda6f | ||
|
|
738c21c625 | ||
|
|
41898ed640 | ||
|
|
1ebc9e2ec0 | ||
|
|
9466d5274e | ||
|
|
a53bcb4697 | ||
|
|
8c5592e406 | ||
|
|
41055cd963 | ||
|
|
43874b1d28 | ||
|
|
b570aa2ec6 | ||
|
|
c43233120a | ||
|
|
57a571cc11 | ||
|
|
8622cb6204 | ||
|
|
90417e0022 | ||
|
|
96b941ed35 | ||
|
|
1832bac1a3 | ||
|
|
86641223c1 | ||
|
|
cc770418f2 | ||
|
|
513e361764 | ||
|
|
ddf7fdccc7 | ||
|
|
3df2dbe051 | ||
|
|
3d1d6908c8 | ||
|
|
7474c27372 | ||
|
|
bb0948154d | ||
|
|
fa2816822b | ||
|
|
5a42c70675 | ||
|
|
949527f9cb | ||
|
|
1a1b7e9cf4 | ||
|
|
edacb17171 | ||
|
|
33fd9de281 | ||
|
|
a126dee068 | ||
|
|
e2b942139a | ||
|
|
823b17c386 | ||
|
|
05d1b2129a | ||
|
|
436c0a4104 | ||
|
|
96f469c737 | ||
|
|
4f77abac4f | ||
|
|
d5cd95c7fb | ||
|
|
a2fbf856ff | ||
|
|
4fa8c43266 |
@@ -869,21 +869,40 @@ class CollectionState():
|
||||
def has(self, item: str, player: int, count: int = 1) -> bool:
|
||||
return self.prog_items[player][item] >= count
|
||||
|
||||
# for loops are specifically used in all/any/count methods, instead of all()/any()/sum(), to avoid the overhead of
|
||||
# creating and iterating generator instances. In `return all(player_prog_items[item] for item in items)`, the
|
||||
# argument to all() would be a new generator instance, for example.
|
||||
def has_all(self, items: Iterable[str], player: int) -> bool:
|
||||
"""Returns True if each item name of items is in state at least once."""
|
||||
return all(self.prog_items[player][item] for item in items)
|
||||
player_prog_items = self.prog_items[player]
|
||||
for item in items:
|
||||
if not player_prog_items[item]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def has_any(self, items: Iterable[str], player: int) -> bool:
|
||||
"""Returns True if at least one item name of items is in state at least once."""
|
||||
return any(self.prog_items[player][item] for item in items)
|
||||
player_prog_items = self.prog_items[player]
|
||||
for item in items:
|
||||
if player_prog_items[item]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_all_counts(self, item_counts: Mapping[str, int], player: int) -> bool:
|
||||
"""Returns True if each item name is in the state at least as many times as specified."""
|
||||
return all(self.prog_items[player][item] >= count for item, count in item_counts.items())
|
||||
player_prog_items = self.prog_items[player]
|
||||
for item, count in item_counts.items():
|
||||
if player_prog_items[item] < count:
|
||||
return False
|
||||
return True
|
||||
|
||||
def has_any_count(self, item_counts: Mapping[str, int], player: int) -> bool:
|
||||
"""Returns True if at least one item name is in the state at least as many times as specified."""
|
||||
return any(self.prog_items[player][item] >= count for item, count in item_counts.items())
|
||||
player_prog_items = self.prog_items[player]
|
||||
for item, count in item_counts.items():
|
||||
if player_prog_items[item] >= count:
|
||||
return True
|
||||
return False
|
||||
|
||||
def count(self, item: str, player: int) -> int:
|
||||
return self.prog_items[player][item]
|
||||
@@ -911,11 +930,20 @@ class CollectionState():
|
||||
|
||||
def count_from_list(self, items: Iterable[str], player: int) -> int:
|
||||
"""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)
|
||||
player_prog_items = self.prog_items[player]
|
||||
total = 0
|
||||
for item_name in items:
|
||||
total += player_prog_items[item_name]
|
||||
return total
|
||||
|
||||
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."""
|
||||
return sum(self.prog_items[player][item_name] > 0 for item_name in items)
|
||||
player_prog_items = self.prog_items[player]
|
||||
total = 0
|
||||
for item_name in items:
|
||||
if player_prog_items[item_name] > 0:
|
||||
total += 1
|
||||
return total
|
||||
|
||||
# item name group related
|
||||
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
|
||||
|
||||
@@ -709,8 +709,16 @@ class CommonContext:
|
||||
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
|
||||
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
|
||||
|
||||
def make_gui(self) -> typing.Type["kvui.GameManager"]:
|
||||
"""To return the Kivy App class needed for run_gui so it can be overridden before being built"""
|
||||
def make_gui(self) -> "type[kvui.GameManager]":
|
||||
"""
|
||||
To return the Kivy `App` class needed for `run_gui` so it can be overridden before being built
|
||||
|
||||
Common changes are changing `base_title` to update the window title of the client and
|
||||
updating `logging_pairs` to automatically make new tabs that can be filled with their respective logger.
|
||||
|
||||
ex. `logging_pairs.append(("Foo", "Bar"))`
|
||||
will add a "Bar" tab which follows the logger returned from `logging.getLogger("Foo")`
|
||||
"""
|
||||
from kvui import GameManager
|
||||
|
||||
class TextManager(GameManager):
|
||||
@@ -899,6 +907,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
ctx.disconnected_intentionally = True
|
||||
ctx.event_invalid_game()
|
||||
elif 'IncompatibleVersion' in errors:
|
||||
ctx.disconnected_intentionally = True
|
||||
raise Exception('Server reported your client version as incompatible. '
|
||||
'This probably means you have to update.')
|
||||
elif 'InvalidItemsHandling' in errors:
|
||||
@@ -1087,7 +1096,7 @@ def run_as_textclient(*args):
|
||||
if password_requested and not self.password:
|
||||
await super(TextContext, self).server_auth(password_requested)
|
||||
await self.get_username()
|
||||
await self.send_connect()
|
||||
await self.send_connect(game="")
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == "Connected":
|
||||
|
||||
8
Fill.py
8
Fill.py
@@ -502,7 +502,13 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
# "priority fill"
|
||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
|
||||
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
|
||||
name="Priority", one_item_per_player=False)
|
||||
name="Priority", one_item_per_player=True, allow_partial=True)
|
||||
|
||||
if prioritylocations:
|
||||
# retry with one_item_per_player off because some priority fills can fail to fill with that optimization
|
||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
|
||||
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
|
||||
name="Priority Retry", one_item_per_player=False)
|
||||
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
||||
defaultlocations = prioritylocations + defaultlocations
|
||||
|
||||
|
||||
@@ -33,10 +33,15 @@ WINDOW_MIN_HEIGHT = 525
|
||||
WINDOW_MIN_WIDTH = 425
|
||||
|
||||
class AdjusterWorld(object):
|
||||
class AdjusterSubWorld(object):
|
||||
def __init__(self, random):
|
||||
self.random = random
|
||||
|
||||
def __init__(self, sprite_pool):
|
||||
import random
|
||||
self.sprite_pool = {1: sprite_pool}
|
||||
self.per_slot_randoms = {1: random}
|
||||
self.worlds = {1: self.AdjusterSubWorld(random)}
|
||||
|
||||
|
||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||
|
||||
3
Main.py
3
Main.py
@@ -148,7 +148,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
else:
|
||||
multiworld.worlds[1].options.non_local_items.value = set()
|
||||
multiworld.worlds[1].options.local_items.value = set()
|
||||
|
||||
|
||||
AutoWorld.call_all(multiworld, "connect_entrances")
|
||||
AutoWorld.call_all(multiworld, "generate_basic")
|
||||
|
||||
# remove starting inventory from pool items.
|
||||
|
||||
@@ -28,9 +28,11 @@ ModuleUpdate.update()
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import ssl
|
||||
from NetUtils import ServerConnection
|
||||
|
||||
import websockets
|
||||
import colorama
|
||||
import websockets
|
||||
from websockets.extensions.permessage_deflate import PerMessageDeflate
|
||||
try:
|
||||
# ponyorm is a requirement for webhost, not default server, so may not be importable
|
||||
from pony.orm.dbapiprovider import OperationalError
|
||||
@@ -119,13 +121,14 @@ def get_saving_second(seed_name: str, interval: int = 60) -> int:
|
||||
|
||||
class Client(Endpoint):
|
||||
version = Version(0, 0, 0)
|
||||
tags: typing.List[str] = []
|
||||
tags: typing.List[str]
|
||||
remote_items: bool
|
||||
remote_start_inventory: bool
|
||||
no_items: bool
|
||||
no_locations: bool
|
||||
no_text: bool
|
||||
|
||||
def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context):
|
||||
def __init__(self, socket: "ServerConnection", ctx: Context) -> None:
|
||||
super().__init__(socket)
|
||||
self.auth = False
|
||||
self.team = None
|
||||
@@ -175,6 +178,7 @@ class Context:
|
||||
"compatibility": int}
|
||||
# team -> slot id -> list of clients authenticated to slot.
|
||||
clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]]
|
||||
endpoints: list[Client]
|
||||
locations: LocationStore # typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
|
||||
location_checks: typing.Dict[typing.Tuple[int, int], typing.Set[int]]
|
||||
hints_used: typing.Dict[typing.Tuple[int, int], int]
|
||||
@@ -364,18 +368,28 @@ class Context:
|
||||
return True
|
||||
|
||||
def broadcast_all(self, msgs: typing.List[dict]):
|
||||
msgs = self.dumper(msgs)
|
||||
endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth)
|
||||
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
|
||||
msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs)
|
||||
data = self.dumper(msgs)
|
||||
endpoints = (
|
||||
endpoint
|
||||
for endpoint in self.endpoints
|
||||
if endpoint.auth and not (msg_is_text and endpoint.no_text)
|
||||
)
|
||||
async_start(self.broadcast_send_encoded_msgs(endpoints, data))
|
||||
|
||||
def broadcast_text_all(self, text: str, additional_arguments: dict = {}):
|
||||
self.logger.info("Notice (all): %s" % text)
|
||||
self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
|
||||
|
||||
def broadcast_team(self, team: int, msgs: typing.List[dict]):
|
||||
msgs = self.dumper(msgs)
|
||||
endpoints = (endpoint for endpoint in itertools.chain.from_iterable(self.clients[team].values()))
|
||||
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
|
||||
msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs)
|
||||
data = self.dumper(msgs)
|
||||
endpoints = (
|
||||
endpoint
|
||||
for endpoint in itertools.chain.from_iterable(self.clients[team].values())
|
||||
if not (msg_is_text and endpoint.no_text)
|
||||
)
|
||||
async_start(self.broadcast_send_encoded_msgs(endpoints, data))
|
||||
|
||||
def broadcast(self, endpoints: typing.Iterable[Client], msgs: typing.List[dict]):
|
||||
msgs = self.dumper(msgs)
|
||||
@@ -389,13 +403,13 @@ class Context:
|
||||
await on_client_disconnected(self, endpoint)
|
||||
|
||||
def notify_client(self, client: Client, text: str, additional_arguments: dict = {}):
|
||||
if not client.auth:
|
||||
if not client.auth or client.no_text:
|
||||
return
|
||||
self.logger.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
||||
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}]))
|
||||
|
||||
def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}):
|
||||
if not client.auth:
|
||||
if not client.auth or client.no_text:
|
||||
return
|
||||
async_start(self.send_msgs(client,
|
||||
[{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}
|
||||
@@ -760,7 +774,7 @@ class Context:
|
||||
self.on_new_hint(team, slot)
|
||||
for slot, hint_data in concerns.items():
|
||||
if recipients is None or slot in recipients:
|
||||
clients = self.clients[team].get(slot)
|
||||
clients = filter(lambda c: not c.no_text, self.clients[team].get(slot, []))
|
||||
if not clients:
|
||||
continue
|
||||
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player != slot)]
|
||||
@@ -769,7 +783,7 @@ class Context:
|
||||
|
||||
def get_hint(self, team: int, finding_player: int, seeked_location: int) -> typing.Optional[Hint]:
|
||||
for hint in self.hints[team, finding_player]:
|
||||
if hint.location == seeked_location:
|
||||
if hint.location == seeked_location and hint.finding_player == finding_player:
|
||||
return hint
|
||||
return None
|
||||
|
||||
@@ -819,7 +833,7 @@ def update_aliases(ctx: Context, team: int):
|
||||
async_start(ctx.send_encoded_msgs(client, cmd))
|
||||
|
||||
|
||||
async def server(websocket, path: str = "/", ctx: Context = None):
|
||||
async def server(websocket: "ServerConnection", path: str = "/", ctx: Context = None) -> None:
|
||||
client = Client(websocket, ctx)
|
||||
ctx.endpoints.append(client)
|
||||
|
||||
@@ -910,6 +924,10 @@ async def on_client_joined(ctx: Context, client: Client):
|
||||
"If your client supports it, "
|
||||
"you may have additional local commands you can list with /help.",
|
||||
{"type": "Tutorial"})
|
||||
if not any(isinstance(extension, PerMessageDeflate) for extension in client.socket.extensions):
|
||||
ctx.notify_client(client, "Warning: your client does not support compressed websocket connections! "
|
||||
"It may stop working in the future. If you are a player, please report this to the "
|
||||
"client's developer.")
|
||||
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
|
||||
@@ -1060,21 +1078,37 @@ def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem
|
||||
|
||||
def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int],
|
||||
count_activity: bool = True):
|
||||
slot_locations = ctx.locations[slot]
|
||||
new_locations = set(locations) - ctx.location_checks[team, slot]
|
||||
new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata
|
||||
new_locations.intersection_update(slot_locations) # ignore location IDs unknown to this multidata
|
||||
if new_locations:
|
||||
if count_activity:
|
||||
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
sortable: list[tuple[int, int, int, int]] = []
|
||||
for location in new_locations:
|
||||
item_id, target_player, flags = ctx.locations[slot][location]
|
||||
# extract all fields to avoid runtime overhead in LocationStore
|
||||
item_id, target_player, flags = slot_locations[location]
|
||||
# sort/group by receiver and item
|
||||
sortable.append((target_player, item_id, location, flags))
|
||||
|
||||
info_texts: list[dict[str, typing.Any]] = []
|
||||
for target_player, item_id, location, flags in sorted(sortable):
|
||||
new_item = NetworkItem(item_id, location, slot, flags)
|
||||
send_items_to(ctx, team, target_player, new_item)
|
||||
|
||||
ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % (
|
||||
team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id],
|
||||
ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location]))
|
||||
info_text = json_format_send_event(new_item, target_player)
|
||||
ctx.broadcast_team(team, [info_text])
|
||||
if len(info_texts) >= 140:
|
||||
# split into chunks that are close to compression window of 64K but not too big on the wire
|
||||
# (roughly 1300-2600 bytes after compression depending on repetitiveness)
|
||||
ctx.broadcast_team(team, info_texts)
|
||||
info_texts.clear()
|
||||
info_texts.append(json_format_send_event(new_item, target_player))
|
||||
ctx.broadcast_team(team, info_texts)
|
||||
del info_texts
|
||||
del sortable
|
||||
|
||||
ctx.location_checks[team, slot] |= new_locations
|
||||
send_new_items(ctx)
|
||||
@@ -1101,7 +1135,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
|
||||
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
|
||||
for finding_player, location_id, item_id, receiving_player, item_flags \
|
||||
in ctx.locations.find_item(slots, seeked_item_id):
|
||||
prev_hint = ctx.get_hint(team, slot, location_id)
|
||||
prev_hint = ctx.get_hint(team, finding_player, location_id)
|
||||
if prev_hint:
|
||||
hints.append(prev_hint)
|
||||
else:
|
||||
@@ -1787,7 +1821,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
ctx.clients[team][slot].append(client)
|
||||
client.version = args['version']
|
||||
client.tags = args['tags']
|
||||
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
|
||||
client.no_locations = "TextOnly" in client.tags or "Tracker" in client.tags
|
||||
# set NoText for old PopTracker clients that predate the tag to save traffic
|
||||
client.no_text = "NoText" in client.tags or ("PopTracker" in client.tags and client.version < (0, 5, 1))
|
||||
connected_packet = {
|
||||
"cmd": "Connected",
|
||||
"team": client.team, "slot": client.slot,
|
||||
@@ -1860,6 +1896,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
client.tags = args["tags"]
|
||||
if set(old_tags) != set(client.tags):
|
||||
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
|
||||
client.no_text = "NoText" in client.tags or (
|
||||
"PopTracker" in client.tags and client.version < (0, 5, 1)
|
||||
)
|
||||
ctx.broadcast_text_all(
|
||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags "
|
||||
f"from {old_tags} to {client.tags}.",
|
||||
|
||||
@@ -5,17 +5,18 @@ import enum
|
||||
import warnings
|
||||
from json import JSONEncoder, JSONDecoder
|
||||
|
||||
import websockets
|
||||
if typing.TYPE_CHECKING:
|
||||
from websockets import WebSocketServerProtocol as ServerConnection
|
||||
|
||||
from Utils import ByValue, Version
|
||||
|
||||
|
||||
class HintStatus(ByValue, enum.IntEnum):
|
||||
HINT_FOUND = 0
|
||||
HINT_UNSPECIFIED = 1
|
||||
HINT_UNSPECIFIED = 0
|
||||
HINT_NO_PRIORITY = 10
|
||||
HINT_AVOID = 20
|
||||
HINT_PRIORITY = 30
|
||||
HINT_FOUND = 40
|
||||
|
||||
|
||||
class JSONMessagePart(typing.TypedDict, total=False):
|
||||
@@ -151,7 +152,7 @@ decode = JSONDecoder(object_hook=_object_hook).decode
|
||||
|
||||
|
||||
class Endpoint:
|
||||
socket: websockets.WebSocketServerProtocol
|
||||
socket: "ServerConnection"
|
||||
|
||||
def __init__(self, socket):
|
||||
self.socket = socket
|
||||
|
||||
@@ -1582,7 +1582,7 @@ def dump_player_options(multiworld: MultiWorld) -> None:
|
||||
}
|
||||
output.append(player_output)
|
||||
for option_key, option in world.options_dataclass.type_hints.items():
|
||||
if issubclass(Removed, option):
|
||||
if option.visibility == Visibility.none:
|
||||
continue
|
||||
display_name = getattr(option, "display_name", option_key)
|
||||
player_output[display_name] = getattr(world.options, option_key).current_option_name
|
||||
|
||||
@@ -22,7 +22,7 @@ players to rely upon each other to complete their game.
|
||||
|
||||
While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows
|
||||
players to randomize any of the supported games, and send items between them. This allows players of different
|
||||
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworld.
|
||||
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworlds.
|
||||
Here is a list of our [Supported Games](https://archipelago.gg/games).
|
||||
|
||||
## Can I generate a single-player game with Archipelago?
|
||||
|
||||
@@ -370,19 +370,13 @@ target_group_lookup = bake_target_group_lookup(world, get_target_groups)
|
||||
|
||||
#### When to call `randomize_entrances`
|
||||
|
||||
The short answer is that you will almost always want to do ER in `pre_fill`. For more information why, continue reading.
|
||||
The correct step for this is `World.connect_entrances`.
|
||||
|
||||
ER begins by collecting the entire item pool and then uses your access rules to try and prevent some kinds of failures.
|
||||
This means 2 things about when you can call ER:
|
||||
1. You must supply your item pool before calling ER, or call ER before setting any rules which require items.
|
||||
2. If you have rules dependent on anything other than items (e.g. `Entrance`s or events), you must set your rules
|
||||
and create your events before you call ER if you want to guarantee a correct output.
|
||||
|
||||
If the conditions above are met, you could theoretically do ER as early as `create_regions`. However, plando is also
|
||||
a consideration. Since item plando happens between `set_rules` and `pre_fill` and modifies the item pool, doing ER
|
||||
in `pre_fill` is the only way to account for placements made by item plando, otherwise you risk impossible seeds or
|
||||
generation failures. Obviously, if your world implements entrance plando, you will likely want to do that before ER as
|
||||
well.
|
||||
Currently, you could theoretically do it as early as `World.create_regions` or as late as `pre_fill`.
|
||||
However, there are upcoming changes to Item Plando and Generic Entrance Randomizer to make the two features work better
|
||||
together.
|
||||
These changes necessitate that entrance randomization is done exactly in `World.connect_entrances`.
|
||||
It is fine for your Entrances to be connected differently or not at all before this step.
|
||||
|
||||
#### Informing your client about randomized entrances
|
||||
|
||||
|
||||
@@ -47,6 +47,9 @@ Packets are simple JSON lists in which any number of ordered network commands ca
|
||||
|
||||
An object can contain the "class" key, which will tell the content data type, such as "Version" in the following example.
|
||||
|
||||
Websocket connections should support per-message compression. Uncompressed connections are deprecated and may stop
|
||||
working in the future.
|
||||
|
||||
Example:
|
||||
```javascript
|
||||
[{"cmd": "RoomInfo", "version": {"major": 0, "minor": 1, "build": 3, "class": "Version"}, "tags": ["WebHost"], ... }]
|
||||
@@ -360,11 +363,11 @@ An enumeration containing the possible hint states.
|
||||
```python
|
||||
import enum
|
||||
class HintStatus(enum.IntEnum):
|
||||
HINT_FOUND = 0 # The location has been collected. Status cannot be changed once found.
|
||||
HINT_UNSPECIFIED = 1 # The receiving player has not specified any status
|
||||
HINT_UNSPECIFIED = 0 # The receiving player has not specified any status
|
||||
HINT_NO_PRIORITY = 10 # The receiving player has specified that the item is unneeded
|
||||
HINT_AVOID = 20 # The receiving player has specified that the item is detrimental
|
||||
HINT_PRIORITY = 30 # The receiving player has specified that the item is needed
|
||||
HINT_FOUND = 40 # The location has been collected. Status cannot be changed once found.
|
||||
```
|
||||
- Hints for items with `ItemClassification.trap` default to `HINT_AVOID`.
|
||||
- Hints created with `LocationScouts`, `!hint_location`, or similar (hinting a location) default to `HINT_UNSPECIFIED`.
|
||||
@@ -530,9 +533,9 @@ In JSON this may look like:
|
||||
{"item": 3, "location": 3, "player": 3, "flags": 0}
|
||||
]
|
||||
```
|
||||
`item` is the item id of the item. Item ids are only supported in the range of [-2<sup>53</sup>, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
|
||||
`item` is the item id of the item. Item ids are only supported in the range of [-2<sup>53</sup> + 1, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
|
||||
|
||||
`location` is the location id of the item inside the world. Location ids are only supported in the range of [-2<sup>53</sup>, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
|
||||
`location` is the location id of the item inside the world. Location ids are only supported in the range of [-2<sup>53</sup> + 1, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
|
||||
|
||||
`player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item
|
||||
|
||||
@@ -745,6 +748,7 @@ Tags are represented as a list of strings, the common client tags follow:
|
||||
| HintGame | Indicates the client is a hint game, made to send hints instead of locations. Special join/leave message,¹ `game` is optional.² |
|
||||
| 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.² |
|
||||
| NoText | Indicates the client does not want to receive text messages, improving performance if not needed. |
|
||||
|
||||
¹: 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.
|
||||
|
||||
@@ -222,8 +222,8 @@ could also be progress in a research tree, or even something more abstract like
|
||||
|
||||
Each location has a `name` and an `address` (hereafter referred to as an `id`), is placed in a Region, has access rules,
|
||||
and has a classification. The name needs to be unique within each game and must not be numeric (must contain least 1
|
||||
letter or symbol). The ID needs to be unique across all games, and is best kept in the same range as the item IDs.
|
||||
Locations and items can share IDs, so typically a game's locations and items start at the same ID.
|
||||
letter or symbol). The ID needs to be unique across all locations within the game.
|
||||
Locations and items can share IDs, and locations can share IDs with other games' locations.
|
||||
|
||||
World-specific IDs must be in the range 1 to 2<sup>53</sup>-1; IDs ≤ 0 are global and reserved.
|
||||
|
||||
@@ -243,7 +243,9 @@ progression. Progression items will be assigned to locations with higher priorit
|
||||
and satisfy progression balancing.
|
||||
|
||||
The name needs to be unique within each game, meaning if you need to create multiple items with the same name, they
|
||||
will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol).
|
||||
will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol).
|
||||
The ID thus also needs to be unique across all items with different names within the game.
|
||||
Items and locations can share IDs, and items can share IDs with other games' items.
|
||||
|
||||
Other classifications include:
|
||||
|
||||
@@ -490,6 +492,9 @@ In addition, the following methods can be implemented and are called in this ord
|
||||
after this step. Locations cannot be moved to different regions after this step.
|
||||
* `set_rules(self)`
|
||||
called to set access and item rules on locations and entrances.
|
||||
* `connect_entrances(self)`
|
||||
by the end of this step, all entrances must exist and be connected to their source and target regions.
|
||||
Entrance randomization should be done here.
|
||||
* `generate_basic(self)`
|
||||
player-specific randomization that does not affect logic can be done here.
|
||||
* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)`
|
||||
@@ -557,17 +562,13 @@ from .items import is_progression # this is just a dummy
|
||||
|
||||
def create_item(self, item: str) -> MyGameItem:
|
||||
# this is called when AP wants to create an item by name (for plando) or when you call it from your own code
|
||||
classification = ItemClassification.progression if is_progression(item) else
|
||||
ItemClassification.filler
|
||||
|
||||
|
||||
return MyGameItem(item, classification, self.item_name_to_id[item],
|
||||
self.player)
|
||||
classification = ItemClassification.progression if is_progression(item) else ItemClassification.filler
|
||||
return MyGameItem(item, classification, self.item_name_to_id[item], self.player)
|
||||
|
||||
|
||||
def create_event(self, event: str) -> MyGameItem:
|
||||
# while we are at it, we can also add a helper to create events
|
||||
return MyGameItem(event, True, None, self.player)
|
||||
return MyGameItem(event, ItemClassification.progression, None, self.player)
|
||||
```
|
||||
|
||||
#### create_items
|
||||
@@ -835,14 +836,16 @@ def generate_output(self, output_directory: str) -> None:
|
||||
|
||||
### Slot Data
|
||||
|
||||
If the game client needs to know information about the generated seed, a preferred method of transferring the data
|
||||
is through the slot data. This is filled with the `fill_slot_data` method of your world by returning
|
||||
a `dict` with `str` keys that can be serialized with json.
|
||||
But, to not waste resources, it should be limited to data that is absolutely necessary. Slot data is sent to your client
|
||||
once it has successfully [connected](network%20protocol.md#connected).
|
||||
If a client or tracker needs to know information about the generated seed, a preferred method of transferring the data
|
||||
is through the slot data. This is filled with the `fill_slot_data` method of your world by returning a `dict` with
|
||||
`str` keys that can be serialized with json. However, to not waste resources, it should be limited to data that is
|
||||
absolutely necessary. Slot data is sent to your client once it has successfully
|
||||
[connected](network%20protocol.md#connected).
|
||||
|
||||
If you need to know information about locations in your world, instead of propagating the slot data, it is preferable
|
||||
to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. The most
|
||||
common usage of slot data is sending option results that the client needs to be aware of.
|
||||
to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. Adding
|
||||
item/location pairs is unnecessary since the AP server already retains and freely gives that information to clients
|
||||
that request it. The most common usage of slot data is sending option results that the client needs to be aware of.
|
||||
|
||||
```python
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
|
||||
@@ -378,13 +378,14 @@ def randomize_entrances(
|
||||
and world.multiworld.has_beaten_game(er_state.collection_state, world.player):
|
||||
# ensure that we have enough locations to place our progression
|
||||
accessible_location_count = 0
|
||||
prog_item_count = sum(er_state.collection_state.prog_items[world.player].values())
|
||||
prog_item_count = len([item for item in world.multiworld.itempool if item.advancement and item.player == world.player])
|
||||
# short-circuit location checking in this case
|
||||
if prog_item_count == 0:
|
||||
return True
|
||||
for region in er_state.placed_regions:
|
||||
for loc in region.locations:
|
||||
if loc.can_reach(er_state.collection_state):
|
||||
if not loc.item and loc.can_reach(er_state.collection_state):
|
||||
# don't count locations with preplaced items
|
||||
accessible_location_count += 1
|
||||
if accessible_location_count >= prog_item_count:
|
||||
perform_validity_check = False
|
||||
|
||||
19
kvui.py
19
kvui.py
@@ -26,6 +26,10 @@ import Utils
|
||||
if Utils.is_frozen():
|
||||
os.environ["KIVY_DATA_DIR"] = Utils.local_path("data")
|
||||
|
||||
import platformdirs
|
||||
os.environ["KIVY_HOME"] = os.path.join(platformdirs.user_config_dir("Archipelago", False), "kivy")
|
||||
os.makedirs(os.environ["KIVY_HOME"], exist_ok=True)
|
||||
|
||||
from kivy.config import Config
|
||||
|
||||
Config.set("input", "mouse", "mouse,disable_multitouch")
|
||||
@@ -440,8 +444,11 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
if child.collide_point(*touch.pos):
|
||||
key = child.sort_key
|
||||
if key == "status":
|
||||
parent.hint_sorter = lambda element: element["status"]["hint"]["status"]
|
||||
else: parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower()
|
||||
parent.hint_sorter = lambda element: status_sort_weights[element["status"]["hint"]["status"]]
|
||||
else:
|
||||
parent.hint_sorter = lambda element: (
|
||||
remove_between_brackets.sub("", element[key]["text"]).lower()
|
||||
)
|
||||
if key == parent.sort_key:
|
||||
# second click reverses order
|
||||
parent.reversed = not parent.reversed
|
||||
@@ -825,7 +832,13 @@ status_colors: typing.Dict[HintStatus, str] = {
|
||||
HintStatus.HINT_AVOID: "salmon",
|
||||
HintStatus.HINT_PRIORITY: "plum",
|
||||
}
|
||||
|
||||
status_sort_weights: dict[HintStatus, int] = {
|
||||
HintStatus.HINT_FOUND: 0,
|
||||
HintStatus.HINT_UNSPECIFIED: 1,
|
||||
HintStatus.HINT_NO_PRIORITY: 2,
|
||||
HintStatus.HINT_AVOID: 3,
|
||||
HintStatus.HINT_PRIORITY: 4,
|
||||
}
|
||||
|
||||
|
||||
class HintLog(RecycleView):
|
||||
|
||||
23
settings.py
23
settings.py
@@ -109,7 +109,7 @@ class Group:
|
||||
def get_type_hints(cls) -> Dict[str, Any]:
|
||||
"""Returns resolved type hints for the class"""
|
||||
if cls._type_cache is None:
|
||||
if not isinstance(next(iter(cls.__annotations__.values())), str):
|
||||
if not cls.__annotations__ or not isinstance(next(iter(cls.__annotations__.values())), str):
|
||||
# non-str: assume already resolved
|
||||
cls._type_cache = cls.__annotations__
|
||||
else:
|
||||
@@ -270,15 +270,20 @@ class Group:
|
||||
# fetch class to avoid going through getattr
|
||||
cls = self.__class__
|
||||
type_hints = cls.get_type_hints()
|
||||
entries = [e for e in self]
|
||||
if not entries:
|
||||
# write empty dict for empty Group with no instance values
|
||||
cls._dump_value({}, f, indent=" " * level)
|
||||
# validate group
|
||||
for name in cls.__annotations__.keys():
|
||||
assert hasattr(cls, name), f"{cls}.{name} is missing a default value"
|
||||
# dump ordered members
|
||||
for name in self:
|
||||
for name in entries:
|
||||
attr = cast(object, getattr(self, name))
|
||||
attr_cls = type_hints[name] if name in type_hints else attr.__class__
|
||||
attr_cls_origin = typing.get_origin(attr_cls)
|
||||
while attr_cls_origin is Union: # resolve to first type for doc string
|
||||
# resolve to first type for doc string
|
||||
while attr_cls_origin is Union or attr_cls_origin is types.UnionType:
|
||||
attr_cls = typing.get_args(attr_cls)[0]
|
||||
attr_cls_origin = typing.get_origin(attr_cls)
|
||||
if attr_cls.__doc__ and attr_cls.__module__ != "builtins":
|
||||
@@ -787,7 +792,17 @@ class Settings(Group):
|
||||
if location:
|
||||
from Utils import parse_yaml
|
||||
with open(location, encoding="utf-8-sig") as f:
|
||||
options = parse_yaml(f.read())
|
||||
from yaml.error import MarkedYAMLError
|
||||
try:
|
||||
options = parse_yaml(f.read())
|
||||
except MarkedYAMLError as ex:
|
||||
if ex.problem_mark:
|
||||
f.seek(0)
|
||||
lines = f.readlines()
|
||||
problem_line = lines[ex.problem_mark.line]
|
||||
error_line = " " * ex.problem_mark.column + "^"
|
||||
raise Exception(f"{ex.context} {ex.problem}\n{problem_line}{error_line}")
|
||||
raise ex
|
||||
# TODO: detect if upgrade is required
|
||||
# TODO: once we have a cache for _world_settings_name_cache, detect if any game section is missing
|
||||
self.update(options or {})
|
||||
|
||||
@@ -18,7 +18,15 @@ def run_locations_benchmark():
|
||||
|
||||
class BenchmarkRunner:
|
||||
gen_steps: typing.Tuple[str, ...] = (
|
||||
"generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
|
||||
"generate_early",
|
||||
"create_regions",
|
||||
"create_items",
|
||||
"set_rules",
|
||||
"connect_entrances",
|
||||
"generate_basic",
|
||||
"pre_fill",
|
||||
)
|
||||
|
||||
rule_iterations: int = 100_000
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
|
||||
@@ -5,7 +5,15 @@ from BaseClasses import CollectionState, Item, ItemClassification, Location, Mul
|
||||
from worlds import network_data_package
|
||||
from worlds.AutoWorld import World, call_all
|
||||
|
||||
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
|
||||
gen_steps = (
|
||||
"generate_early",
|
||||
"create_regions",
|
||||
"create_items",
|
||||
"set_rules",
|
||||
"connect_entrances",
|
||||
"generate_basic",
|
||||
"pre_fill",
|
||||
)
|
||||
|
||||
|
||||
def setup_solo_multiworld(
|
||||
|
||||
@@ -311,6 +311,37 @@ class TestRandomizeEntrances(unittest.TestCase):
|
||||
self.assertEqual([], [exit_ for region in multiworld.get_regions()
|
||||
for exit_ in region.exits if not exit_.connected_region])
|
||||
|
||||
def test_minimal_entrance_rando_with_collect_override(self):
|
||||
"""
|
||||
tests that entrance randomization can complete with minimal accessibility and unreachable exits
|
||||
when the world defines a collect override that add extra values to prog_items
|
||||
"""
|
||||
multiworld = generate_test_multiworld()
|
||||
multiworld.worlds[1].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
|
||||
multiworld.completion_condition[1] = lambda state: state.can_reach("region24", player=1)
|
||||
generate_disconnected_region_grid(multiworld, 5, 1)
|
||||
prog_items = generate_items(10, 1, True)
|
||||
multiworld.itempool += prog_items
|
||||
filler_items = generate_items(15, 1, False)
|
||||
multiworld.itempool += filler_items
|
||||
e = multiworld.get_entrance("region1_right", 1)
|
||||
set_rule(e, lambda state: False)
|
||||
|
||||
old_collect = multiworld.worlds[1].collect
|
||||
|
||||
def new_collect(state, item):
|
||||
old_collect(state, item)
|
||||
state.prog_items[item.player]["counter"] += 300
|
||||
|
||||
multiworld.worlds[1].collect = new_collect
|
||||
|
||||
randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
|
||||
|
||||
self.assertEqual([], [entrance for region in multiworld.get_regions()
|
||||
for entrance in region.entrances if not entrance.parent_region])
|
||||
self.assertEqual([], [exit_ for region in multiworld.get_regions()
|
||||
for exit_ in region.exits if not exit_.connected_region])
|
||||
|
||||
def test_restrictive_region_requirement_does_not_fail(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
generate_disconnected_region_grid(multiworld, 2, 1)
|
||||
|
||||
63
test/general/test_entrances.py
Normal file
63
test/general/test_entrances.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import unittest
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all, World
|
||||
from . import setup_solo_multiworld
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
def test_entrance_connection_steps(self):
|
||||
"""Tests that Entrances are connected and not changed after connect_entrances."""
|
||||
def get_entrance_name_to_source_and_target_dict(world: World):
|
||||
return [
|
||||
(entrance.name, entrance.parent_region, entrance.connected_region)
|
||||
for entrance in world.get_entrances()
|
||||
]
|
||||
|
||||
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances")
|
||||
additional_steps = ("generate_basic", "pre_fill")
|
||||
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game_name=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type, gen_steps)
|
||||
|
||||
original_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])
|
||||
|
||||
self.assertTrue(
|
||||
all(entrance[1] is not None and entrance[2] is not None for entrance in original_entrances),
|
||||
f"{game_name} had unconnected entrances after connect_entrances"
|
||||
)
|
||||
|
||||
for step in additional_steps:
|
||||
with self.subTest("Step", step=step):
|
||||
call_all(multiworld, step)
|
||||
step_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])
|
||||
|
||||
self.assertEqual(
|
||||
original_entrances, step_entrances, f"{game_name} modified entrances during {step}"
|
||||
)
|
||||
|
||||
def test_all_state_before_connect_entrances(self):
|
||||
"""Before connect_entrances, Entrance objects may be unconnected.
|
||||
Thus, we test that get_all_state is performed with allow_partial_entrances if used before or during
|
||||
connect_entrances."""
|
||||
|
||||
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances")
|
||||
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game_name=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type, ())
|
||||
|
||||
original_get_all_state = multiworld.get_all_state
|
||||
|
||||
def patched_get_all_state(use_cache: bool, allow_partial_entrances: bool = False):
|
||||
self.assertTrue(allow_partial_entrances, (
|
||||
"Before the connect_entrances step finishes, other worlds might still have partial entrances. "
|
||||
"As such, any call to get_all_state must use allow_partial_entrances = True."
|
||||
))
|
||||
|
||||
return original_get_all_state(use_cache, allow_partial_entrances)
|
||||
|
||||
multiworld.get_all_state = patched_get_all_state
|
||||
|
||||
for step in gen_steps:
|
||||
with self.subTest("Step", step=step):
|
||||
call_all(multiworld, step)
|
||||
@@ -67,7 +67,7 @@ class TestBase(unittest.TestCase):
|
||||
def test_itempool_not_modified(self):
|
||||
"""Test that worlds don't modify the itempool after `create_items`"""
|
||||
gen_steps = ("generate_early", "create_regions", "create_items")
|
||||
additional_steps = ("set_rules", "generate_basic", "pre_fill")
|
||||
additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill")
|
||||
excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3")
|
||||
worlds_to_test = {game: world
|
||||
for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games}
|
||||
@@ -84,7 +84,7 @@ class TestBase(unittest.TestCase):
|
||||
def test_locality_not_modified(self):
|
||||
"""Test that worlds don't modify the locality of items after duplicates are resolved"""
|
||||
gen_steps = ("generate_early", "create_regions", "create_items")
|
||||
additional_steps = ("set_rules", "generate_basic", "pre_fill")
|
||||
additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill")
|
||||
worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items()}
|
||||
for game_name, world_type in worlds_to_test.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
|
||||
@@ -45,6 +45,12 @@ class TestBase(unittest.TestCase):
|
||||
self.assertEqual(location_count, len(multiworld.get_locations()),
|
||||
f"{game_name} modified locations count during rule creation")
|
||||
|
||||
call_all(multiworld, "connect_entrances")
|
||||
self.assertEqual(region_count, len(multiworld.get_regions()),
|
||||
f"{game_name} modified region count during rule creation")
|
||||
self.assertEqual(location_count, len(multiworld.get_locations()),
|
||||
f"{game_name} modified locations count during rule creation")
|
||||
|
||||
call_all(multiworld, "generate_basic")
|
||||
self.assertEqual(region_count, len(multiworld.get_regions()),
|
||||
f"{game_name} modified region count during generate_basic")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import unittest
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from . import setup_solo_multiworld
|
||||
|
||||
@@ -9,8 +10,12 @@ class TestWorldMemory(unittest.TestCase):
|
||||
"""Tests that worlds don't leak references to MultiWorld or themselves with default options."""
|
||||
import gc
|
||||
import weakref
|
||||
refs: dict[str, weakref.ReferenceType[MultiWorld]] = {}
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game_name=game_name):
|
||||
with self.subTest("Game creation", game_name=game_name):
|
||||
weak = weakref.ref(setup_solo_multiworld(world_type))
|
||||
gc.collect()
|
||||
refs[game_name] = weak
|
||||
gc.collect()
|
||||
for game_name, weak in refs.items():
|
||||
with self.subTest("Game cleanup", game_name=game_name):
|
||||
self.assertFalse(weak(), "World leaked a reference")
|
||||
|
||||
@@ -2,11 +2,11 @@ import unittest
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from . import setup_solo_multiworld
|
||||
from . import setup_solo_multiworld, gen_steps
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]
|
||||
gen_steps = gen_steps
|
||||
|
||||
default_settings_unreachable_regions = {
|
||||
"A Link to the Past": {
|
||||
|
||||
29
test/general/test_state.py
Normal file
29
test/general/test_state.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import unittest
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all
|
||||
from . import setup_solo_multiworld
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
gen_steps = (
|
||||
"generate_early",
|
||||
"create_regions",
|
||||
)
|
||||
|
||||
test_steps = (
|
||||
"create_items",
|
||||
"set_rules",
|
||||
"connect_entrances",
|
||||
"generate_basic",
|
||||
"pre_fill",
|
||||
)
|
||||
|
||||
def test_all_state_is_available(self):
|
||||
"""Ensure all_state can be created at certain steps."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type, self.gen_steps)
|
||||
for step in self.test_steps:
|
||||
with self.subTest("Step", step=step):
|
||||
call_all(multiworld, step)
|
||||
self.assertTrue(multiworld.get_all_state(False, True))
|
||||
@@ -378,6 +378,10 @@ class World(metaclass=AutoWorldRegister):
|
||||
"""Method for setting the rules on the World's regions and locations."""
|
||||
pass
|
||||
|
||||
def connect_entrances(self) -> None:
|
||||
"""Method to finalize the source and target regions of the World's entrances"""
|
||||
pass
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
"""
|
||||
Useful for randomizing things that don't affect logic but are better to be determined before the output stage.
|
||||
|
||||
@@ -87,7 +87,7 @@ class Component:
|
||||
processes = weakref.WeakSet()
|
||||
|
||||
|
||||
def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None:
|
||||
def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
|
||||
global processes
|
||||
import multiprocessing
|
||||
process = multiprocessing.Process(target=func, name=name, args=args)
|
||||
@@ -95,6 +95,14 @@ def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] =
|
||||
processes.add(process)
|
||||
|
||||
|
||||
def launch(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
|
||||
from Utils import is_kivy_running
|
||||
if is_kivy_running():
|
||||
launch_subprocess(func, name, args)
|
||||
else:
|
||||
func(*args)
|
||||
|
||||
|
||||
class SuffixIdentifier:
|
||||
suffixes: Iterable[str]
|
||||
|
||||
@@ -111,7 +119,7 @@ class SuffixIdentifier:
|
||||
|
||||
def launch_textclient(*args):
|
||||
import CommonClient
|
||||
launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args)
|
||||
launch(CommonClient.run_as_textclient, name="TextClient", args=args)
|
||||
|
||||
|
||||
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
|
||||
|
||||
@@ -55,6 +55,7 @@ async def lock(ctx) -> None
|
||||
async def unlock(ctx) -> None
|
||||
|
||||
async def get_hash(ctx) -> str
|
||||
async def get_memory_size(ctx, domain: str) -> int
|
||||
async def get_system(ctx) -> str
|
||||
async def get_cores(ctx) -> dict[str, str]
|
||||
async def ping(ctx) -> None
|
||||
@@ -168,9 +169,10 @@ select dialog and they will be associated with BizHawkClient. This does not affe
|
||||
associate the file extension with Archipelago.
|
||||
|
||||
`validate_rom` is called to figure out whether a given ROM belongs to your client. It will only be called when a ROM is
|
||||
running on a system you specified in your `system` class variable. In most cases, that will be a single system and you
|
||||
can be sure that you're not about to try to read from nonexistent domains or out of bounds. If you decide to claim this
|
||||
ROM as yours, this is where you should do setup for things like `items_handling`.
|
||||
running on a system you specified in your `system` class variable. Take extra care here, because your code will run
|
||||
against ROMs that you have no control over. If you're reading an address deep in ROM, you might want to check the size
|
||||
of ROM before you attempt to read it using `get_memory_size`. If you decide to claim this ROM as yours, this is where
|
||||
you should do setup for things like `items_handling`.
|
||||
|
||||
`game_watcher` is the "main loop" of your client where you should be checking memory and sending new items to the ROM.
|
||||
`BizHawkClient` will make sure that your `game_watcher` only runs when your client has validated the ROM, and will do
|
||||
@@ -268,6 +270,8 @@ server connection before trying to interact with it.
|
||||
- By default, the player will be asked to provide their slot name after connecting to the server and validating, and
|
||||
that input will be used to authenticate with the `Connect` command. You can override `set_auth` in your own client to
|
||||
set it automatically based on data in the ROM or on your client instance.
|
||||
- Use `get_memory_size` inside `validate_rom` if you need to read at large addresses, in case some other game has a
|
||||
smaller ROM size.
|
||||
- You can override `on_package` in your client to watch raw packages, but don't forget you also have access to a
|
||||
subclass of `CommonContext` and its API.
|
||||
- You can import `BizHawkClientContext` for type hints using `typing.TYPE_CHECKING`. Importing it without conditions at
|
||||
|
||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
||||
import abc
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess
|
||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch as launch_component
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .context import BizHawkClientContext
|
||||
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
||||
|
||||
def launch_client(*args) -> None:
|
||||
from .context import launch
|
||||
launch_subprocess(launch, name="BizHawkClient", args=args)
|
||||
launch_component(launch, name="BizHawkClient", args=args)
|
||||
|
||||
|
||||
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
||||
|
||||
@@ -238,6 +238,7 @@ def _patch_and_run_game(patch_file: str):
|
||||
return metadata
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
Utils.messagebox("Error Patching Game", str(exc), True)
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
@@ -338,7 +338,7 @@ class MinExtraYarn(Range):
|
||||
There must be at least this much more yarn over the total number of yarn needed to craft all hats.
|
||||
For example, if this option's value is 10, and the total yarn needed to craft all hats is 40,
|
||||
there must be at least 50 yarn in the pool."""
|
||||
display_name = "Max Extra Yarn"
|
||||
display_name = "Min Extra Yarn"
|
||||
range_start = 5
|
||||
range_end = 15
|
||||
default = 10
|
||||
|
||||
@@ -12,13 +12,13 @@ from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses
|
||||
from worlds.AutoWorld import World, WebWorld, CollectionState
|
||||
from worlds.generic.Rules import add_rule
|
||||
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 as launch_component, Type
|
||||
from Utils import local_path
|
||||
|
||||
|
||||
def launch_client():
|
||||
from .Client import launch
|
||||
launch_subprocess(launch, name="AHITClient")
|
||||
launch_component(launch, name="AHITClient")
|
||||
|
||||
|
||||
components.append(Component("A Hat in Time Client", "AHITClient", func=launch_client,
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
|
||||
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))
|
||||
While it downloads, you can subscribe to the [Archipelago workshop mod](https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601).
|
||||
|
||||
|
||||
4. Once the game finishes downloading, start it up.
|
||||
@@ -62,4 +62,4 @@ The level that the relic set unlocked will stay unlocked.
|
||||
|
||||
### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work!
|
||||
There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly
|
||||
if you have too many save files. Delete them and it should fix the problem.
|
||||
if you have too many save files. Delete them and it should fix the problem.
|
||||
|
||||
@@ -1125,7 +1125,7 @@ def set_trock_key_rules(world, player):
|
||||
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']:
|
||||
set_rule(world.get_entrance(entrance, player), lambda state: False)
|
||||
|
||||
all_state = world.get_all_state(use_cache=False)
|
||||
all_state = world.get_all_state(use_cache=False, allow_partial_entrances=True)
|
||||
all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work
|
||||
all_state.stale[player] = True
|
||||
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
|
||||
1. Download the above release and extract it.
|
||||
|
||||
## Installation Procedures (Linux and Steam Deck)
|
||||
|
||||
1. Download the above release and extract it.
|
||||
|
||||
2. Add Celeste64.exe to Steam as a Non-Steam Game. In the properties for it on Steam, set it to use Proton as the compatibility tool. Launch the game through Steam in order to run it.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Before launching the game, edit the `AP.json` file in the root of the Celeste 64 install.
|
||||
@@ -33,5 +39,3 @@ An Example `AP.json` file:
|
||||
"Password": ""
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -304,6 +304,11 @@ class EvolutionTrapIncrease(Range):
|
||||
range_end = 100
|
||||
|
||||
|
||||
class InventorySpillTrapCount(TrapCount):
|
||||
"""Trap items that when received trigger dropping your main inventory and trash inventory onto the ground."""
|
||||
display_name = "Inventory Spill Traps"
|
||||
|
||||
|
||||
class FactorioWorldGen(OptionDict):
|
||||
"""World Generation settings. Overview of options at https://wiki.factorio.com/Map_generator,
|
||||
with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings"""
|
||||
@@ -484,6 +489,7 @@ class FactorioOptions(PerGameCommonOptions):
|
||||
artillery_traps: ArtilleryTrapCount
|
||||
atomic_rocket_traps: AtomicRocketTrapCount
|
||||
atomic_cliff_remover_traps: AtomicCliffRemoverTrapCount
|
||||
inventory_spill_traps: InventorySpillTrapCount
|
||||
attack_traps: AttackTrapCount
|
||||
evolution_traps: EvolutionTrapCount
|
||||
evolution_trap_increase: EvolutionTrapIncrease
|
||||
@@ -518,6 +524,7 @@ option_groups: list[OptionGroup] = [
|
||||
ArtilleryTrapCount,
|
||||
AtomicRocketTrapCount,
|
||||
AtomicCliffRemoverTrapCount,
|
||||
InventorySpillTrapCount,
|
||||
],
|
||||
start_collapsed=True
|
||||
),
|
||||
|
||||
@@ -8,7 +8,7 @@ import Utils
|
||||
import settings
|
||||
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
|
||||
from worlds.LauncherComponents import Component, components, Type, launch as launch_component
|
||||
from worlds.generic import Rules
|
||||
from .Locations import location_pools, location_table
|
||||
from .Mod import generate_mod
|
||||
@@ -24,7 +24,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
|
||||
|
||||
def launch_client():
|
||||
from .Client import launch
|
||||
launch_subprocess(launch, name="FactorioClient")
|
||||
launch_component(launch, name="FactorioClient")
|
||||
|
||||
|
||||
components.append(Component("Factorio Client", "FactorioClient", func=launch_client, component_type=Type.CLIENT))
|
||||
@@ -78,6 +78,7 @@ all_items["Cluster Grenade Trap"] = factorio_base_id - 5
|
||||
all_items["Artillery Trap"] = factorio_base_id - 6
|
||||
all_items["Atomic Rocket Trap"] = factorio_base_id - 7
|
||||
all_items["Atomic Cliff Remover Trap"] = factorio_base_id - 8
|
||||
all_items["Inventory Spill Trap"] = factorio_base_id - 9
|
||||
|
||||
|
||||
class Factorio(World):
|
||||
@@ -112,6 +113,8 @@ class Factorio(World):
|
||||
science_locations: typing.List[FactorioScienceLocation]
|
||||
removed_technologies: typing.Set[str]
|
||||
settings: typing.ClassVar[FactorioSettings]
|
||||
trap_names: tuple[str] = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery",
|
||||
"Atomic Rocket", "Atomic Cliff Remover", "Inventory Spill")
|
||||
|
||||
def __init__(self, world, player: int):
|
||||
super(Factorio, self).__init__(world, player)
|
||||
@@ -136,15 +139,11 @@ class Factorio(World):
|
||||
random = self.random
|
||||
nauvis = Region("Nauvis", player, self.multiworld)
|
||||
|
||||
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
|
||||
self.options.evolution_traps + \
|
||||
self.options.attack_traps + \
|
||||
self.options.teleport_traps + \
|
||||
self.options.grenade_traps + \
|
||||
self.options.cluster_grenade_traps + \
|
||||
self.options.atomic_rocket_traps + \
|
||||
self.options.atomic_cliff_remover_traps + \
|
||||
self.options.artillery_traps
|
||||
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo
|
||||
|
||||
for name in self.trap_names:
|
||||
name = name.replace(" ", "_").lower()+"_traps"
|
||||
location_count += getattr(self.options, name)
|
||||
|
||||
location_pool = []
|
||||
|
||||
@@ -196,9 +195,8 @@ class Factorio(World):
|
||||
def create_items(self) -> None:
|
||||
self.custom_technologies = self.set_custom_technologies()
|
||||
self.set_custom_recipes()
|
||||
traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket",
|
||||
"Atomic Cliff Remover")
|
||||
for trap_name in traps:
|
||||
|
||||
for trap_name in self.trap_names:
|
||||
self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in
|
||||
range(getattr(self.options,
|
||||
f"{trap_name.lower().replace(' ', '_')}_traps")))
|
||||
@@ -280,9 +278,6 @@ class Factorio(World):
|
||||
self.get_location("Rocket Launch").access_rule = lambda state: all(state.has(technology, player)
|
||||
for technology in
|
||||
victory_tech_names)
|
||||
for tech_name in victory_tech_names:
|
||||
if not self.multiworld.get_all_state(True).has(tech_name, player):
|
||||
print(tech_name)
|
||||
self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player)
|
||||
|
||||
def get_recipe(self, name: str) -> Recipe:
|
||||
|
||||
@@ -48,3 +48,40 @@ function fire_entity_at_entities(entity_name, entities, speed)
|
||||
target=target, speed=speed}
|
||||
end
|
||||
end
|
||||
|
||||
function spill_character_inventory(character)
|
||||
if not (character and character.valid) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- grab attrs once pre-loop
|
||||
local position = character.position
|
||||
local surface = character.surface
|
||||
|
||||
local inventories_to_spill = {
|
||||
defines.inventory.character_main, -- Main inventory
|
||||
defines.inventory.character_trash, -- Logistic trash slots
|
||||
}
|
||||
|
||||
for _, inventory_type in pairs(inventories_to_spill) do
|
||||
local inventory = character.get_inventory(inventory_type)
|
||||
if inventory and inventory.valid then
|
||||
-- Spill each item stack onto the ground
|
||||
for i = 1, #inventory do
|
||||
local stack = inventory[i]
|
||||
if stack and stack.valid_for_read then
|
||||
local spilled_items = surface.spill_item_stack{
|
||||
position = position,
|
||||
stack = stack,
|
||||
enable_looted = false, -- do not mark for auto-pickup
|
||||
force = nil, -- do not mark for auto-deconstruction
|
||||
allow_belts = true, -- do mark for putting it onto belts
|
||||
}
|
||||
if #spilled_items > 0 then
|
||||
stack.clear() -- only delete if spilled successfully
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -750,6 +750,11 @@ end,
|
||||
fire_entity_at_entities("atomic-rocket", {cliffs[math.random(#cliffs)]}, 0.1)
|
||||
end
|
||||
end,
|
||||
["Inventory Spill Trap"] = function ()
|
||||
for _, player in ipairs(game.forces["player"].players) do
|
||||
spill_character_inventory(player.character)
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)
|
||||
|
||||
@@ -152,14 +152,23 @@ class FFMQWorld(World):
|
||||
return FFMQItem(name, self.player)
|
||||
|
||||
def collect_item(self, state, item, remove=False):
|
||||
if not item.advancement:
|
||||
return None
|
||||
if "Progressive" in item.name:
|
||||
i = item.code - 256
|
||||
if remove:
|
||||
if state.has(self.item_id_to_name[i+1], self.player):
|
||||
if state.has(self.item_id_to_name[i+2], self.player):
|
||||
return self.item_id_to_name[i+2]
|
||||
return self.item_id_to_name[i+1]
|
||||
return self.item_id_to_name[i]
|
||||
|
||||
if state.has(self.item_id_to_name[i], self.player):
|
||||
if state.has(self.item_id_to_name[i+1], self.player):
|
||||
return self.item_id_to_name[i+2]
|
||||
return self.item_id_to_name[i+1]
|
||||
return self.item_id_to_name[i]
|
||||
return item.name if item.advancement else None
|
||||
return item.name
|
||||
|
||||
def modify_multidata(self, multidata):
|
||||
# wait for self.rom_name to be available.
|
||||
|
||||
@@ -333,7 +333,7 @@ class PlandoCharmCosts(OptionDict):
|
||||
continue
|
||||
try:
|
||||
self.value[key] = CharmCost.from_any(data).value
|
||||
except ValueError as ex:
|
||||
except ValueError:
|
||||
# will fail schema afterwords
|
||||
self.value[key] = data
|
||||
|
||||
|
||||
@@ -7,22 +7,22 @@ import itertools
|
||||
import operator
|
||||
from collections import defaultdict, Counter
|
||||
|
||||
logger = logging.getLogger("Hollow Knight")
|
||||
|
||||
from .Items import item_table, lookup_type_to_names, item_name_groups
|
||||
from .Regions import create_regions
|
||||
from .Items import item_table, item_name_groups
|
||||
from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance
|
||||
from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \
|
||||
shop_to_option, HKOptions, GrubHuntGoal
|
||||
from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \
|
||||
event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs
|
||||
from .ExtractedData import locations, starts, multi_locations, event_names, item_effects, connectors, \
|
||||
vanilla_shop_costs, vanilla_location_costs
|
||||
from .Charms import names as charm_names
|
||||
|
||||
from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState
|
||||
from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, \
|
||||
CollectionState
|
||||
from worlds.AutoWorld import World, LogicMixin, WebWorld
|
||||
|
||||
from settings import Group, Bool
|
||||
|
||||
logger = logging.getLogger("Hollow Knight")
|
||||
|
||||
|
||||
class HollowKnightSettings(Group):
|
||||
class DisableMapModSpoilers(Bool):
|
||||
@@ -160,7 +160,7 @@ class HKWeb(WebWorld):
|
||||
|
||||
|
||||
class HKWorld(World):
|
||||
"""Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface,
|
||||
"""Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface,
|
||||
searching for riches, or glory, or answers to old secrets.
|
||||
|
||||
As the enigmatic Knight, you’ll traverse the depths, unravel its mysteries and conquer its evils.
|
||||
@@ -209,7 +209,7 @@ class HKWorld(World):
|
||||
# defaulting so completion condition isn't incorrect before pre_fill
|
||||
self.grub_count = (
|
||||
46 if options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]
|
||||
else options.GrubHuntGoal
|
||||
else options.GrubHuntGoal.value
|
||||
)
|
||||
self.grub_player_count = {self.player: self.grub_count}
|
||||
|
||||
@@ -231,7 +231,6 @@ class HKWorld(World):
|
||||
def create_regions(self):
|
||||
menu_region: Region = create_region(self.multiworld, self.player, 'Menu')
|
||||
self.multiworld.regions.append(menu_region)
|
||||
# wp_exclusions = self.white_palace_exclusions()
|
||||
|
||||
# check for any goal that godhome events are relevant to
|
||||
all_event_names = event_names.copy()
|
||||
@@ -241,21 +240,17 @@ class HKWorld(World):
|
||||
|
||||
# Link regions
|
||||
for event_name in sorted(all_event_names):
|
||||
#if event_name in wp_exclusions:
|
||||
# continue
|
||||
loc = HKLocation(self.player, event_name, None, menu_region)
|
||||
loc.place_locked_item(HKItem(event_name,
|
||||
True, #event_name not in wp_exclusions,
|
||||
True,
|
||||
None, "Event", self.player))
|
||||
menu_region.locations.append(loc)
|
||||
for entry_transition, exit_transition in connectors.items():
|
||||
#if entry_transition in wp_exclusions:
|
||||
# continue
|
||||
if exit_transition:
|
||||
# if door logic fulfilled -> award vanilla target as event
|
||||
loc = HKLocation(self.player, entry_transition, None, menu_region)
|
||||
loc.place_locked_item(HKItem(exit_transition,
|
||||
True, #exit_transition not in wp_exclusions,
|
||||
True,
|
||||
None, "Event", self.player))
|
||||
menu_region.locations.append(loc)
|
||||
|
||||
@@ -292,7 +287,10 @@ class HKWorld(World):
|
||||
if item_name in junk_replace:
|
||||
item_name = self.get_filler_item_name()
|
||||
|
||||
item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations else self.create_event(item_name)
|
||||
item = (self.create_item(item_name)
|
||||
if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations
|
||||
else self.create_event(item_name)
|
||||
)
|
||||
|
||||
if location_name == "Start":
|
||||
if item_name in randomized_starting_items:
|
||||
@@ -347,8 +345,8 @@ class HKWorld(World):
|
||||
randomized = True
|
||||
_add("Elevator_Pass", "Elevator_Pass", randomized)
|
||||
|
||||
for shop, locations in self.created_multi_locations.items():
|
||||
for _ in range(len(locations), getattr(self.options, shop_to_option[shop]).value):
|
||||
for shop, shop_locations in self.created_multi_locations.items():
|
||||
for _ in range(len(shop_locations), getattr(self.options, shop_to_option[shop]).value):
|
||||
self.create_location(shop)
|
||||
unfilled_locations += 1
|
||||
|
||||
@@ -358,7 +356,7 @@ class HKWorld(World):
|
||||
|
||||
# Add additional shop items, as needed.
|
||||
if additional_shop_items > 0:
|
||||
shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16)
|
||||
shops = [shop for shop, shop_locations in self.created_multi_locations.items() if len(shop_locations) < 16]
|
||||
if not self.options.EggShopSlots: # No eggshop, so don't place items there
|
||||
shops.remove('Egg_Shop')
|
||||
|
||||
@@ -380,8 +378,8 @@ class HKWorld(World):
|
||||
self.sort_shops_by_cost()
|
||||
|
||||
def sort_shops_by_cost(self):
|
||||
for shop, locations in self.created_multi_locations.items():
|
||||
randomized_locations = list(loc for loc in locations if not loc.vanilla)
|
||||
for shop, shop_locations in self.created_multi_locations.items():
|
||||
randomized_locations = [loc for loc in shop_locations if not loc.vanilla]
|
||||
prices = sorted(
|
||||
(loc.costs for loc in randomized_locations),
|
||||
key=lambda costs: (len(costs),) + tuple(costs.values())
|
||||
@@ -405,7 +403,7 @@ class HKWorld(World):
|
||||
return {k: v for k, v in weights.items() if v}
|
||||
|
||||
random = self.random
|
||||
hybrid_chance = getattr(self.options, f"CostSanityHybridChance").value
|
||||
hybrid_chance = getattr(self.options, "CostSanityHybridChance").value
|
||||
weights = {
|
||||
data.term: getattr(self.options, f"CostSanity{data.option}Weight").value
|
||||
for data in cost_terms.values()
|
||||
@@ -493,7 +491,11 @@ class HKWorld(World):
|
||||
worlds = [world for world in multiworld.get_game_worlds(cls.game) if world.options.Goal in ["any", "grub_hunt"]]
|
||||
if worlds:
|
||||
grubs = [item for item in multiworld.get_items() if item.name == "Grub"]
|
||||
all_grub_players = [world.player for world in worlds if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]]
|
||||
all_grub_players = [
|
||||
world.player
|
||||
for world in worlds
|
||||
if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]
|
||||
]
|
||||
|
||||
if all_grub_players:
|
||||
group_lookup = defaultdict(set)
|
||||
@@ -668,8 +670,8 @@ class HKWorld(World):
|
||||
):
|
||||
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}")
|
||||
else:
|
||||
for shop_name, locations in hk_world.created_multi_locations.items():
|
||||
for loc in locations:
|
||||
for shop_name, shop_locations in hk_world.created_multi_locations.items():
|
||||
for loc in shop_locations:
|
||||
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}")
|
||||
|
||||
def get_multi_location_name(self, base: str, i: typing.Optional[int]) -> str:
|
||||
|
||||
@@ -2,7 +2,6 @@ import typing
|
||||
from argparse import Namespace
|
||||
from BaseClasses import CollectionState, MultiWorld
|
||||
from Options import ItemLinks
|
||||
from test.bases import WorldTestBase
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all
|
||||
from .. import HKWorld
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from . import linkedTestHK, WorldTestBase
|
||||
from test.bases import WorldTestBase
|
||||
from Options import ItemLinks
|
||||
from . import linkedTestHK
|
||||
|
||||
|
||||
class test_grubcount_limited(linkedTestHK, WorldTestBase):
|
||||
|
||||
@@ -206,19 +206,19 @@ def set_rules(world: "KDL3World") -> None:
|
||||
lambda state: can_reach_needle(state, world.player))
|
||||
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u2, world.player),
|
||||
lambda state: can_reach_ice(state, world.player) and
|
||||
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
|
||||
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
|
||||
or can_reach_nago(state, world.player)))
|
||||
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
|
||||
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
|
||||
or can_reach_nago(state, world.player)))
|
||||
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u3, world.player),
|
||||
lambda state: can_reach_ice(state, world.player) and
|
||||
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
|
||||
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
|
||||
or can_reach_nago(state, world.player)))
|
||||
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
|
||||
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
|
||||
or can_reach_nago(state, world.player)))
|
||||
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u4, world.player),
|
||||
lambda state: can_reach_ice(state, world.player) and
|
||||
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
|
||||
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
|
||||
or can_reach_nago(state, world.player)))
|
||||
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
|
||||
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
|
||||
or can_reach_nago(state, world.player)))
|
||||
set_rule(world.multiworld.get_location(location_name.cloudy_park_6_u1, world.player),
|
||||
lambda state: can_reach_cutter(state, world.player))
|
||||
|
||||
@@ -248,9 +248,9 @@ def set_rules(world: "KDL3World") -> None:
|
||||
for i in range(12, 18):
|
||||
set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player),
|
||||
lambda state: can_reach_ice(state, world.player) and
|
||||
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
|
||||
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
|
||||
or can_reach_nago(state, world.player)))
|
||||
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
|
||||
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
|
||||
or can_reach_nago(state, world.player)))
|
||||
for i in range(21, 23):
|
||||
set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player),
|
||||
lambda state: can_reach_chuchu(state, world.player))
|
||||
@@ -307,7 +307,7 @@ def set_rules(world: "KDL3World") -> None:
|
||||
lambda state: can_reach_coo(state, world.player) and can_reach_burning(state, world.player))
|
||||
set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a3, world.player),
|
||||
lambda state: can_reach_chuchu(state, world.player) and can_reach_coo(state, world.player)
|
||||
and can_reach_burning(state, world.player))
|
||||
and can_reach_burning(state, world.player))
|
||||
|
||||
for boss_flag, purification, i in zip(["Level 1 Boss - Purified", "Level 2 Boss - Purified",
|
||||
"Level 3 Boss - Purified", "Level 4 Boss - Purified",
|
||||
@@ -329,6 +329,14 @@ def set_rules(world: "KDL3World") -> None:
|
||||
world.options.ow_boss_requirement.value,
|
||||
world.player_levels)))
|
||||
|
||||
if world.options.open_world:
|
||||
for boss_flag, level in zip(["Level 1 Boss - Defeated", "Level 2 Boss - Defeated", "Level 3 Boss - Defeated",
|
||||
"Level 4 Boss - Defeated", "Level 5 Boss - Defeated"],
|
||||
location_name.level_names.keys()):
|
||||
set_rule(world.get_location(boss_flag),
|
||||
lambda state, lvl=level: state.has(f"{lvl} - Stage Completion", world.player,
|
||||
world.options.ow_boss_requirement.value))
|
||||
|
||||
set_rule(world.multiworld.get_entrance("To Level 6", world.player),
|
||||
lambda state: state.has("Heart Star", world.player, world.required_heart_stars))
|
||||
|
||||
|
||||
@@ -483,6 +483,8 @@ def create_regions(multiworld: MultiWorld, player: int, options):
|
||||
for name, data in regions.items():
|
||||
multiworld.regions.append(create_region(multiworld, player, name, data))
|
||||
|
||||
|
||||
def connect_entrances(multiworld: MultiWorld, player: int):
|
||||
multiworld.get_entrance("Awakening", player).connect(multiworld.get_region("Awakening", player))
|
||||
multiworld.get_entrance("Destiny Islands", player).connect(multiworld.get_region("Destiny Islands", player))
|
||||
multiworld.get_entrance("Traverse Town", player).connect(multiworld.get_region("Traverse Town", player))
|
||||
@@ -500,6 +502,7 @@ def create_regions(multiworld: MultiWorld, player: int, options):
|
||||
multiworld.get_entrance("World Map", player).connect(multiworld.get_region("World Map", player))
|
||||
multiworld.get_entrance("Levels", player).connect(multiworld.get_region("Levels", player))
|
||||
|
||||
|
||||
def create_region(multiworld: MultiWorld, player: int, name: str, data: KH1RegionData):
|
||||
region = Region(name, player, multiworld)
|
||||
if data.locations:
|
||||
|
||||
@@ -6,15 +6,15 @@ from worlds.AutoWorld import WebWorld, World
|
||||
from .Items import KH1Item, KH1ItemData, event_item_table, get_items_by_category, item_table, item_name_groups
|
||||
from .Locations import KH1Location, location_table, get_locations_by_category, location_name_groups
|
||||
from .Options import KH1Options, kh1_option_groups
|
||||
from .Regions import create_regions
|
||||
from .Regions import connect_entrances, create_regions
|
||||
from .Rules import set_rules
|
||||
from .Presets import kh1_option_presets
|
||||
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
|
||||
from worlds.LauncherComponents import Component, components, Type, launch as launch_component
|
||||
|
||||
|
||||
def launch_client():
|
||||
from .Client import launch
|
||||
launch_subprocess(launch, name="KH1 Client")
|
||||
launch_component(launch, name="KH1 Client")
|
||||
|
||||
|
||||
components.append(Component("KH1 Client", "KH1Client", func=launch_client, component_type=Type.CLIENT))
|
||||
@@ -242,6 +242,9 @@ class KH1World(World):
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self.multiworld, self.player, self.options)
|
||||
|
||||
def connect_entrances(self):
|
||||
connect_entrances(self.multiworld, self.player)
|
||||
|
||||
def generate_early(self):
|
||||
value_names = ["Reports to Open End of the World", "Reports to Open Final Rest Door", "Reports in Pool"]
|
||||
|
||||
@@ -5,8 +5,10 @@ ModuleUpdate.update()
|
||||
import os
|
||||
import asyncio
|
||||
import json
|
||||
import requests
|
||||
from pymem import pymem
|
||||
from . import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, SupportAbility_Table, ActionAbility_Table, all_weapon_slot
|
||||
from . import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, \
|
||||
SupportAbility_Table, ActionAbility_Table, all_weapon_slot
|
||||
from .Names import ItemName
|
||||
from .WorldLocations import *
|
||||
|
||||
@@ -82,6 +84,7 @@ class KH2Context(CommonContext):
|
||||
}
|
||||
self.kh2seedname = None
|
||||
self.kh2slotdata = None
|
||||
self.mem_json = None
|
||||
self.itemamount = {}
|
||||
if "localappdata" in os.environ:
|
||||
self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP")
|
||||
@@ -178,7 +181,8 @@ class KH2Context(CommonContext):
|
||||
self.base_accessory_slots = 1
|
||||
self.base_armor_slots = 1
|
||||
self.base_item_slots = 3
|
||||
self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, 0x2770, 0x2772]
|
||||
self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E,
|
||||
0x2770, 0x2772]
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
@@ -340,12 +344,8 @@ class KH2Context(CommonContext):
|
||||
self.locations_checked |= new_locations
|
||||
|
||||
if cmd in {"DataPackage"}:
|
||||
self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"]
|
||||
self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()}
|
||||
self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"]
|
||||
self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()}
|
||||
self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]]
|
||||
|
||||
if "Kingdom Hearts 2" in args["data"]["games"]:
|
||||
self.data_package_kh2_cache(args)
|
||||
if "KeybladeAbilities" in self.kh2slotdata.keys():
|
||||
# sora ability to slot
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["KeybladeAbilities"])
|
||||
@@ -359,24 +359,9 @@ class KH2Context(CommonContext):
|
||||
self.all_weapon_location_id = set(all_weapon_location_id)
|
||||
|
||||
try:
|
||||
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
if self.kh2_game_version is None:
|
||||
if self.kh2_read_string(0x09A9830, 4) == "KH2J":
|
||||
self.kh2_game_version = "STEAM"
|
||||
self.Now = 0x0717008
|
||||
self.Save = 0x09A9830
|
||||
self.Slot1 = 0x2A23518
|
||||
self.Journal = 0x7434E0
|
||||
self.Shop = 0x7435D0
|
||||
|
||||
elif self.kh2_read_string(0x09A92F0, 4) == "KH2J":
|
||||
self.kh2_game_version = "EGS"
|
||||
else:
|
||||
self.kh2_game_version = None
|
||||
logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.")
|
||||
if self.kh2_game_version is not None:
|
||||
logger.info(f"You are now auto-tracking. {self.kh2_game_version}")
|
||||
self.kh2connected = True
|
||||
if not self.kh2:
|
||||
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
self.get_addresses()
|
||||
|
||||
except Exception as e:
|
||||
if self.kh2connected:
|
||||
@@ -385,6 +370,13 @@ class KH2Context(CommonContext):
|
||||
self.serverconneced = True
|
||||
asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}]))
|
||||
|
||||
def data_package_kh2_cache(self, args):
|
||||
self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"]
|
||||
self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()}
|
||||
self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"]
|
||||
self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()}
|
||||
self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]]
|
||||
|
||||
async def checkWorldLocations(self):
|
||||
try:
|
||||
currentworldint = self.kh2_read_byte(self.Now)
|
||||
@@ -425,7 +417,6 @@ class KH2Context(CommonContext):
|
||||
0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels],
|
||||
3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels], 5: ["SummonLevel", SummonLevels]
|
||||
}
|
||||
# TODO: remove formDict[i][0] in self.kh2_seed_save_cache["Levels"].keys() after 4.3
|
||||
for i in range(6):
|
||||
for location, data in formDict[i][1].items():
|
||||
formlevel = self.kh2_read_byte(self.Save + data.addrObtained)
|
||||
@@ -469,9 +460,11 @@ class KH2Context(CommonContext):
|
||||
if locationName in self.chest_set:
|
||||
if locationName in self.location_name_to_worlddata.keys():
|
||||
locationData = self.location_name_to_worlddata[locationName]
|
||||
if self.kh2_read_byte(self.Save + locationData.addrObtained) & 0x1 << locationData.bitIndex == 0:
|
||||
if self.kh2_read_byte(
|
||||
self.Save + locationData.addrObtained) & 0x1 << locationData.bitIndex == 0:
|
||||
roomData = self.kh2_read_byte(self.Save + locationData.addrObtained)
|
||||
self.kh2_write_byte(self.Save + locationData.addrObtained, roomData | 0x01 << locationData.bitIndex)
|
||||
self.kh2_write_byte(self.Save + locationData.addrObtained,
|
||||
roomData | 0x01 << locationData.bitIndex)
|
||||
|
||||
except Exception as e:
|
||||
if self.kh2connected:
|
||||
@@ -494,6 +487,9 @@ class KH2Context(CommonContext):
|
||||
async def give_item(self, item, location):
|
||||
try:
|
||||
# todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites
|
||||
#sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts
|
||||
while not self.lookup_id_to_item:
|
||||
await asyncio.sleep(0.5)
|
||||
itemname = self.lookup_id_to_item[item]
|
||||
itemdata = self.item_name_to_data[itemname]
|
||||
# itemcode = self.kh2_item_name_to_id[itemname]
|
||||
@@ -637,7 +633,8 @@ class KH2Context(CommonContext):
|
||||
item_data = self.item_name_to_data[item_name]
|
||||
# if the inventory slot for that keyblade is less than the amount they should have,
|
||||
# and they are not in stt
|
||||
if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 and self.kh2_read_byte(self.Save + 0x1CFF) != 13:
|
||||
if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 and self.kh2_read_byte(
|
||||
self.Save + 0x1CFF) != 13:
|
||||
# Checking form anchors for the keyblade to remove extra keyblades
|
||||
if self.kh2_read_short(self.Save + 0x24F0) == item_data.kh2id \
|
||||
or self.kh2_read_short(self.Save + 0x32F4) == item_data.kh2id \
|
||||
@@ -738,7 +735,8 @@ class KH2Context(CommonContext):
|
||||
item_data = self.item_name_to_data[item_name]
|
||||
amount_of_items = 0
|
||||
amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Magic"][item_name]
|
||||
if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(self.Shop) in {10, 8}:
|
||||
if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(
|
||||
self.Shop) in {10, 8}:
|
||||
self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
|
||||
|
||||
for item_name in master_stat:
|
||||
@@ -797,7 +795,8 @@ class KH2Context(CommonContext):
|
||||
# self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
|
||||
|
||||
if "PoptrackerVersionCheck" in self.kh2slotdata:
|
||||
if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3
|
||||
if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(
|
||||
self.Save + 0x3607) != 1: # telling the goa they are on version 4.3
|
||||
self.kh2_write_byte(self.Save + 0x3607, 1)
|
||||
|
||||
except Exception as e:
|
||||
@@ -806,10 +805,59 @@ class KH2Context(CommonContext):
|
||||
logger.info(e)
|
||||
logger.info("line 840")
|
||||
|
||||
def get_addresses(self):
|
||||
if not self.kh2connected and self.kh2 is not None:
|
||||
if self.kh2_game_version is None:
|
||||
|
||||
if self.kh2_read_string(0x09A9830, 4) == "KH2J":
|
||||
self.kh2_game_version = "STEAM"
|
||||
self.Now = 0x0717008
|
||||
self.Save = 0x09A9830
|
||||
self.Slot1 = 0x2A23518
|
||||
self.Journal = 0x7434E0
|
||||
self.Shop = 0x7435D0
|
||||
elif self.kh2_read_string(0x09A92F0, 4) == "KH2J":
|
||||
self.kh2_game_version = "EGS"
|
||||
else:
|
||||
if self.game_communication_path:
|
||||
logger.info("Checking with most up to date addresses of github. If file is not found will be downloading datafiles. This might take a moment")
|
||||
#if mem addresses file is found then check version and if old get new one
|
||||
kh2memaddresses_path = os.path.join(self.game_communication_path, f"kh2memaddresses.json")
|
||||
if not os.path.exists(kh2memaddresses_path):
|
||||
mem_resp = requests.get("https://raw.githubusercontent.com/JaredWeakStrike/KH2APMemoryValues/master/kh2memaddresses.json")
|
||||
if mem_resp.status_code == 200:
|
||||
self.mem_json = json.loads(mem_resp.content)
|
||||
with open(kh2memaddresses_path,
|
||||
'w') as f:
|
||||
f.write(json.dumps(self.mem_json, indent=4))
|
||||
else:
|
||||
with open(kh2memaddresses_path, 'r') as f:
|
||||
self.mem_json = json.load(f)
|
||||
if self.mem_json:
|
||||
for key in self.mem_json.keys():
|
||||
|
||||
if self.kh2_read_string(int(self.mem_json[key]["GameVersionCheck"], 0), 4) == "KH2J":
|
||||
self.Now = int(self.mem_json[key]["Now"], 0)
|
||||
self.Save = int(self.mem_json[key]["Save"], 0)
|
||||
self.Slot1 = int(self.mem_json[key]["Slot1"], 0)
|
||||
self.Journal = int(self.mem_json[key]["Journal"], 0)
|
||||
self.Shop = int(self.mem_json[key]["Shop"], 0)
|
||||
self.kh2_game_version = key
|
||||
|
||||
if self.kh2_game_version is not None:
|
||||
logger.info(f"You are now auto-tracking {self.kh2_game_version}")
|
||||
self.kh2connected = True
|
||||
else:
|
||||
logger.info("Your game version does not match what the client requires. Check in the "
|
||||
"kingdom-hearts-2-final-mix channel for more information on correcting the game "
|
||||
"version.")
|
||||
self.kh2connected = False
|
||||
|
||||
|
||||
def finishedGame(ctx: KH2Context):
|
||||
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||
if not ctx.final_xemnas and ctx.kh2_read_byte(ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \
|
||||
if not ctx.final_xemnas and ctx.kh2_read_byte(
|
||||
ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \
|
||||
& 0x1 << all_world_locations[LocationName.FinalXemnas].bitIndex > 0:
|
||||
ctx.final_xemnas = True
|
||||
# three proofs
|
||||
@@ -843,7 +891,8 @@ def finishedGame(ctx: KH2Context):
|
||||
for boss in ctx.kh2slotdata["hitlist"]:
|
||||
if boss in locations:
|
||||
ctx.hitlist_bounties += 1
|
||||
if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"]["Bounty"] >= ctx.kh2slotdata["BountyRequired"]:
|
||||
if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"][
|
||||
"Bounty"] >= ctx.kh2slotdata["BountyRequired"]:
|
||||
if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1:
|
||||
ctx.kh2_write_byte(ctx.Save + 0x36B2, 1)
|
||||
ctx.kh2_write_byte(ctx.Save + 0x36B3, 1)
|
||||
@@ -894,24 +943,7 @@ async def kh2_watcher(ctx: KH2Context):
|
||||
while not ctx.kh2connected and ctx.serverconneced:
|
||||
await asyncio.sleep(15)
|
||||
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
if ctx.kh2 is not None:
|
||||
if ctx.kh2_game_version is None:
|
||||
if ctx.kh2_read_string(0x09A9830, 4) == "KH2J":
|
||||
ctx.kh2_game_version = "STEAM"
|
||||
ctx.Now = 0x0717008
|
||||
ctx.Save = 0x09A9830
|
||||
ctx.Slot1 = 0x2A23518
|
||||
ctx.Journal = 0x7434E0
|
||||
ctx.Shop = 0x7435D0
|
||||
|
||||
elif ctx.kh2_read_string(0x09A92F0, 4) == "KH2J":
|
||||
ctx.kh2_game_version = "EGS"
|
||||
else:
|
||||
ctx.kh2_game_version = None
|
||||
logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.")
|
||||
if ctx.kh2_game_version is not None:
|
||||
logger.info(f"You are now auto-tracking {ctx.kh2_game_version}")
|
||||
ctx.kh2connected = True
|
||||
ctx.get_addresses()
|
||||
except Exception as e:
|
||||
if ctx.kh2connected:
|
||||
ctx.kh2connected = False
|
||||
|
||||
@@ -540,7 +540,7 @@ KH2REGIONS: typing.Dict[str, typing.List[str]] = {
|
||||
LocationName.SephirothFenrir,
|
||||
LocationName.SephiEventLocation
|
||||
],
|
||||
RegionName.CoR: [
|
||||
RegionName.CoR: [ #todo: make logic for getting these checks.
|
||||
LocationName.CoRDepthsAPBoost,
|
||||
LocationName.CoRDepthsPowerCrystal,
|
||||
LocationName.CoRDepthsFrostCrystal,
|
||||
@@ -1032,99 +1032,99 @@ def connect_regions(self):
|
||||
multiworld = self.multiworld
|
||||
player = self.player
|
||||
# connecting every first visit to the GoA
|
||||
KH2RegionConnections: typing.Dict[str, typing.Set[str]] = {
|
||||
"Menu": {RegionName.GoA},
|
||||
RegionName.GoA: {RegionName.Sp, RegionName.Pr, RegionName.Tt, RegionName.Oc, RegionName.Ht,
|
||||
KH2RegionConnections: typing.Dict[str, typing.Tuple[str]] = {
|
||||
"Menu": (RegionName.GoA,),
|
||||
RegionName.GoA: (RegionName.Sp, RegionName.Pr, RegionName.Tt, RegionName.Oc, RegionName.Ht,
|
||||
RegionName.LoD,
|
||||
RegionName.Twtnw, RegionName.Bc, RegionName.Ag, RegionName.Pl, RegionName.Hb,
|
||||
RegionName.Dc, RegionName.Stt,
|
||||
RegionName.Ha1, RegionName.Keyblade, RegionName.LevelsVS1,
|
||||
RegionName.Valor, RegionName.Wisdom, RegionName.Limit, RegionName.Master,
|
||||
RegionName.Final, RegionName.Summon, RegionName.AtlanticaSongOne},
|
||||
RegionName.LoD: {RegionName.ShanYu},
|
||||
RegionName.ShanYu: {RegionName.LoD2},
|
||||
RegionName.LoD2: {RegionName.AnsemRiku},
|
||||
RegionName.AnsemRiku: {RegionName.StormRider},
|
||||
RegionName.StormRider: {RegionName.DataXigbar},
|
||||
RegionName.Ag: {RegionName.TwinLords},
|
||||
RegionName.TwinLords: {RegionName.Ag2},
|
||||
RegionName.Ag2: {RegionName.GenieJafar},
|
||||
RegionName.GenieJafar: {RegionName.DataLexaeus},
|
||||
RegionName.Dc: {RegionName.Tr},
|
||||
RegionName.Tr: {RegionName.OldPete},
|
||||
RegionName.OldPete: {RegionName.FuturePete},
|
||||
RegionName.FuturePete: {RegionName.Terra, RegionName.DataMarluxia},
|
||||
RegionName.Ha1: {RegionName.Ha2},
|
||||
RegionName.Ha2: {RegionName.Ha3},
|
||||
RegionName.Ha3: {RegionName.Ha4},
|
||||
RegionName.Ha4: {RegionName.Ha5},
|
||||
RegionName.Ha5: {RegionName.Ha6},
|
||||
RegionName.Pr: {RegionName.Barbosa},
|
||||
RegionName.Barbosa: {RegionName.Pr2},
|
||||
RegionName.Pr2: {RegionName.GrimReaper1},
|
||||
RegionName.GrimReaper1: {RegionName.GrimReaper2},
|
||||
RegionName.GrimReaper2: {RegionName.DataLuxord},
|
||||
RegionName.Oc: {RegionName.Cerberus},
|
||||
RegionName.Cerberus: {RegionName.OlympusPete},
|
||||
RegionName.OlympusPete: {RegionName.Hydra},
|
||||
RegionName.Hydra: {RegionName.OcPainAndPanicCup, RegionName.OcCerberusCup, RegionName.Oc2},
|
||||
RegionName.Oc2: {RegionName.Hades},
|
||||
RegionName.Hades: {RegionName.Oc2TitanCup, RegionName.Oc2GofCup, RegionName.DataZexion},
|
||||
RegionName.Oc2GofCup: {RegionName.HadesCups},
|
||||
RegionName.Bc: {RegionName.Thresholder},
|
||||
RegionName.Thresholder: {RegionName.Beast},
|
||||
RegionName.Beast: {RegionName.DarkThorn},
|
||||
RegionName.DarkThorn: {RegionName.Bc2},
|
||||
RegionName.Bc2: {RegionName.Xaldin},
|
||||
RegionName.Xaldin: {RegionName.DataXaldin},
|
||||
RegionName.Sp: {RegionName.HostileProgram},
|
||||
RegionName.HostileProgram: {RegionName.Sp2},
|
||||
RegionName.Sp2: {RegionName.Mcp},
|
||||
RegionName.Mcp: {RegionName.DataLarxene},
|
||||
RegionName.Ht: {RegionName.PrisonKeeper},
|
||||
RegionName.PrisonKeeper: {RegionName.OogieBoogie},
|
||||
RegionName.OogieBoogie: {RegionName.Ht2},
|
||||
RegionName.Ht2: {RegionName.Experiment},
|
||||
RegionName.Experiment: {RegionName.DataVexen},
|
||||
RegionName.Hb: {RegionName.Hb2},
|
||||
RegionName.Hb2: {RegionName.CoR, RegionName.HBDemyx},
|
||||
RegionName.HBDemyx: {RegionName.ThousandHeartless},
|
||||
RegionName.ThousandHeartless: {RegionName.Mushroom13, RegionName.DataDemyx, RegionName.Sephi},
|
||||
RegionName.CoR: {RegionName.CorFirstFight},
|
||||
RegionName.CorFirstFight: {RegionName.CorSecondFight},
|
||||
RegionName.CorSecondFight: {RegionName.Transport},
|
||||
RegionName.Pl: {RegionName.Scar},
|
||||
RegionName.Scar: {RegionName.Pl2},
|
||||
RegionName.Pl2: {RegionName.GroundShaker},
|
||||
RegionName.GroundShaker: {RegionName.DataSaix},
|
||||
RegionName.Stt: {RegionName.TwilightThorn},
|
||||
RegionName.TwilightThorn: {RegionName.Axel1},
|
||||
RegionName.Axel1: {RegionName.Axel2},
|
||||
RegionName.Axel2: {RegionName.DataRoxas},
|
||||
RegionName.Tt: {RegionName.Tt2},
|
||||
RegionName.Tt2: {RegionName.Tt3},
|
||||
RegionName.Tt3: {RegionName.DataAxel},
|
||||
RegionName.Twtnw: {RegionName.Roxas},
|
||||
RegionName.Roxas: {RegionName.Xigbar},
|
||||
RegionName.Xigbar: {RegionName.Luxord},
|
||||
RegionName.Luxord: {RegionName.Saix},
|
||||
RegionName.Saix: {RegionName.Twtnw2},
|
||||
RegionName.Twtnw2: {RegionName.Xemnas},
|
||||
RegionName.Xemnas: {RegionName.ArmoredXemnas, RegionName.DataXemnas},
|
||||
RegionName.ArmoredXemnas: {RegionName.ArmoredXemnas2},
|
||||
RegionName.ArmoredXemnas2: {RegionName.FinalXemnas},
|
||||
RegionName.LevelsVS1: {RegionName.LevelsVS3},
|
||||
RegionName.LevelsVS3: {RegionName.LevelsVS6},
|
||||
RegionName.LevelsVS6: {RegionName.LevelsVS9},
|
||||
RegionName.LevelsVS9: {RegionName.LevelsVS12},
|
||||
RegionName.LevelsVS12: {RegionName.LevelsVS15},
|
||||
RegionName.LevelsVS15: {RegionName.LevelsVS18},
|
||||
RegionName.LevelsVS18: {RegionName.LevelsVS21},
|
||||
RegionName.LevelsVS21: {RegionName.LevelsVS24},
|
||||
RegionName.LevelsVS24: {RegionName.LevelsVS26},
|
||||
RegionName.AtlanticaSongOne: {RegionName.AtlanticaSongTwo},
|
||||
RegionName.AtlanticaSongTwo: {RegionName.AtlanticaSongThree},
|
||||
RegionName.AtlanticaSongThree: {RegionName.AtlanticaSongFour},
|
||||
RegionName.Final, RegionName.Summon, RegionName.AtlanticaSongOne),
|
||||
RegionName.LoD: (RegionName.ShanYu,),
|
||||
RegionName.ShanYu: (RegionName.LoD2,),
|
||||
RegionName.LoD2: (RegionName.AnsemRiku,),
|
||||
RegionName.AnsemRiku: (RegionName.StormRider,),
|
||||
RegionName.StormRider: (RegionName.DataXigbar,),
|
||||
RegionName.Ag: (RegionName.TwinLords,),
|
||||
RegionName.TwinLords: (RegionName.Ag2,),
|
||||
RegionName.Ag2: (RegionName.GenieJafar,),
|
||||
RegionName.GenieJafar: (RegionName.DataLexaeus,),
|
||||
RegionName.Dc: (RegionName.Tr,),
|
||||
RegionName.Tr: (RegionName.OldPete,),
|
||||
RegionName.OldPete: (RegionName.FuturePete,),
|
||||
RegionName.FuturePete: (RegionName.Terra, RegionName.DataMarluxia),
|
||||
RegionName.Ha1: (RegionName.Ha2,),
|
||||
RegionName.Ha2: (RegionName.Ha3,),
|
||||
RegionName.Ha3: (RegionName.Ha4,),
|
||||
RegionName.Ha4: (RegionName.Ha5,),
|
||||
RegionName.Ha5: (RegionName.Ha6,),
|
||||
RegionName.Pr: (RegionName.Barbosa,),
|
||||
RegionName.Barbosa: (RegionName.Pr2,),
|
||||
RegionName.Pr2: (RegionName.GrimReaper1,),
|
||||
RegionName.GrimReaper1: (RegionName.GrimReaper2,),
|
||||
RegionName.GrimReaper2: (RegionName.DataLuxord,),
|
||||
RegionName.Oc: (RegionName.Cerberus,),
|
||||
RegionName.Cerberus: (RegionName.OlympusPete,),
|
||||
RegionName.OlympusPete: (RegionName.Hydra,),
|
||||
RegionName.Hydra: (RegionName.OcPainAndPanicCup, RegionName.OcCerberusCup, RegionName.Oc2),
|
||||
RegionName.Oc2: (RegionName.Hades,),
|
||||
RegionName.Hades: (RegionName.Oc2TitanCup, RegionName.Oc2GofCup, RegionName.DataZexion),
|
||||
RegionName.Oc2GofCup: (RegionName.HadesCups,),
|
||||
RegionName.Bc: (RegionName.Thresholder,),
|
||||
RegionName.Thresholder: (RegionName.Beast,),
|
||||
RegionName.Beast: (RegionName.DarkThorn,),
|
||||
RegionName.DarkThorn: (RegionName.Bc2,),
|
||||
RegionName.Bc2: (RegionName.Xaldin,),
|
||||
RegionName.Xaldin: (RegionName.DataXaldin,),
|
||||
RegionName.Sp: (RegionName.HostileProgram,),
|
||||
RegionName.HostileProgram: (RegionName.Sp2,),
|
||||
RegionName.Sp2: (RegionName.Mcp,),
|
||||
RegionName.Mcp: (RegionName.DataLarxene,),
|
||||
RegionName.Ht: (RegionName.PrisonKeeper,),
|
||||
RegionName.PrisonKeeper: (RegionName.OogieBoogie,),
|
||||
RegionName.OogieBoogie: (RegionName.Ht2,),
|
||||
RegionName.Ht2: (RegionName.Experiment,),
|
||||
RegionName.Experiment: (RegionName.DataVexen,),
|
||||
RegionName.Hb: (RegionName.Hb2,),
|
||||
RegionName.Hb2: (RegionName.CoR, RegionName.HBDemyx),
|
||||
RegionName.HBDemyx: (RegionName.ThousandHeartless,),
|
||||
RegionName.ThousandHeartless: (RegionName.Mushroom13, RegionName.DataDemyx, RegionName.Sephi),
|
||||
RegionName.CoR: (RegionName.CorFirstFight,),
|
||||
RegionName.CorFirstFight: (RegionName.CorSecondFight,),
|
||||
RegionName.CorSecondFight: (RegionName.Transport,),
|
||||
RegionName.Pl: (RegionName.Scar,),
|
||||
RegionName.Scar: (RegionName.Pl2,),
|
||||
RegionName.Pl2: (RegionName.GroundShaker,),
|
||||
RegionName.GroundShaker: (RegionName.DataSaix,),
|
||||
RegionName.Stt: (RegionName.TwilightThorn,),
|
||||
RegionName.TwilightThorn: (RegionName.Axel1,),
|
||||
RegionName.Axel1: (RegionName.Axel2,),
|
||||
RegionName.Axel2: (RegionName.DataRoxas,),
|
||||
RegionName.Tt: (RegionName.Tt2,),
|
||||
RegionName.Tt2: (RegionName.Tt3,),
|
||||
RegionName.Tt3: (RegionName.DataAxel,),
|
||||
RegionName.Twtnw: (RegionName.Roxas,),
|
||||
RegionName.Roxas: (RegionName.Xigbar,),
|
||||
RegionName.Xigbar: (RegionName.Luxord,),
|
||||
RegionName.Luxord: (RegionName.Saix,),
|
||||
RegionName.Saix: (RegionName.Twtnw2,),
|
||||
RegionName.Twtnw2: (RegionName.Xemnas,),
|
||||
RegionName.Xemnas: (RegionName.ArmoredXemnas, RegionName.DataXemnas),
|
||||
RegionName.ArmoredXemnas: (RegionName.ArmoredXemnas2,),
|
||||
RegionName.ArmoredXemnas2: (RegionName.FinalXemnas,),
|
||||
RegionName.LevelsVS1: (RegionName.LevelsVS3,),
|
||||
RegionName.LevelsVS3: (RegionName.LevelsVS6,),
|
||||
RegionName.LevelsVS6: (RegionName.LevelsVS9,),
|
||||
RegionName.LevelsVS9: (RegionName.LevelsVS12,),
|
||||
RegionName.LevelsVS12: (RegionName.LevelsVS15,),
|
||||
RegionName.LevelsVS15: (RegionName.LevelsVS18,),
|
||||
RegionName.LevelsVS18: (RegionName.LevelsVS21,),
|
||||
RegionName.LevelsVS21: (RegionName.LevelsVS24,),
|
||||
RegionName.LevelsVS24: (RegionName.LevelsVS26,),
|
||||
RegionName.AtlanticaSongOne: (RegionName.AtlanticaSongTwo,),
|
||||
RegionName.AtlanticaSongTwo: (RegionName.AtlanticaSongThree,),
|
||||
RegionName.AtlanticaSongThree: (RegionName.AtlanticaSongFour,),
|
||||
}
|
||||
|
||||
for source, target in KH2RegionConnections.items():
|
||||
|
||||
@@ -194,8 +194,8 @@ class KH2WorldRules(KH2Rules):
|
||||
RegionName.Oc: lambda state: self.oc_unlocked(state, 1),
|
||||
RegionName.Oc2: lambda state: self.oc_unlocked(state, 2),
|
||||
|
||||
#twtnw1 is actually the roxas fight region thus roxas requires 1 way to the dawn
|
||||
RegionName.Twtnw2: lambda state: self.twtnw_unlocked(state, 2),
|
||||
# These will be swapped and First Visit lock for twtnw is in development.
|
||||
# RegionName.Twtnw1: lambda state: self.lod_unlocked(state, 2),
|
||||
|
||||
RegionName.Ht: lambda state: self.ht_unlocked(state, 1),
|
||||
@@ -263,7 +263,10 @@ class KH2WorldRules(KH2Rules):
|
||||
|
||||
weapon_region = self.multiworld.get_region(RegionName.Keyblade, self.player)
|
||||
for location in weapon_region.locations:
|
||||
add_rule(location, lambda state: state.has(exclusion_table["WeaponSlots"][location.name], self.player))
|
||||
if location.name in exclusion_table["WeaponSlots"]: # shop items and starting items are not in this list
|
||||
exclusion_item = exclusion_table["WeaponSlots"][location.name]
|
||||
add_rule(location, lambda state, e_item=exclusion_item: state.has(e_item, self.player))
|
||||
|
||||
if location.name in Goofy_Checks:
|
||||
add_item_rule(location, lambda item: item.player == self.player and item.name in GoofyAbility_Table.keys())
|
||||
elif location.name in Donald_Checks:
|
||||
@@ -919,8 +922,8 @@ class KH2FightRules(KH2Rules):
|
||||
# normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus
|
||||
# hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus
|
||||
sephiroth_rules = {
|
||||
"easy": self.kh2_dict_count(easy_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit], state) >= 1,
|
||||
"normal": self.kh2_dict_count(normal_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2,
|
||||
"easy": self.kh2_dict_count(easy_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state),
|
||||
"normal": self.kh2_dict_count(normal_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([gap_closer], state) >= 1,
|
||||
"hard": self.kh2_dict_count(hard_sephiroth_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2,
|
||||
}
|
||||
return sephiroth_rules[self.fight_logic]
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import List
|
||||
|
||||
from BaseClasses import Tutorial, ItemClassification
|
||||
from Fill import fast_fill
|
||||
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
|
||||
from worlds.LauncherComponents import Component, components, Type, launch as launch_component
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .Items import *
|
||||
from .Locations import *
|
||||
@@ -17,7 +17,7 @@ from .Subclasses import KH2Item
|
||||
|
||||
def launch_client():
|
||||
from .Client import launch
|
||||
launch_subprocess(launch, name="KH2Client")
|
||||
launch_component(launch, name="KH2Client")
|
||||
|
||||
|
||||
components.append(Component("KH2 Client", "KH2Client", func=launch_client, component_type=Type.CLIENT))
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
|
||||
|
||||
- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
|
||||
1. Version 3.4.0 or greater OpenKH Mod Manager with Panacea
|
||||
1. Version 25.01.26.0 or greater OpenKH Mod Manager with Panacea
|
||||
2. Lua Backend from the OpenKH Mod Manager
|
||||
3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager
|
||||
- Needed for Archipelago
|
||||
@@ -52,7 +52,7 @@ After Installing the seed click "Mod Loader -> Build/Build and Run". Every slot
|
||||
|
||||
<h2 style="text-transform:none";>What the Mod Manager Should Look Like.</h2>
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
<h2 style="text-transform:none";>Using the KH2 Client</h2>
|
||||
|
||||
@@ -253,7 +253,7 @@ def isConsumable(item) -> bool:
|
||||
|
||||
class RequirementsSettings:
|
||||
def __init__(self, options):
|
||||
self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG)
|
||||
self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG, BOMB)
|
||||
self.pit_bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, BOOMERANG, BOMB) # unique
|
||||
self.attack = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG)
|
||||
self.attack_hookshot = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # hinox, shrouded stalfos
|
||||
|
||||
@@ -9,7 +9,7 @@ import re
|
||||
import bsdiff4
|
||||
|
||||
import settings
|
||||
from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial, MultiWorld
|
||||
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Tutorial, MultiWorld
|
||||
from Fill import fill_restrictive
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from .Common import *
|
||||
@@ -315,8 +315,6 @@ class LinksAwakeningWorld(World):
|
||||
|
||||
# Set up filter rules
|
||||
|
||||
# The list of items we will pass to fill_restrictive, contains at first the items that go to all dungeons
|
||||
all_dungeon_items_to_fill = list(self.prefill_own_dungeons)
|
||||
# set containing the list of all possible dungeon locations for the player
|
||||
all_dungeon_locs = set()
|
||||
|
||||
@@ -327,9 +325,6 @@ class LinksAwakeningWorld(World):
|
||||
for item in self.prefill_original_dungeon[dungeon_index]:
|
||||
allowed_locations_by_item[item] = locs
|
||||
|
||||
# put the items for this dungeon in the list to fill
|
||||
all_dungeon_items_to_fill.extend(self.prefill_original_dungeon[dungeon_index])
|
||||
|
||||
# ...and gather the list of all dungeon locations
|
||||
all_dungeon_locs |= locs
|
||||
# ...also set the rules for the dungeon
|
||||
@@ -369,16 +364,27 @@ class LinksAwakeningWorld(World):
|
||||
if allowed_locations_by_item[item] is all_dungeon_locs:
|
||||
i += 3
|
||||
return i
|
||||
all_dungeon_items_to_fill = self.get_pre_fill_items()
|
||||
all_dungeon_items_to_fill.sort(key=priority)
|
||||
|
||||
# Set up state
|
||||
all_state = self.multiworld.get_all_state(use_cache=False)
|
||||
# Remove dungeon items we are about to put in from the state so that we don't double count
|
||||
for item in all_dungeon_items_to_fill:
|
||||
all_state.remove(item)
|
||||
partial_all_state = CollectionState(self.multiworld)
|
||||
# Collect every item from the item pool and every pre-fill item like MultiWorld.get_all_state, except not our own pre-fill items.
|
||||
for item in self.multiworld.itempool:
|
||||
partial_all_state.collect(item, prevent_sweep=True)
|
||||
for player in self.multiworld.player_ids:
|
||||
if player == self.player:
|
||||
# Don't collect the items we're about to place.
|
||||
continue
|
||||
subworld = self.multiworld.worlds[player]
|
||||
for item in subworld.get_pre_fill_items():
|
||||
partial_all_state.collect(item, prevent_sweep=True)
|
||||
|
||||
# Sweep to pick up already placed items that are reachable with everything but the dungeon items.
|
||||
partial_all_state.sweep_for_advancements()
|
||||
|
||||
# Finally, fill!
|
||||
fill_restrictive(self.multiworld, all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False)
|
||||
fill_restrictive(self.multiworld, partial_all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False)
|
||||
|
||||
|
||||
name_cache = {}
|
||||
# Tries to associate an icon from another game with an icon we have
|
||||
|
||||
@@ -136,6 +136,12 @@ class MeritousWorld(World):
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self.multiworld, self.player)
|
||||
if self.goal == 0:
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has_any(
|
||||
["Victory", "Full Victory"], self.player)
|
||||
else:
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has(
|
||||
"Full Victory", self.player)
|
||||
|
||||
def generate_basic(self):
|
||||
self.multiworld.get_location("Place of Power", self.player).place_locked_item(
|
||||
@@ -166,13 +172,6 @@ class MeritousWorld(World):
|
||||
self.multiworld.get_location(boss, self.player).place_locked_item(
|
||||
self.create_item("Evolution Trap"))
|
||||
|
||||
if self.goal == 0:
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has_any(
|
||||
["Victory", "Full Victory"], self.player)
|
||||
else:
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has(
|
||||
"Full Victory", self.player)
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
return {
|
||||
"goal": self.goal,
|
||||
|
||||
@@ -175,7 +175,7 @@ class WeaknessPlando(OptionDict):
|
||||
display_name = "Plando Weaknesses"
|
||||
schema = Schema({
|
||||
Optional(And(str, Use(str.title), lambda s: s in bosses)): {
|
||||
And(str, Use(str.title), lambda s: s in weapons_to_id): And(int, lambda i: i in range(-1, 14))
|
||||
And(str, Use(str.title), lambda s: s in weapons_to_id): And(int, lambda i: i in range(-1, 15))
|
||||
}
|
||||
})
|
||||
default = {}
|
||||
|
||||
@@ -135,41 +135,47 @@ def set_rules(world: "MM2World") -> None:
|
||||
world.weapon_damage[weapon][i] = 0
|
||||
|
||||
for p_boss in world.options.plando_weakness:
|
||||
boss = bosses[p_boss]
|
||||
for p_weapon in world.options.plando_weakness[p_boss]:
|
||||
if world.options.plando_weakness[p_boss][p_weapon] < minimum_weakness_requirement[p_weapon] \
|
||||
and not any(w != p_weapon
|
||||
and world.weapon_damage[w][bosses[p_boss]] > minimum_weakness_requirement[w]
|
||||
for w in world.weapon_damage):
|
||||
weapon = weapons_to_id[p_weapon]
|
||||
if world.options.plando_weakness[p_boss][p_weapon] < minimum_weakness_requirement[weapon] \
|
||||
and not any(w != weapon
|
||||
and world.weapon_damage[w][boss] >= minimum_weakness_requirement[w]
|
||||
for w in world.weapon_damage):
|
||||
# we need to replace this weakness
|
||||
weakness = world.random.choice([key for key in world.weapon_damage if key != p_weapon])
|
||||
world.weapon_damage[weakness][bosses[p_boss]] = minimum_weakness_requirement[weakness]
|
||||
world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \
|
||||
= world.options.plando_weakness[p_boss][p_weapon]
|
||||
weakness = world.random.choice([key for key in world.weapon_damage if key != weapon])
|
||||
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
|
||||
world.weapon_damage[weapon][boss] = world.options.plando_weakness[p_boss][p_weapon]
|
||||
|
||||
# handle special cases
|
||||
for boss in range(14):
|
||||
for weapon in (1, 2, 3, 6, 8):
|
||||
if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and
|
||||
not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[weapon]
|
||||
not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[i]
|
||||
for i in range(9) if i != weapon)):
|
||||
# Weapon does not have enough possible ammo to kill the boss, raise the damage
|
||||
if boss == 9:
|
||||
if weapon in (1, 6):
|
||||
# Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness
|
||||
world.weapon_damage[weapon][boss] = 0
|
||||
weakness = world.random.choice((2, 3, 4, 5, 7, 8))
|
||||
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
|
||||
elif boss == 11:
|
||||
if weapon == 1:
|
||||
# Atomic Fire cannot be Boobeam Trap's only weakness
|
||||
world.weapon_damage[weapon][boss] = 0
|
||||
weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8))
|
||||
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
|
||||
else:
|
||||
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
|
||||
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
|
||||
|
||||
for weapon in (1, 6):
|
||||
if (world.weapon_damage[weapon][9] >= minimum_weakness_requirement[weapon] and
|
||||
not any(world.weapon_damage[i][9] >= minimum_weakness_requirement[i]
|
||||
for i in range(9) if i not in (1, 6))):
|
||||
# Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness
|
||||
world.weapon_damage[weapon][9] = 0
|
||||
weakness = world.random.choice((2, 3, 4, 5, 7, 8))
|
||||
world.weapon_damage[weakness][9] = minimum_weakness_requirement[weakness]
|
||||
|
||||
if (world.weapon_damage[1][11] >= minimum_weakness_requirement[1] and
|
||||
not any(world.weapon_damage[i][11] >= minimum_weakness_requirement[i]
|
||||
for i in range(9) if i != 1)):
|
||||
# Atomic Fire cannot be Boobeam Trap's only weakness
|
||||
world.weapon_damage[1][11] = 0
|
||||
weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8))
|
||||
world.weapon_damage[weakness][11] = minimum_weakness_requirement[weakness]
|
||||
|
||||
if world.weapon_damage[0][world.options.starting_robot_master.value] < 1:
|
||||
world.weapon_damage[0][world.options.starting_robot_master.value] = weapon_damage[0][world.options.starting_robot_master.value]
|
||||
world.weapon_damage[0][world.options.starting_robot_master.value] = \
|
||||
weapon_damage[0][world.options.starting_robot_master.value]
|
||||
|
||||
# final special case
|
||||
# There's a vanilla crash if Time Stopper kills Wily phase 1
|
||||
@@ -218,9 +224,10 @@ def set_rules(world: "MM2World") -> None:
|
||||
# we are out of weapons that can actually damage the boss
|
||||
# so find the weapon that has the most uses, and apply that as an additional weakness
|
||||
# it should be impossible to be out of energy, simply because even if every boss took 1 from
|
||||
# Quick Boomerang and no other, it would only be 28 off from defeating all 9, which Metal Blade should
|
||||
# be able to cover
|
||||
wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon]) for weapon in weapon_weight
|
||||
# Quick Boomerang and no other, it would only be 28 off from defeating all 9,
|
||||
# which Metal Blade should be able to cover
|
||||
wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon])
|
||||
for weapon in weapon_weight
|
||||
if weapon != 0 and (weapon != 8 or boss != 12))
|
||||
# Wily Machine cannot under any circumstances take damage from Time Stopper, prevent this
|
||||
world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp]
|
||||
|
||||
@@ -20,6 +20,8 @@ class PathOption(Choice):
|
||||
class HiddenChests(Range):
|
||||
"""
|
||||
Number of hidden chest checks added to the applicable biomes.
|
||||
Note: The number of hidden chests that spawn per run in each biome varies.
|
||||
You are expected do multiple runs to get all of your checks.
|
||||
"""
|
||||
display_name = "Hidden Chests per Biome"
|
||||
range_start = 0
|
||||
@@ -30,6 +32,8 @@ class HiddenChests(Range):
|
||||
class PedestalChecks(Range):
|
||||
"""
|
||||
Number of checks that will spawn on pedestals in the applicable biomes.
|
||||
Note: The number of pedestals that spawn per run in each biome varies.
|
||||
You are expected do multiple runs to get all of your checks.
|
||||
"""
|
||||
display_name = "Pedestal Checks per Biome"
|
||||
range_start = 0
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
# 2.4.0
|
||||
|
||||
### Features
|
||||
|
||||
- New option `free_fly_blacklist` limits which cities can show up as a free fly location.
|
||||
- Spoiler log and hint text for maps where a species can be found now use human-friendly labels.
|
||||
- Added many item and location groups based on item type, location type, and location geography.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Now excludes the location "Navel Rock Top - Hidden Item Sacred Ash" if your goal is Champion and you didn't randomize
|
||||
event tickets.
|
||||
|
||||
# 2.3.0
|
||||
|
||||
### Features
|
||||
|
||||
@@ -22,7 +22,7 @@ from .locations import (PokemonEmeraldLocation, create_location_label_to_id_map,
|
||||
set_free_fly, set_legendary_cave_entrances)
|
||||
from .opponents import randomize_opponent_parties
|
||||
from .options import (Goal, DarkCavesRequireFlash, HmRequirements, ItemPoolType, PokemonEmeraldOptions,
|
||||
RandomizeWildPokemon, RandomizeBadges, RandomizeHms, NormanRequirement)
|
||||
RandomizeWildPokemon, RandomizeBadges, RandomizeHms, NormanRequirement, OPTION_GROUPS)
|
||||
from .pokemon import (get_random_move, get_species_id_by_label, randomize_abilities, randomize_learnsets,
|
||||
randomize_legendary_encounters, randomize_misc_pokemon, randomize_starters,
|
||||
randomize_tm_hm_compatibility,randomize_types, randomize_wild_encounters)
|
||||
@@ -63,6 +63,7 @@ class PokemonEmeraldWebWorld(WebWorld):
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_es, setup_sv]
|
||||
option_groups = OPTION_GROUPS
|
||||
|
||||
|
||||
class PokemonEmeraldSettings(settings.Group):
|
||||
|
||||
@@ -33,6 +33,26 @@ VISITED_EVENT_NAME_TO_ID = {
|
||||
"EVENT_VISITED_SOUTHERN_ISLAND": 17,
|
||||
}
|
||||
|
||||
BLACKLIST_OPTION_TO_VISITED_EVENT = {
|
||||
"Littleroot Town": "EVENT_VISITED_LITTLEROOT_TOWN",
|
||||
"Oldale Town": "EVENT_VISITED_OLDALE_TOWN",
|
||||
"Petalburg City": "EVENT_VISITED_PETALBURG_CITY",
|
||||
"Rustboro City": "EVENT_VISITED_RUSTBORO_CITY",
|
||||
"Dewford Town": "EVENT_VISITED_DEWFORD_TOWN",
|
||||
"Slateport City": "EVENT_VISITED_SLATEPORT_CITY",
|
||||
"Mauville City": "EVENT_VISITED_MAUVILLE_CITY",
|
||||
"Verdanturf Town": "EVENT_VISITED_VERDANTURF_TOWN",
|
||||
"Fallarbor Town": "EVENT_VISITED_FALLARBOR_TOWN",
|
||||
"Lavaridge Town": "EVENT_VISITED_LAVARIDGE_TOWN",
|
||||
"Fortree City": "EVENT_VISITED_FORTREE_CITY",
|
||||
"Lilycove City": "EVENT_VISITED_LILYCOVE_CITY",
|
||||
"Mossdeep City": "EVENT_VISITED_MOSSDEEP_CITY",
|
||||
"Sootopolis City": "EVENT_VISITED_SOOTOPOLIS_CITY",
|
||||
"Ever Grande City": "EVENT_VISITED_EVER_GRANDE_CITY",
|
||||
}
|
||||
|
||||
VISITED_EVENTS = frozenset(BLACKLIST_OPTION_TO_VISITED_EVENT.values())
|
||||
|
||||
|
||||
class PokemonEmeraldLocation(Location):
|
||||
game: str = "Pokemon Emerald"
|
||||
@@ -129,18 +149,10 @@ def set_free_fly(world: "PokemonEmeraldWorld") -> None:
|
||||
# If not enabled, set it to Littleroot Town by default
|
||||
fly_location_name = "EVENT_VISITED_LITTLEROOT_TOWN"
|
||||
if world.options.free_fly_location:
|
||||
fly_location_name = world.random.choice([
|
||||
"EVENT_VISITED_SLATEPORT_CITY",
|
||||
"EVENT_VISITED_MAUVILLE_CITY",
|
||||
"EVENT_VISITED_VERDANTURF_TOWN",
|
||||
"EVENT_VISITED_FALLARBOR_TOWN",
|
||||
"EVENT_VISITED_LAVARIDGE_TOWN",
|
||||
"EVENT_VISITED_FORTREE_CITY",
|
||||
"EVENT_VISITED_LILYCOVE_CITY",
|
||||
"EVENT_VISITED_MOSSDEEP_CITY",
|
||||
"EVENT_VISITED_SOOTOPOLIS_CITY",
|
||||
"EVENT_VISITED_EVER_GRANDE_CITY",
|
||||
])
|
||||
blacklisted_locations = set(BLACKLIST_OPTION_TO_VISITED_EVENT[city] for city in world.options.free_fly_blacklist.value)
|
||||
free_fly_locations = sorted(VISITED_EVENTS - blacklisted_locations)
|
||||
if free_fly_locations:
|
||||
fly_location_name = world.random.choice(free_fly_locations)
|
||||
|
||||
world.free_fly_location_id = VISITED_EVENT_NAME_TO_ID[fly_location_name]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Option definitions for Pokemon Emerald
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Options import (Choice, DeathLink, DefaultOnToggle, OptionSet, NamedRange, Range, Toggle, FreeText,
|
||||
PerGameCommonOptions)
|
||||
PerGameCommonOptions, OptionGroup, StartInventory)
|
||||
|
||||
from .data import data
|
||||
|
||||
@@ -726,6 +726,39 @@ class FreeFlyLocation(Toggle):
|
||||
display_name = "Free Fly Location"
|
||||
|
||||
|
||||
class FreeFlyBlacklist(OptionSet):
|
||||
"""
|
||||
Disables specific locations as valid free fly locations.
|
||||
|
||||
Has no effect if Free Fly Location is disabled.
|
||||
"""
|
||||
display_name = "Free Fly Blacklist"
|
||||
valid_keys = [
|
||||
"Littleroot Town",
|
||||
"Oldale Town",
|
||||
"Petalburg City",
|
||||
"Rustboro City",
|
||||
"Dewford Town",
|
||||
"Slateport City",
|
||||
"Mauville City",
|
||||
"Verdanturf Town",
|
||||
"Fallarbor Town",
|
||||
"Lavaridge Town",
|
||||
"Fortree City",
|
||||
"Lilycove City",
|
||||
"Mossdeep City",
|
||||
"Sootopolis City",
|
||||
"Ever Grande City",
|
||||
]
|
||||
default = [
|
||||
"Littleroot Town",
|
||||
"Oldale Town",
|
||||
"Petalburg City",
|
||||
"Rustboro City",
|
||||
"Dewford Town",
|
||||
]
|
||||
|
||||
|
||||
class HmRequirements(Choice):
|
||||
"""
|
||||
Sets the requirements to use HMs outside of battle.
|
||||
@@ -785,6 +818,10 @@ class RandomizeFanfares(Toggle):
|
||||
display_name = "Randomize Fanfares"
|
||||
|
||||
|
||||
class PokemonEmeraldDeathLink(DeathLink):
|
||||
__doc__ = DeathLink.__doc__ + "\n\n In Pokemon Emerald, whiting out sends a death and receiving a death causes you to white out."
|
||||
|
||||
|
||||
class WonderTrading(DefaultOnToggle):
|
||||
"""
|
||||
Allows participation in wonder trading with other players in your current multiworld. Speak with the center receptionist on the second floor of any pokecenter.
|
||||
@@ -810,6 +847,14 @@ class EasterEgg(FreeText):
|
||||
default = "EMERALD SECRET"
|
||||
|
||||
|
||||
class PokemonEmeraldStartInventory(StartInventory):
|
||||
"""
|
||||
Start with these items.
|
||||
|
||||
They will be in your PC, which you can access from your home or a pokemon center.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class PokemonEmeraldOptions(PerGameCommonOptions):
|
||||
goal: Goal
|
||||
@@ -876,6 +921,7 @@ class PokemonEmeraldOptions(PerGameCommonOptions):
|
||||
extra_bumpy_slope: ExtraBumpySlope
|
||||
modify_118: ModifyRoute118
|
||||
free_fly_location: FreeFlyLocation
|
||||
free_fly_blacklist: FreeFlyBlacklist
|
||||
hm_requirements: HmRequirements
|
||||
|
||||
turbo_a: TurboA
|
||||
@@ -885,7 +931,18 @@ class PokemonEmeraldOptions(PerGameCommonOptions):
|
||||
music: RandomizeMusic
|
||||
fanfares: RandomizeFanfares
|
||||
|
||||
death_link: DeathLink
|
||||
death_link: PokemonEmeraldDeathLink
|
||||
|
||||
enable_wonder_trading: WonderTrading
|
||||
easter_egg: EasterEgg
|
||||
|
||||
start_inventory: PokemonEmeraldStartInventory
|
||||
|
||||
|
||||
OPTION_GROUPS = [
|
||||
OptionGroup(
|
||||
"Item & Location Options", [
|
||||
PokemonEmeraldStartInventory,
|
||||
], True,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- Sonic Adventure 2: Battle from: [Sonic Adventure 2: Battle Steam Store Page](https://store.steampowered.com/app/213610/Sonic_Adventure_2/)
|
||||
- The Battle DLC is required if you choose to add Chao Karate locations to the randomizer
|
||||
- SA Mod Manager from: [SA Mod Manager GitHub Releases Page](https://github.com/X-Hax/SA-Mod-Manager/releases)
|
||||
- .NET Desktop Runtime 7.0 from: [.NET Desktop Runtime 7.0 Download Page](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-7.0.9-windows-x64-installer)
|
||||
- .NET Desktop Runtime 8.0 from: [.NET Desktop Runtime 8.0 Download Page](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.12-windows-x64-installer)
|
||||
- Archipelago Mod for Sonic Adventure 2: Battle
|
||||
from: [Sonic Adventure 2: Battle Archipelago Randomizer Mod Releases Page](https://github.com/PoryGone/SA2B_Archipelago/releases/)
|
||||
|
||||
@@ -36,27 +36,23 @@
|
||||
|
||||
1. Install Sonic Adventure 2: Battle from Steam.
|
||||
|
||||
2. In the properties for Sonic Adventure 2 on Steam, force the use of Proton Experimental as the compatibility tool.
|
||||
2. Launch the game at least once without mods.
|
||||
|
||||
3. Launch the game at least once without mods.
|
||||
3. Create both a `/mods` directory and a `/SAManager` directory in the folder into which you installed Sonic Adventure 2: Battle.
|
||||
|
||||
4. Create both a `/mods` directory and a `/SAManager` directory in the folder into which you installed Sonic Adventure 2: Battle.
|
||||
4. Unpack the Archipelago Mod into this folder, so that `/mods/SA2B_Archipelago` is a valid path.
|
||||
|
||||
5. Install SA Mod Manager as per [its instructions](https://github.com/X-Hax/SA-Mod-Manager/tree/master?tab=readme-ov-file). Specifically, extract SAModManager.exe file to the folder that Sonic Adventure 2: Battle is installed to. To launch it, add ``SAModManager.exe`` as a non-Steam game. In the properties on Steam for SA Mod Manager, set it to use Proton as the compatibility tool.
|
||||
5. In the SA2B_Archipelago folder, copy the `APCpp.dll` file and paste it in the Sonic Adventure 2 install folder (where `sonic2app.exe` is).
|
||||
|
||||
6. Run SAModManager.exe from Steam once. It should produce an error popup for a missing dependency, close the error.
|
||||
6. Install SA Mod Manager as per [its instructions](https://github.com/X-Hax/SA-Mod-Manager/tree/master?tab=readme-ov-file). Specifically, extract SAModManager.exe file to the folder that Sonic Adventure 2: Battle is installed to. To launch it, add ``SAModManager.exe`` as a non-Steam game. In the properties on Steam for SA Mod Manager, set it to use Proton as the compatibility tool.
|
||||
|
||||
7. Install protontricks, on the Steam Deck this can be done via the Discover store, on other distros instructions vary, [see its github page](https://github.com/Matoking/protontricks).
|
||||
7. Run SAModManager.exe from Steam once. It should produce an error popup saying you need .NET Desktop Runtime and ask you if you'd like to download it. Say yes and it will download through your browser.
|
||||
|
||||
8. Download the [.NET 7 Desktop Runtime for x64 Windows](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-7.0.17-windows-x64-installer). If this link does not work, the download can be found on [this page](https://dotnet.microsoft.com/en-us/download/dotnet/7.0).
|
||||
8. Install protontricks, on the Steam Deck this can be done via the Discover store, on other distros instructions vary, [see its github page](https://github.com/Matoking/protontricks).
|
||||
|
||||
9. Right click the .NET 7 Desktop Runtime exe, and assuming protontricks was installed correctly, the option to "Open with Protontricks Launcher" should be available. Click that, and in the popup window that opens, select SAModManager.exe. Follow the prompts after this to install the .NET 7 Desktop Runtime for SAModManager. Once it is done, you should be able to successfully launch SAModManager to steam.
|
||||
9. Right click the .NET Desktop Runtime exe that was downloaded in step 6, and assuming protontricks was installed correctly, the option to "Open with Protontricks Launcher" should be available. Click that, and in the popup window that opens, select SAModManager.exe. Follow the prompts after this to install the .NET Desktop Runtime for SAModManager. Once it is done, you should be able to successfully launch SAModManager to steam.
|
||||
|
||||
6. Unpack the Archipelago Mod into this folder, so that `/mods/SA2B_Archipelago` is a valid path.
|
||||
|
||||
7. In the SA2B_Archipelago folder, copy the `APCpp.dll` file and paste it in the Sonic Adventure 2 install folder (where `sonic2app.exe` is).
|
||||
|
||||
8. Launch `SAModManager.exe` from Steam and make sure the SA2B_Archipelago mod is listed and enabled.
|
||||
10. Launch `SAModManager.exe` from Steam and make sure the SA2B_Archipelago mod is listed and enabled.
|
||||
|
||||
Note: Ensure that you launch Sonic Adventure 2 from Steam directly on Linux, rather than launching using the `Save & Play` button in SA Mod Manager.
|
||||
|
||||
@@ -77,7 +73,7 @@ Note: Ensure that you launch Sonic Adventure 2 from Steam directly on Linux, rat
|
||||
## Additional Options
|
||||
|
||||
Some additional settings related to the Archipelago messages in game can be adjusted in the SAModManager if you select `Configure Mod` on the SA2B_Archipelago mod. This settings will be under a `General Settings` tab.
|
||||
|
||||
|
||||
- Message Display Count: This is the maximum number of Archipelago messages that can be displayed on screen at any given time.
|
||||
- Message Display Duration: This dictates how long Archipelago messages are displayed on screen (in seconds).
|
||||
- Message Font Size: The is the size of the font used to display the messages from Archipelago.
|
||||
@@ -94,7 +90,7 @@ If you wish to use the `SADX Music` option of the Randomizer, you must own a cop
|
||||
|
||||
- "The following mods didn't load correctly: SA2B_Archipelago: DLL error - The specified module could not be found."
|
||||
- Make sure the `APCpp.dll` is in the same folder as the `sonic2app.exe`. (See Installation Procedures step 6)
|
||||
|
||||
|
||||
- "sonic2app.exe - Entry Point Not Found"
|
||||
- Make sure the `APCpp.dll` is up to date. Follow Installation Procedures step 6 to update the dll.
|
||||
|
||||
@@ -116,7 +112,7 @@ If you wish to use the `SADX Music` option of the Randomizer, you must own a cop
|
||||
1. Run the Launcher.exe which should be in the same folder as the your Sonic Adventure 2: Battle install.
|
||||
2. Select the `Player` tab and reselect the controller for the player 1 input method.
|
||||
3. Click the `Save settings and launch SONIC ADVENTURE 2` button. (Any mod manager settings will apply even if the game is launched this way rather than through the mod manager)
|
||||
|
||||
|
||||
- Game crashes after display logos.
|
||||
- This may be caused by a high monitor refresh rate.
|
||||
- Change the monitor refresh rate to 60 Hz [Change display refresh rate on Windows] (https://support.microsoft.com/en-us/windows/change-your-display-refresh-rate-in-windows-c8ea729e-0678-015c-c415-f806f04aae5a)
|
||||
@@ -125,13 +121,13 @@ If you wish to use the `SADX Music` option of the Randomizer, you must own a cop
|
||||
2. Select the `Compatibility` tab.
|
||||
3. Check the `Run this program in compatility mode for:` box and select Windows 7 in the drop down.
|
||||
4. Click the `Apply` button.
|
||||
|
||||
|
||||
- No resolution options in the Launcher.exe.
|
||||
- In the `Graphics device` dropdown, select the device and display you plan to run the game on. The `Resolution` dropdown should populate once a graphics device is selected.
|
||||
|
||||
|
||||
- No music is playing in the game.
|
||||
- If you enabled an `SADX Music` option, then most likely the music data was not copied properly into the mod folder (See Additional Options for instructions).
|
||||
|
||||
|
||||
- Mission 1 is missing a texture in the stage select UI.
|
||||
- Most likely another mod is conflicting and overwriting the texture pack. It is recommeded to have the SA2B Archipelago mod load last in the mod manager.
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ class ShiversWorld(World):
|
||||
|
||||
storage_items += [self.create_item("Empty") for _ in range(3)]
|
||||
|
||||
state = self.multiworld.get_all_state(True)
|
||||
state = self.multiworld.get_all_state(False)
|
||||
|
||||
self.random.shuffle(storage_locs)
|
||||
self.random.shuffle(storage_items)
|
||||
|
||||
@@ -9,6 +9,7 @@ configuration file.
|
||||
|
||||
All Ixupi pot pieces are randomized. Keys have been added to the game to lock off different rooms in the museum,
|
||||
these are randomized. Crawling has been added and is required to use any crawl space.
|
||||
Randomization can also control if Ixupi pots are in pieces, mixed, or complete, and in which worlds they will show up in.
|
||||
|
||||
## What is considered a location check in Shivers?
|
||||
|
||||
@@ -27,4 +28,5 @@ Victory is achieved when the player has captured the required number Ixupi set i
|
||||
|
||||
## Encountered a bug?
|
||||
|
||||
Please contact GodlFire or Cynbel_Terreus on Discord for bugs related to Shivers world generation or the Shivers Randomizer.
|
||||
Please contact GodlFire or Cynbel_Terreus on Discord for bugs related to Shivers world generation or the Shivers Randomizer.
|
||||
You may also open issues for the Shivers Randomizer Client [here](https://github.com/Shivers-Randomizer/Shivers-Randomizer/issues).
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
- [Shivers (GOG version)](https://www.gog.com/en/game/shivers) or original disc
|
||||
- [ScummVM](https://www.scummvm.org/downloads/) version 2.7.0 or later
|
||||
- [Shivers Randomizer](https://github.com/GodlFire/Shivers-Randomizer-CSharp/releases/latest) Latest release version
|
||||
- [Shivers Randomizer Client](https://github.com/Shivers-Randomizer/Shivers-Randomizer/releases/latest) Latest release version
|
||||
|
||||
## Optional Software
|
||||
|
||||
- [PopTracker](https://github.com/black-sliver/PopTracker/releases/)
|
||||
- [Jax's Shivers PopTracker pack](https://github.com/blazik-barth/Shivers-Tracker/releases/)
|
||||
- [Shivers PopTracker pack](https://github.com/Shivers-Randomizer/Shivers-AP-Tracker/releases/latest)
|
||||
|
||||
## Setup ScummVM for Shivers
|
||||
|
||||
@@ -59,7 +59,9 @@ validator page: [YAML Validation page](/mysterycheck)
|
||||
|
||||
## What is a check
|
||||
|
||||
- Every puzzle
|
||||
- Every puzzle hint/solution
|
||||
- Every document that is considered a Flashback
|
||||
- All puzzles
|
||||
- All puzzle hints or solutions
|
||||
- All documents that are considered Flashbacks
|
||||
- All Ixupi captures (Lightning only if early)
|
||||
- Optionally information plaques
|
||||
- Optionally elevators
|
||||
|
||||
@@ -48,6 +48,17 @@ class SM64World(World):
|
||||
filler_count: int
|
||||
star_costs: typing.Dict[str, int]
|
||||
|
||||
# Spoiler specific variable(s)
|
||||
star_costs_spoiler_key_maxlen = len(max([
|
||||
'First Floor Big Star Door',
|
||||
'Basement Big Star Door',
|
||||
'Second Floor Big Star Door',
|
||||
'MIPS 1',
|
||||
'MIPS 2',
|
||||
'Endless Stairs',
|
||||
], key=len))
|
||||
|
||||
|
||||
def generate_early(self):
|
||||
max_stars = 120
|
||||
if (not self.options.enable_coin_stars):
|
||||
@@ -238,3 +249,19 @@ class SM64World(World):
|
||||
for location in region.locations:
|
||||
er_hint_data[location.address] = entrance_name
|
||||
hint_data[self.player] = er_hint_data
|
||||
|
||||
def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
|
||||
# Write calculated star costs to spoiler.
|
||||
star_cost_spoiler_header = '\n\n' + self.player_name + ' Star Costs for Super Mario 64:\n\n'
|
||||
spoiler_handle.write(star_cost_spoiler_header)
|
||||
# - Reformat star costs dictionary in spoiler to be a bit more readable.
|
||||
star_costs_spoiler = {}
|
||||
star_costs_copy = self.star_costs.copy()
|
||||
star_costs_spoiler['First Floor Big Star Door'] = star_costs_copy['FirstBowserDoorCost']
|
||||
star_costs_spoiler['Basement Big Star Door'] = star_costs_copy['BasementDoorCost']
|
||||
star_costs_spoiler['Second Floor Big Star Door'] = star_costs_copy['SecondFloorDoorCost']
|
||||
star_costs_spoiler['MIPS 1'] = star_costs_copy['MIPS1Cost']
|
||||
star_costs_spoiler['MIPS 2'] = star_costs_copy['MIPS2Cost']
|
||||
star_costs_spoiler['Endless Stairs'] = star_costs_copy['StarsToFinish']
|
||||
for star, cost in star_costs_spoiler.items():
|
||||
spoiler_handle.write(f"{star:{self.star_costs_spoiler_key_maxlen}s} = {cost}\n")
|
||||
|
||||
@@ -217,6 +217,10 @@ class SMZ3World(World):
|
||||
SMZ3World.location_names = frozenset(self.smz3World.locationLookup.keys())
|
||||
|
||||
self.multiworld.state.smz3state[self.player] = TotalSMZ3Item.Progression([])
|
||||
|
||||
if not self.smz3World.Config.Keysanity:
|
||||
# Dungeons items here are not in the itempool and will be prefilled locally so they must stay local
|
||||
self.options.non_local_items.value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name))
|
||||
|
||||
def create_items(self):
|
||||
self.dungeon = TotalSMZ3Item.Item.CreateDungeonPool(self.smz3World)
|
||||
@@ -233,8 +237,6 @@ class SMZ3World(World):
|
||||
progressionItems = self.progression + self.dungeon + self.keyCardsItems + self.SmMapsItems
|
||||
else:
|
||||
progressionItems = self.progression
|
||||
# Dungeons items here are not in the itempool and will be prefilled locally so they must stay local
|
||||
self.options.non_local_items.value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name))
|
||||
for item in self.keyCardsItems:
|
||||
self.multiworld.push_precollected(SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item))
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from random import Random
|
||||
from typing import Dict, Any, Iterable, Optional, List, TextIO
|
||||
from typing import Dict, Any, Iterable, Optional, List, TextIO, cast
|
||||
|
||||
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
|
||||
from Options import PerGameCommonOptions
|
||||
@@ -124,7 +124,7 @@ class StardewValleyWorld(World):
|
||||
self.options)
|
||||
|
||||
def add_location(name: str, code: Optional[int], region: str):
|
||||
region = world_regions[region]
|
||||
region: Region = world_regions[region]
|
||||
location = StardewLocation(self.player, name, code, region)
|
||||
region.locations.append(location)
|
||||
|
||||
@@ -314,9 +314,9 @@ class StardewValleyWorld(World):
|
||||
include_traps = True
|
||||
exclude_island = False
|
||||
for player in link_group["players"]:
|
||||
player_options = self.multiworld.worlds[player].options
|
||||
if self.multiworld.game[player] != self.game:
|
||||
continue
|
||||
player_options = cast(StardewValleyOptions, self.multiworld.worlds[player].options)
|
||||
if player_options.trap_items == TrapItems.option_no_traps:
|
||||
include_traps = False
|
||||
if player_options.exclude_ginger_island == ExcludeGingerIsland.option_true:
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import List
|
||||
|
||||
from .bundle import Bundle, BundleTemplate
|
||||
from ..content import StardewContent
|
||||
from ..options import BundlePrice, StardewValleyOptions
|
||||
from ..options import StardewValleyOptions
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -10,15 +10,14 @@ from ...data.shop import ShopSource
|
||||
from ...mods.mod_data import ModNames
|
||||
from ...strings.craftable_names import ModEdible
|
||||
from ...strings.crop_names import Fruit, SVEVegetable, SVEFruit
|
||||
from ...strings.fish_names import WaterItem, SVEFish, SVEWaterItem
|
||||
from ...strings.fish_names import WaterItem, SVEWaterItem
|
||||
from ...strings.flower_names import Flower
|
||||
from ...strings.food_names import SVEMeal, SVEBeverage
|
||||
from ...strings.forageable_names import Mushroom, Forageable, SVEForage
|
||||
from ...strings.gift_names import SVEGift
|
||||
from ...strings.metal_names import Ore
|
||||
from ...strings.monster_drop_names import ModLoot, Loot
|
||||
from ...strings.monster_drop_names import ModLoot
|
||||
from ...strings.performance_names import Performance
|
||||
from ...strings.region_names import Region, SVERegion, LogicRegion
|
||||
from ...strings.region_names import Region, SVERegion
|
||||
from ...strings.season_names import Season
|
||||
from ...strings.seed_names import SVESeed
|
||||
from ...strings.skill_names import Skill
|
||||
@@ -81,7 +80,8 @@ register_mod_content_pack(SVEContentPack(
|
||||
ModEdible.lightning_elixir: (ShopSource(money_price=12000, shop_region=SVERegion.galmoran_outpost),),
|
||||
ModEdible.barbarian_elixir: (ShopSource(money_price=22000, shop_region=SVERegion.galmoran_outpost),),
|
||||
ModEdible.gravity_elixir: (ShopSource(money_price=4000, shop_region=SVERegion.galmoran_outpost),),
|
||||
SVEMeal.grampleton_orange_chicken: (ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),),
|
||||
SVEMeal.grampleton_orange_chicken: (
|
||||
ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),),
|
||||
ModEdible.hero_elixir: (ShopSource(money_price=8000, shop_region=SVERegion.isaac_shop),),
|
||||
ModEdible.aegis_elixir: (ShopSource(money_price=28000, shop_region=SVERegion.galmoran_outpost),),
|
||||
SVEBeverage.sports_drink: (ShopSource(money_price=750, shop_region=Region.hospital),),
|
||||
@@ -92,7 +92,8 @@ register_mod_content_pack(SVEContentPack(
|
||||
ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.summer, Season.fall)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), )
|
||||
),
|
||||
Mushroom.purple: (
|
||||
ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave, SVERegion.junimo_woods), )
|
||||
ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)),
|
||||
ForagingSource(regions=(SVERegion.sprite_spring_cave, SVERegion.junimo_woods), )
|
||||
),
|
||||
Mushroom.morel: (
|
||||
ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), )
|
||||
@@ -117,7 +118,8 @@ register_mod_content_pack(SVEContentPack(
|
||||
|
||||
ModLoot.green_mushroom: (ForagingSource(regions=(SVERegion.highlands_pond,), seasons=Season.not_winter),),
|
||||
ModLoot.ornate_treasure_chest: (ForagingSource(regions=(SVERegion.highlands_outside,),
|
||||
other_requirements=(CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),),
|
||||
other_requirements=(
|
||||
CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),),
|
||||
ModLoot.swirl_stone: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.galaxy),)),),
|
||||
ModLoot.void_soul: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.good),)),),
|
||||
SVEForage.winter_star_rose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.winter,)),),
|
||||
@@ -137,7 +139,8 @@ register_mod_content_pack(SVEContentPack(
|
||||
SVEForage.thistle: (ForagingSource(regions=(SVERegion.summit,)),),
|
||||
ModLoot.void_pebble: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),),
|
||||
ModLoot.void_shard: (ForagingSource(regions=(SVERegion.crimson_badlands,),
|
||||
other_requirements=(CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),),
|
||||
other_requirements=(
|
||||
CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),),
|
||||
SVEWaterItem.dulse_seaweed: (ForagingSource(regions=(Region.beach,), other_requirements=(FishingRequirement(Region.beach),)),),
|
||||
|
||||
# Fable Reef
|
||||
|
||||
@@ -140,7 +140,7 @@ base_game = BaseGameContentPack(
|
||||
|
||||
Vegetable.broccoli: (HarvestCropSource(seed=Seed.broccoli, seasons=(Season.fall,)),),
|
||||
Vegetable.carrot: (HarvestCropSource(seed=Seed.carrot, seasons=(Season.spring,)),),
|
||||
Fruit.powdermelon: (HarvestCropSource(seed=Seed.powdermelon, seasons=(Season.summer,)),),
|
||||
Fruit.powdermelon: (HarvestCropSource(seed=Seed.powdermelon, seasons=(Season.winter,)),),
|
||||
Vegetable.summer_squash: (HarvestCropSource(seed=Seed.summer_squash, seasons=(Season.summer,)),),
|
||||
|
||||
Fruit.strawberry: (HarvestCropSource(seed=Seed.strawberry, seasons=(Season.spring,)),),
|
||||
|
||||
@@ -6,7 +6,6 @@ from ...data.game_item import GenericSource, ItemTag
|
||||
from ...data.harvest import HarvestCropSource
|
||||
from ...strings.crop_names import Fruit
|
||||
from ...strings.region_names import Region
|
||||
from ...strings.season_names import Season
|
||||
from ...strings.seed_names import Seed
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from ..strings.craftable_names import Fishing, Craftable, Bomb, Consumable, Ligh
|
||||
from ..strings.crop_names import Fruit, Vegetable
|
||||
from ..strings.currency_names import Currency
|
||||
from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro
|
||||
from ..strings.fish_names import Fish, WaterItem, Trash, all_fish
|
||||
from ..strings.fish_names import Fish, WaterItem, Trash
|
||||
from ..strings.flower_names import Flower
|
||||
from ..strings.food_names import Beverage, Meal
|
||||
from ..strings.forageable_names import Forageable, Mushroom
|
||||
@@ -832,7 +832,7 @@ calico_items = [calico_egg.as_amount(200), calico_egg.as_amount(200), calico_egg
|
||||
magic_rock_candy, mega_bomb.as_amount(10), mystery_box.as_amount(10), mixed_seeds.as_amount(50),
|
||||
strawberry_seeds.as_amount(20),
|
||||
spicy_eel.as_amount(5), crab_cakes.as_amount(5), eggplant_parmesan.as_amount(5),
|
||||
pumpkin_soup.as_amount(5), lucky_lunch.as_amount(5),]
|
||||
pumpkin_soup.as_amount(5), lucky_lunch.as_amount(5)]
|
||||
calico_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.calico, calico_items, 2, 2)
|
||||
|
||||
raccoon_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.raccoon, raccoon_foraging_items, 4, 4)
|
||||
|
||||
@@ -14,7 +14,7 @@ from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro
|
||||
from ..strings.fish_names import Fish, WaterItem, ModTrash, Trash
|
||||
from ..strings.flower_names import Flower
|
||||
from ..strings.food_names import Meal
|
||||
from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom
|
||||
from ..strings.forageable_names import Forageable, DistantLandsForageable, Mushroom
|
||||
from ..strings.gift_names import Gift
|
||||
from ..strings.ingredient_names import Ingredient
|
||||
from ..strings.machine_names import Machine
|
||||
@@ -318,7 +318,8 @@ travel_charm = shop_recipe(ModCraftable.travel_core, Region.adventurer_guild, 25
|
||||
preservation_chamber = skill_recipe(ModMachine.preservation_chamber, ModSkill.archaeology, 1,
|
||||
{MetalBar.copper: 1, Material.wood: 15, ArtisanGood.oak_resin: 30},
|
||||
ModNames.archaeology)
|
||||
restoration_table = skill_recipe(ModMachine.restoration_table, ModSkill.archaeology, 1, {Material.wood: 15, MetalBar.copper: 1, MetalBar.iron: 1}, ModNames.archaeology)
|
||||
restoration_table = skill_recipe(ModMachine.restoration_table, ModSkill.archaeology, 1, {Material.wood: 15, MetalBar.copper: 1, MetalBar.iron: 1},
|
||||
ModNames.archaeology)
|
||||
preservation_chamber_h = skill_recipe(ModMachine.hardwood_preservation_chamber, ModSkill.archaeology, 6, {MetalBar.copper: 1, Material.hardwood: 15,
|
||||
ArtisanGood.oak_resin: 30}, ModNames.archaeology)
|
||||
grinder = skill_recipe(ModMachine.grinder, ModSkill.archaeology, 2, {Artifact.rusty_cog: 10, MetalBar.iron: 5, ArtisanGood.battery_pack: 1},
|
||||
@@ -330,12 +331,14 @@ glass_path = skill_recipe(ModFloor.glass_path, ModSkill.archaeology, 3, {Artifac
|
||||
glass_fence = skill_recipe(ModCraftable.glass_fence, ModSkill.archaeology, 7, {Artifact.glass_shards: 5}, ModNames.archaeology)
|
||||
bone_path = skill_recipe(ModFloor.bone_path, ModSkill.archaeology, 4, {Fossil.bone_fragment: 1}, ModNames.archaeology)
|
||||
rust_path = skill_recipe(ModFloor.rusty_path, ModSkill.archaeology, 2, {ModTrash.rusty_scrap: 2}, ModNames.archaeology)
|
||||
rusty_brazier = skill_recipe(ModCraftable.rusty_brazier, ModSkill.archaeology, 3, {ModTrash.rusty_scrap: 10, Material.coal: 1, Material.fiber: 1}, ModNames.archaeology)
|
||||
rusty_brazier = skill_recipe(ModCraftable.rusty_brazier, ModSkill.archaeology, 3, {ModTrash.rusty_scrap: 10, Material.coal: 1, Material.fiber: 1},
|
||||
ModNames.archaeology)
|
||||
bone_fence = skill_recipe(ModCraftable.bone_fence, ModSkill.archaeology, 8, {Fossil.bone_fragment: 2}, ModNames.archaeology)
|
||||
water_shifter = skill_recipe(ModCraftable.water_shifter, ModSkill.archaeology, 4, {Material.wood: 40, MetalBar.copper: 4}, ModNames.archaeology)
|
||||
wooden_display = skill_recipe(ModCraftable.wooden_display, ModSkill.archaeology, 1, {Material.wood: 25}, ModNames.archaeology)
|
||||
hardwood_display = skill_recipe(ModCraftable.hardwood_display, ModSkill.archaeology, 7, {Material.hardwood: 10}, ModNames.archaeology)
|
||||
lucky_ring = skill_recipe(Ring.lucky_ring, ModSkill.archaeology, 8, {Artifact.elvish_jewelry: 1, AnimalProduct.rabbit_foot: 5, Mineral.tigerseye: 1}, ModNames.archaeology)
|
||||
lucky_ring = skill_recipe(Ring.lucky_ring, ModSkill.archaeology, 8, {Artifact.elvish_jewelry: 1, AnimalProduct.rabbit_foot: 5, Mineral.tigerseye: 1},
|
||||
ModNames.archaeology)
|
||||
volcano_totem = skill_recipe(ModConsumable.volcano_totem, ModSkill.archaeology, 9, {Material.cinder_shard: 5, Artifact.rare_disc: 1, Artifact.dwarf_gadget: 1},
|
||||
ModNames.archaeology)
|
||||
haste_elixir = shop_recipe(ModEdible.haste_elixir, SVERegion.alesia_shop, 35000, {Loot.void_essence: 35, ModLoot.void_soul: 5, Ingredient.sugar: 1,
|
||||
|
||||
@@ -2938,7 +2938,7 @@ id,region,name,tags,mod_name
|
||||
7440,Farm,Craft Copper Slot Machine,"CRAFTSANITY",Luck Skill
|
||||
7441,Farm,Craft Gold Slot Machine,"CRAFTSANITY",Luck Skill
|
||||
7442,Farm,Craft Iridium Slot Machine,"CRAFTSANITY",Luck Skill
|
||||
7443,Farm,Craft Radioactive Slot Machine,"CRAFTSANITY",Luck Skill
|
||||
7443,Farm,Craft Radioactive Slot Machine,"CRAFTSANITY,GINGER_ISLAND",Luck Skill
|
||||
7451,Adventurer's Guild,Magic Elixir Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Magic
|
||||
7452,Adventurer's Guild,Travel Core Recipe,CRAFTSANITY,Magic
|
||||
7453,Alesia Shop,Haste Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded
|
||||
|
||||
|
@@ -1,15 +1,16 @@
|
||||
from typing import Dict, List, Optional
|
||||
from ..mods.mod_data import ModNames
|
||||
|
||||
from .recipe_source import RecipeSource, FriendshipSource, SkillSource, QueenOfSauceSource, ShopSource, StarterSource, ShopTradeSource, ShopFriendshipSource
|
||||
from ..mods.mod_data import ModNames
|
||||
from ..strings.animal_product_names import AnimalProduct
|
||||
from ..strings.artisan_good_names import ArtisanGood
|
||||
from ..strings.craftable_names import ModEdible, Edible
|
||||
from ..strings.crop_names import Fruit, Vegetable, SVEFruit, DistantLandsCrop
|
||||
from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish, SVEWaterItem
|
||||
from ..strings.flower_names import Flower
|
||||
from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom
|
||||
from ..strings.ingredient_names import Ingredient
|
||||
from ..strings.food_names import Meal, SVEMeal, Beverage, DistantLandsMeal, BoardingHouseMeal, ArchaeologyMeal, TrashyMeal
|
||||
from ..strings.forageable_names import Forageable, SVEForage, Mushroom
|
||||
from ..strings.ingredient_names import Ingredient
|
||||
from ..strings.material_names import Material
|
||||
from ..strings.metal_names import Fossil, Artifact
|
||||
from ..strings.monster_drop_names import Loot
|
||||
@@ -45,7 +46,8 @@ def friendship_recipe(name: str, friend: str, hearts: int, ingredients: Dict[str
|
||||
return create_recipe(name, ingredients, source, mod_name)
|
||||
|
||||
|
||||
def friendship_and_shop_recipe(name: str, friend: str, hearts: int, region: str, price: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CookingRecipe:
|
||||
def friendship_and_shop_recipe(name: str, friend: str, hearts: int, region: str, price: int, ingredients: Dict[str, int],
|
||||
mod_name: Optional[str] = None) -> CookingRecipe:
|
||||
source = ShopFriendshipSource(friend, hearts, region, price)
|
||||
return create_recipe(name, ingredients, source, mod_name)
|
||||
|
||||
@@ -85,7 +87,8 @@ algae_soup = friendship_recipe(Meal.algae_soup, NPC.clint, 3, {WaterItem.green_a
|
||||
artichoke_dip = queen_of_sauce_recipe(Meal.artichoke_dip, 1, Season.fall, 28, {Vegetable.artichoke: 1, AnimalProduct.cow_milk: 1})
|
||||
autumn_bounty = friendship_recipe(Meal.autumn_bounty, NPC.demetrius, 7, {Vegetable.yam: 1, Vegetable.pumpkin: 1})
|
||||
baked_fish = queen_of_sauce_recipe(Meal.baked_fish, 1, Season.summer, 7, {Fish.sunfish: 1, Fish.bream: 1, Ingredient.wheat_flour: 1})
|
||||
banana_pudding = shop_trade_recipe(Meal.banana_pudding, Region.island_trader, Fossil.bone_fragment, 30, {Fruit.banana: 1, AnimalProduct.cow_milk: 1, Ingredient.sugar: 1})
|
||||
banana_pudding = shop_trade_recipe(Meal.banana_pudding, Region.island_trader, Fossil.bone_fragment, 30,
|
||||
{Fruit.banana: 1, AnimalProduct.cow_milk: 1, Ingredient.sugar: 1})
|
||||
bean_hotpot = friendship_recipe(Meal.bean_hotpot, NPC.clint, 7, {Vegetable.green_bean: 2})
|
||||
blackberry_cobbler_ingredients = {Forageable.blackberry: 2, Ingredient.sugar: 1, Ingredient.wheat_flour: 1}
|
||||
blackberry_cobbler_qos = queen_of_sauce_recipe(Meal.blackberry_cobbler, 2, Season.fall, 14, blackberry_cobbler_ingredients)
|
||||
@@ -181,21 +184,23 @@ vegetable_medley = friendship_recipe(Meal.vegetable_medley, NPC.caroline, 7, {Ve
|
||||
magic_elixir = shop_recipe(ModEdible.magic_elixir, Region.adventurer_guild, 3000, {Edible.life_elixir: 1, Mushroom.purple: 1}, ModNames.magic)
|
||||
|
||||
baked_berry_oatmeal = shop_recipe(SVEMeal.baked_berry_oatmeal, SVERegion.bear_shop, 0, {Forageable.salmonberry: 15, Forageable.blackberry: 15,
|
||||
Ingredient.sugar: 1, Ingredient.wheat_flour: 2}, ModNames.sve)
|
||||
Ingredient.sugar: 1, Ingredient.wheat_flour: 2}, ModNames.sve)
|
||||
big_bark_burger = friendship_and_shop_recipe(SVEMeal.big_bark_burger, NPC.gus, 5, Region.saloon, 5500,
|
||||
{SVEFish.puppyfish: 1, Meal.bread: 1, Ingredient.oil: 1}, ModNames.sve)
|
||||
flower_cookie = shop_recipe(SVEMeal.flower_cookie, SVERegion.bear_shop, 0, {SVEForage.ferngill_primrose: 1, SVEForage.goldenrod: 1,
|
||||
SVEForage.winter_star_rose: 1, Ingredient.wheat_flour: 1, Ingredient.sugar: 1,
|
||||
AnimalProduct.large_egg: 1}, ModNames.sve)
|
||||
SVEForage.winter_star_rose: 1, Ingredient.wheat_flour: 1, Ingredient.sugar: 1,
|
||||
AnimalProduct.large_egg: 1}, ModNames.sve)
|
||||
frog_legs = shop_recipe(SVEMeal.frog_legs, Region.adventurer_guild, 2000, {SVEFish.frog: 1, Ingredient.oil: 1, Ingredient.wheat_flour: 1}, ModNames.sve)
|
||||
glazed_butterfish = friendship_and_shop_recipe(SVEMeal.glazed_butterfish, NPC.gus, 10, Region.saloon, 4000,
|
||||
{SVEFish.butterfish: 1, Ingredient.wheat_flour: 1, Ingredient.oil: 1}, ModNames.sve)
|
||||
mixed_berry_pie = shop_recipe(SVEMeal.mixed_berry_pie, Region.saloon, 3500, {Fruit.strawberry: 6, SVEFruit.salal_berry: 6, Forageable.blackberry: 6,
|
||||
SVEForage.bearberry: 6, Ingredient.sugar: 1, Ingredient.wheat_flour: 1},
|
||||
ModNames.sve)
|
||||
mushroom_berry_rice = friendship_and_shop_recipe(SVEMeal.mushroom_berry_rice, ModNPC.marlon, 6, Region.adventurer_guild, 1500, {SVEForage.poison_mushroom: 3, SVEForage.red_baneberry: 10,
|
||||
Ingredient.rice: 1, Ingredient.sugar: 2}, ModNames.sve)
|
||||
seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEWaterItem.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1}, ModNames.sve)
|
||||
mushroom_berry_rice = friendship_and_shop_recipe(SVEMeal.mushroom_berry_rice, ModNPC.marlon, 6, Region.adventurer_guild, 1500,
|
||||
{SVEForage.poison_mushroom: 3, SVEForage.red_baneberry: 10, Ingredient.rice: 1, Ingredient.sugar: 2},
|
||||
ModNames.sve)
|
||||
seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEWaterItem.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1},
|
||||
ModNames.sve)
|
||||
void_delight = friendship_and_shop_recipe(SVEMeal.void_delight, NPC.krobus, 10, Region.sewer, 5000,
|
||||
{SVEFish.void_eel: 1, Loot.void_essence: 50, Loot.solar_essence: 20}, ModNames.sve)
|
||||
void_salmon_sushi = friendship_and_shop_recipe(SVEMeal.void_salmon_sushi, NPC.krobus, 10, Region.sewer, 5000,
|
||||
@@ -205,17 +210,22 @@ mushroom_kebab = friendship_recipe(DistantLandsMeal.mushroom_kebab, ModNPC.gobli
|
||||
Mushroom.red: 1, Material.wood: 1}, ModNames.distant_lands)
|
||||
void_mint_tea = friendship_recipe(DistantLandsMeal.void_mint_tea, ModNPC.goblin, 4, {DistantLandsCrop.void_mint: 1}, ModNames.distant_lands)
|
||||
crayfish_soup = friendship_recipe(DistantLandsMeal.crayfish_soup, ModNPC.goblin, 6, {Forageable.cave_carrot: 1, Fish.crayfish: 1,
|
||||
DistantLandsFish.purple_algae: 1, WaterItem.white_algae: 1}, ModNames.distant_lands)
|
||||
DistantLandsFish.purple_algae: 1, WaterItem.white_algae: 1},
|
||||
ModNames.distant_lands)
|
||||
pemmican = friendship_recipe(DistantLandsMeal.pemmican, ModNPC.goblin, 8, {Loot.bug_meat: 1, Fish.any: 1, Forageable.salmonberry: 3,
|
||||
Material.stone: 2}, ModNames.distant_lands)
|
||||
|
||||
special_pumpkin_soup = friendship_recipe(BoardingHouseMeal.special_pumpkin_soup, ModNPC.joel, 6, {Vegetable.pumpkin: 2, AnimalProduct.large_goat_milk: 1,
|
||||
Vegetable.garlic: 1}, ModNames.boarding_house)
|
||||
diggers_delight = skill_recipe(ArchaeologyMeal.diggers_delight, ModSkill.archaeology, 3, {Forageable.cave_carrot: 2, Ingredient.sugar: 1, AnimalProduct.milk: 1}, ModNames.archaeology)
|
||||
rocky_root = skill_recipe(ArchaeologyMeal.rocky_root, ModSkill.archaeology, 7, {Forageable.cave_carrot: 3, Seed.coffee: 1, Material.stone: 1}, ModNames.archaeology)
|
||||
ancient_jello = skill_recipe(ArchaeologyMeal.ancient_jello, ModSkill.archaeology, 9, {WaterItem.cave_jelly: 6, Ingredient.sugar: 5, AnimalProduct.egg: 1, AnimalProduct.milk: 1, Artifact.chipped_amphora: 1}, ModNames.archaeology)
|
||||
diggers_delight = skill_recipe(ArchaeologyMeal.diggers_delight, ModSkill.archaeology, 3,
|
||||
{Forageable.cave_carrot: 2, Ingredient.sugar: 1, AnimalProduct.milk: 1}, ModNames.archaeology)
|
||||
rocky_root = skill_recipe(ArchaeologyMeal.rocky_root, ModSkill.archaeology, 7, {Forageable.cave_carrot: 3, Seed.coffee: 1, Material.stone: 1},
|
||||
ModNames.archaeology)
|
||||
ancient_jello = skill_recipe(ArchaeologyMeal.ancient_jello, ModSkill.archaeology, 9,
|
||||
{WaterItem.cave_jelly: 6, Ingredient.sugar: 5, AnimalProduct.egg: 1, AnimalProduct.milk: 1, Artifact.chipped_amphora: 1},
|
||||
ModNames.archaeology)
|
||||
|
||||
grilled_cheese = skill_recipe(TrashyMeal.grilled_cheese, ModSkill.binning, 1, {Meal.bread: 1, ArtisanGood.cheese: 1}, ModNames.binning_skill)
|
||||
fish_casserole = skill_recipe(TrashyMeal.fish_casserole, ModSkill.binning, 8, {Fish.any: 1, AnimalProduct.milk: 1, Vegetable.carrot: 1}, ModNames.binning_skill)
|
||||
|
||||
all_cooking_recipes_by_name = {recipe.meal: recipe for recipe in all_cooking_recipes}
|
||||
all_cooking_recipes_by_name = {recipe.meal: recipe for recipe in all_cooking_recipes}
|
||||
|
||||
@@ -106,7 +106,7 @@ class MasterySource(RecipeSource):
|
||||
self.skill = skill
|
||||
|
||||
def __repr__(self):
|
||||
return f"MasterySource at level {self.level} {self.skill}"
|
||||
return f"MasterySource {self.skill}"
|
||||
|
||||
|
||||
class ShopSource(RecipeSource):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import typing
|
||||
from typing import Union
|
||||
|
||||
from .base_logic import BaseLogicMixin, BaseLogic
|
||||
from .cooking_logic import CookingLogicMixin
|
||||
from .mine_logic import MineLogicMixin
|
||||
from .received_logic import ReceivedLogicMixin
|
||||
from .region_logic import RegionLogicMixin
|
||||
@@ -13,6 +13,11 @@ from ..strings.region_names import Region
|
||||
from ..strings.skill_names import Skill, ModSkill
|
||||
from ..strings.tool_names import ToolMaterial, Tool
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from ..mods.logic.mod_logic import ModLogicMixin
|
||||
else:
|
||||
ModLogicMixin = object
|
||||
|
||||
|
||||
class AbilityLogicMixin(BaseLogicMixin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -20,7 +25,8 @@ class AbilityLogicMixin(BaseLogicMixin):
|
||||
self.ability = AbilityLogic(*args, **kwargs)
|
||||
|
||||
|
||||
class AbilityLogic(BaseLogic[Union[AbilityLogicMixin, RegionLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, MineLogicMixin, MagicLogicMixin]]):
|
||||
class AbilityLogic(BaseLogic[Union[AbilityLogicMixin, RegionLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, MineLogicMixin, MagicLogicMixin,
|
||||
ModLogicMixin]]):
|
||||
def can_mine_perfectly(self) -> StardewRule:
|
||||
return self.logic.mine.can_progress_in_the_mines_from_floor(160)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from .has_logic import HasLogicMixin
|
||||
from .received_logic import ReceivedLogicMixin
|
||||
from .region_logic import RegionLogicMixin
|
||||
from .tool_logic import ToolLogicMixin
|
||||
from ..options import ToolProgression
|
||||
from ..stardew_rule import StardewRule, True_
|
||||
from ..strings.generic_names import Generic
|
||||
from ..strings.geode_names import Geode
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import typing
|
||||
from functools import cached_property
|
||||
from typing import Union, Tuple
|
||||
|
||||
@@ -24,6 +25,11 @@ from ..strings.skill_names import Skill, all_mod_skills, all_vanilla_skills
|
||||
from ..strings.tool_names import ToolMaterial, Tool
|
||||
from ..strings.wallet_item_names import Wallet
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from ..mods.logic.mod_logic import ModLogicMixin
|
||||
else:
|
||||
ModLogicMixin = object
|
||||
|
||||
fishing_regions = (Region.beach, Region.town, Region.forest, Region.mountain, Region.island_south, Region.island_west)
|
||||
vanilla_skill_items = ("Farming Level", "Mining Level", "Foraging Level", "Fishing Level", "Combat Level")
|
||||
|
||||
@@ -35,7 +41,7 @@ class SkillLogicMixin(BaseLogicMixin):
|
||||
|
||||
|
||||
class SkillLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, TimeLogicMixin, ToolLogicMixin, SkillLogicMixin,
|
||||
CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
|
||||
CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin, ModLogicMixin]]):
|
||||
|
||||
# Should be cached
|
||||
def can_earn_level(self, skill: str, level: int) -> StardewRule:
|
||||
|
||||
@@ -2,7 +2,6 @@ from typing import Dict, Union
|
||||
|
||||
from ..mod_data import ModNames
|
||||
from ... import options
|
||||
from ...data.craftable_data import all_crafting_recipes_by_name
|
||||
from ...logic.base_logic import BaseLogicMixin, BaseLogic
|
||||
from ...logic.combat_logic import CombatLogicMixin
|
||||
from ...logic.cooking_logic import CookingLogicMixin
|
||||
@@ -20,11 +19,9 @@ from ...logic.season_logic import SeasonLogicMixin
|
||||
from ...logic.skill_logic import SkillLogicMixin
|
||||
from ...logic.time_logic import TimeLogicMixin
|
||||
from ...logic.tool_logic import ToolLogicMixin
|
||||
from ...options import Cropsanity
|
||||
from ...stardew_rule import StardewRule, True_
|
||||
from ...stardew_rule import StardewRule
|
||||
from ...strings.artisan_good_names import ModArtisanGood
|
||||
from ...strings.craftable_names import ModCraftable, ModMachine
|
||||
from ...strings.fish_names import ModTrash
|
||||
from ...strings.craftable_names import ModCraftable
|
||||
from ...strings.ingredient_names import Ingredient
|
||||
from ...strings.material_names import Material
|
||||
from ...strings.metal_names import all_fossils, all_artifacts, Ore, ModFossil
|
||||
|
||||
@@ -3,8 +3,8 @@ from typing import Dict, Union
|
||||
from ..mod_data import ModNames
|
||||
from ...logic.base_logic import BaseLogic, BaseLogicMixin
|
||||
from ...logic.has_logic import HasLogicMixin
|
||||
from ...logic.quest_logic import QuestLogicMixin
|
||||
from ...logic.monster_logic import MonsterLogicMixin
|
||||
from ...logic.quest_logic import QuestLogicMixin
|
||||
from ...logic.received_logic import ReceivedLogicMixin
|
||||
from ...logic.region_logic import RegionLogicMixin
|
||||
from ...logic.relationship_logic import RelationshipLogicMixin
|
||||
@@ -16,7 +16,6 @@ from ...strings.artisan_good_names import ArtisanGood
|
||||
from ...strings.crop_names import Fruit, SVEFruit, SVEVegetable, Vegetable
|
||||
from ...strings.fertilizer_names import Fertilizer
|
||||
from ...strings.food_names import Meal, Beverage
|
||||
from ...strings.forageable_names import SVEForage
|
||||
from ...strings.material_names import Material
|
||||
from ...strings.metal_names import Ore, MetalBar
|
||||
from ...strings.monster_drop_names import Loot, ModLoot
|
||||
@@ -35,7 +34,7 @@ class ModQuestLogicMixin(BaseLogicMixin):
|
||||
|
||||
|
||||
class ModQuestLogic(BaseLogic[Union[HasLogicMixin, QuestLogicMixin, ReceivedLogicMixin, RegionLogicMixin,
|
||||
TimeLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MonsterLogicMixin]]):
|
||||
TimeLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MonsterLogicMixin]]):
|
||||
def get_modded_quest_rules(self) -> Dict[str, StardewRule]:
|
||||
quests = dict()
|
||||
quests.update(self._get_juna_quest_rules())
|
||||
|
||||
@@ -66,7 +66,8 @@ class Goal(Choice):
|
||||
|
||||
|
||||
class FarmType(Choice):
|
||||
"""What farm to play on?"""
|
||||
"""What farm to play on?
|
||||
Custom farms are not supported"""
|
||||
internal_name = "farm_type"
|
||||
display_name = "Farm Type"
|
||||
default = "random"
|
||||
@@ -203,7 +204,7 @@ class SeasonRandomization(Choice):
|
||||
|
||||
|
||||
class Cropsanity(Choice):
|
||||
"""Formerly named "Seed Shuffle"
|
||||
"""
|
||||
Pierre now sells a random amount of seasonal seeds and Joja sells them without season requirements, but only in huge packs.
|
||||
Disabled: All the seeds are unlocked from the start, there are no location checks for growing and harvesting crops
|
||||
Enabled: Seeds are unlocked as archipelago items, for each seed there is a location check for growing and harvesting that crop
|
||||
@@ -233,9 +234,9 @@ class BackpackProgression(Choice):
|
||||
class ToolProgression(Choice):
|
||||
"""Shuffle the tool upgrades?
|
||||
Vanilla: Clint will upgrade your tools with metal bars.
|
||||
Progressive: You will randomly find Progressive Tool upgrades.
|
||||
Cheap: Tool Upgrades will cost 2/5th as much
|
||||
Very Cheap: Tool Upgrades will cost 1/5th as much"""
|
||||
Progressive: Your tools upgrades are randomized.
|
||||
Cheap: Tool Upgrades have a 60% discount
|
||||
Very Cheap: Tool Upgrades have an 80% discount"""
|
||||
internal_name = "tool_progression"
|
||||
display_name = "Tool Progression"
|
||||
default = 1
|
||||
@@ -279,8 +280,8 @@ class BuildingProgression(Choice):
|
||||
Vanilla: You can buy each building normally.
|
||||
Progressive: You will receive the buildings and will be able to build the first one of each type for free,
|
||||
once it is received. If you want more of the same building, it will cost the vanilla price.
|
||||
Cheap: Buildings will cost half as much
|
||||
Very Cheap: Buildings will cost 1/5th as much
|
||||
Cheap: Buildings will have a 50% discount
|
||||
Very Cheap: Buildings will an 80% discount
|
||||
"""
|
||||
internal_name = "building_progression"
|
||||
display_name = "Building Progression"
|
||||
@@ -327,7 +328,7 @@ class ArcadeMachineLocations(Choice):
|
||||
|
||||
class SpecialOrderLocations(Choice):
|
||||
"""Shuffle Special Orders?
|
||||
Disabled: The special orders are not included in the Archipelago shuffling.
|
||||
Vanilla: The special orders are not included in the Archipelago shuffling. You may need to complete some of them anyway for their vanilla rewards
|
||||
Board Only: The Special Orders on the board in town are location checks
|
||||
Board and Qi: The Special Orders from Mr Qi's walnut room are checks, in addition to the board in town
|
||||
Short: All Special Order requirements are reduced by 40%
|
||||
@@ -377,12 +378,12 @@ class QuestLocations(NamedRange):
|
||||
|
||||
|
||||
class Fishsanity(Choice):
|
||||
"""Locations for catching a fish the first time?
|
||||
"""Locations for catching each fish the first time?
|
||||
None: There are no locations for catching fish
|
||||
Legendaries: Each of the 5 legendary fish are checks, plus the extended family if qi board is turned on
|
||||
Special: A curated selection of strong fish are checks
|
||||
Randomized: A random selection of fish are checks
|
||||
All: Every single fish in the game is a location that contains an item. Pairs well with the Master Angler Goal
|
||||
All: Every single fish in the game is a location that contains an item.
|
||||
Exclude Legendaries: Every fish except legendaries
|
||||
Exclude Hard Fish: Every fish under difficulty 80
|
||||
Only Easy Fish: Every fish under difficulty 50
|
||||
@@ -517,7 +518,7 @@ class Chefsanity(NamedRange):
|
||||
class Craftsanity(Choice):
|
||||
"""Checks for crafting items?
|
||||
If enabled, all recipes purchased in shops will be checks as well.
|
||||
Recipes obtained from other sources will depend on related archipelago settings
|
||||
Recipes obtained from other sources will depend on their respective archipelago settings
|
||||
"""
|
||||
internal_name = "craftsanity"
|
||||
display_name = "Craftsanity"
|
||||
@@ -530,9 +531,9 @@ class Friendsanity(Choice):
|
||||
"""Shuffle Friendships?
|
||||
None: Friendship hearts are earned normally
|
||||
Bachelors: Hearts with bachelors are shuffled
|
||||
Starting NPCs: Hearts for NPCs available immediately are checks
|
||||
All: Hearts for all npcs are checks, including Leo, Kent, Sandy, etc
|
||||
All With Marriage: Hearts for all npcs are checks, including romance hearts up to 14 when applicable
|
||||
Starting NPCs: Hearts for NPCs available immediately are shuffled
|
||||
All: Hearts for all npcs are shuffled, including Leo, Kent, Sandy, etc
|
||||
All With Marriage: All hearts for all npcs are shuffled, including romance hearts up to 14 when applicable
|
||||
"""
|
||||
internal_name = "friendsanity"
|
||||
display_name = "Friendsanity"
|
||||
@@ -577,7 +578,7 @@ class Walnutsanity(OptionSet):
|
||||
"""Shuffle walnuts?
|
||||
Puzzles: Walnuts obtained from solving a special puzzle or winning a minigame
|
||||
Bushes: Walnuts that are in a bush and can be collected by clicking it
|
||||
Dig spots: Walnuts that are underground and must be digged up. Includes Journal scrap walnuts
|
||||
Dig Spots: Walnuts that are underground and must be digged up. Includes Journal scrap walnuts
|
||||
Repeatables: Random chance walnuts from normal actions (fishing, farming, combat, etc)
|
||||
"""
|
||||
internal_name = "walnutsanity"
|
||||
@@ -612,7 +613,7 @@ class NumberOfMovementBuffs(Range):
|
||||
|
||||
class EnabledFillerBuffs(OptionSet):
|
||||
"""Enable various permanent player buffs to roll as filler items
|
||||
Luck: Increase daily luck
|
||||
Luck: Increased daily luck
|
||||
Damage: Increased Damage %
|
||||
Defense: Increased Defense
|
||||
Immunity: Increased Immunity
|
||||
@@ -637,7 +638,7 @@ class EnabledFillerBuffs(OptionSet):
|
||||
class ExcludeGingerIsland(Toggle):
|
||||
"""Exclude Ginger Island?
|
||||
This option will forcefully exclude everything related to Ginger Island from the slot.
|
||||
If you pick a goal that requires Ginger Island, you cannot exclude it and it will get included anyway"""
|
||||
If you pick a goal that requires Ginger Island, this option will get forced to 'false'"""
|
||||
internal_name = "exclude_ginger_island"
|
||||
display_name = "Exclude Ginger Island"
|
||||
default = 0
|
||||
|
||||
@@ -122,7 +122,7 @@ medium_settings = {
|
||||
options.Friendsanity.internal_name: options.Friendsanity.option_starting_npcs,
|
||||
options.FriendsanityHeartSize.internal_name: 4,
|
||||
options.Booksanity.internal_name: options.Booksanity.option_power_skill,
|
||||
options.Walnutsanity.internal_name: [WalnutsanityOptionName.puzzles],
|
||||
options.Walnutsanity.internal_name: options.Walnutsanity.preset_none,
|
||||
options.NumberOfMovementBuffs.internal_name: 6,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
|
||||
|
||||
@@ -7,7 +7,7 @@ from .mods.mod_regions import ModDataList, vanilla_connections_to_remove_by_mod
|
||||
from .options import EntranceRandomization, ExcludeGingerIsland, StardewValleyOptions
|
||||
from .region_classes import RegionData, ConnectionData, RandomizationFlag, ModificationFlag
|
||||
from .strings.entrance_names import Entrance, LogicEntrance
|
||||
from .strings.region_names import Region, LogicRegion
|
||||
from .strings.region_names import Region as RegionName, LogicRegion
|
||||
|
||||
|
||||
class RegionFactory(Protocol):
|
||||
@@ -16,192 +16,192 @@ class RegionFactory(Protocol):
|
||||
|
||||
|
||||
vanilla_regions = [
|
||||
RegionData(Region.menu, [Entrance.to_stardew_valley]),
|
||||
RegionData(Region.stardew_valley, [Entrance.to_farmhouse]),
|
||||
RegionData(Region.farm_house,
|
||||
RegionData(RegionName.menu, [Entrance.to_stardew_valley]),
|
||||
RegionData(RegionName.stardew_valley, [Entrance.to_farmhouse]),
|
||||
RegionData(RegionName.farm_house,
|
||||
[Entrance.farmhouse_to_farm, Entrance.downstairs_to_cellar, LogicEntrance.farmhouse_cooking, LogicEntrance.watch_queen_of_sauce]),
|
||||
RegionData(Region.cellar),
|
||||
RegionData(Region.farm,
|
||||
RegionData(RegionName.cellar),
|
||||
RegionData(RegionName.farm,
|
||||
[Entrance.farm_to_backwoods, Entrance.farm_to_bus_stop, Entrance.farm_to_forest, Entrance.farm_to_farmcave, Entrance.enter_greenhouse,
|
||||
Entrance.enter_coop, Entrance.enter_barn, Entrance.enter_shed, Entrance.enter_slime_hutch, LogicEntrance.grow_spring_crops,
|
||||
LogicEntrance.grow_summer_crops, LogicEntrance.grow_fall_crops, LogicEntrance.grow_winter_crops, LogicEntrance.shipping]),
|
||||
RegionData(Region.backwoods, [Entrance.backwoods_to_mountain]),
|
||||
RegionData(Region.bus_stop,
|
||||
RegionData(RegionName.backwoods, [Entrance.backwoods_to_mountain]),
|
||||
RegionData(RegionName.bus_stop,
|
||||
[Entrance.bus_stop_to_town, Entrance.take_bus_to_desert, Entrance.bus_stop_to_tunnel_entrance]),
|
||||
RegionData(Region.forest,
|
||||
RegionData(RegionName.forest,
|
||||
[Entrance.forest_to_town, Entrance.enter_secret_woods, Entrance.forest_to_wizard_tower, Entrance.forest_to_marnie_ranch,
|
||||
Entrance.forest_to_leah_cottage, Entrance.forest_to_sewer, Entrance.forest_to_mastery_cave, LogicEntrance.buy_from_traveling_merchant,
|
||||
LogicEntrance.complete_raccoon_requests, LogicEntrance.fish_in_waterfall, LogicEntrance.attend_flower_dance, LogicEntrance.attend_trout_derby,
|
||||
LogicEntrance.attend_festival_of_ice]),
|
||||
RegionData(LogicRegion.forest_waterfall),
|
||||
RegionData(Region.farm_cave),
|
||||
RegionData(Region.greenhouse,
|
||||
RegionData(RegionName.farm_cave),
|
||||
RegionData(RegionName.greenhouse,
|
||||
[LogicEntrance.grow_spring_crops_in_greenhouse, LogicEntrance.grow_summer_crops_in_greenhouse, LogicEntrance.grow_fall_crops_in_greenhouse,
|
||||
LogicEntrance.grow_winter_crops_in_greenhouse, LogicEntrance.grow_indoor_crops_in_greenhouse]),
|
||||
RegionData(Region.mountain,
|
||||
RegionData(RegionName.mountain,
|
||||
[Entrance.mountain_to_railroad, Entrance.mountain_to_tent, Entrance.mountain_to_carpenter_shop,
|
||||
Entrance.mountain_to_the_mines, Entrance.enter_quarry, Entrance.mountain_to_adventurer_guild,
|
||||
Entrance.mountain_to_town, Entrance.mountain_to_maru_room,
|
||||
Entrance.mountain_to_leo_treehouse]),
|
||||
RegionData(Region.leo_treehouse, is_ginger_island=True),
|
||||
RegionData(Region.maru_room),
|
||||
RegionData(Region.tunnel_entrance, [Entrance.tunnel_entrance_to_bus_tunnel]),
|
||||
RegionData(Region.bus_tunnel),
|
||||
RegionData(Region.town,
|
||||
RegionData(RegionName.leo_treehouse, is_ginger_island=True),
|
||||
RegionData(RegionName.maru_room),
|
||||
RegionData(RegionName.tunnel_entrance, [Entrance.tunnel_entrance_to_bus_tunnel]),
|
||||
RegionData(RegionName.bus_tunnel),
|
||||
RegionData(RegionName.town,
|
||||
[Entrance.town_to_community_center, Entrance.town_to_beach, Entrance.town_to_hospital, Entrance.town_to_pierre_general_store,
|
||||
Entrance.town_to_saloon, Entrance.town_to_alex_house, Entrance.town_to_trailer, Entrance.town_to_mayor_manor, Entrance.town_to_sam_house,
|
||||
Entrance.town_to_haley_house, Entrance.town_to_sewer, Entrance.town_to_clint_blacksmith, Entrance.town_to_museum, Entrance.town_to_jojamart,
|
||||
Entrance.purchase_movie_ticket, LogicEntrance.buy_experience_books, LogicEntrance.attend_egg_festival, LogicEntrance.attend_fair,
|
||||
LogicEntrance.attend_spirit_eve, LogicEntrance.attend_winter_star]),
|
||||
RegionData(Region.beach,
|
||||
RegionData(RegionName.beach,
|
||||
[Entrance.beach_to_willy_fish_shop, Entrance.enter_elliott_house, Entrance.enter_tide_pools, LogicEntrance.fishing, LogicEntrance.attend_luau,
|
||||
LogicEntrance.attend_moonlight_jellies, LogicEntrance.attend_night_market, LogicEntrance.attend_squidfest]),
|
||||
RegionData(Region.railroad, [Entrance.enter_bathhouse_entrance, Entrance.enter_witch_warp_cave]),
|
||||
RegionData(Region.ranch),
|
||||
RegionData(Region.leah_house),
|
||||
RegionData(Region.mastery_cave),
|
||||
RegionData(Region.sewer, [Entrance.enter_mutant_bug_lair]),
|
||||
RegionData(Region.mutant_bug_lair),
|
||||
RegionData(Region.wizard_tower, [Entrance.enter_wizard_basement, Entrance.use_desert_obelisk, Entrance.use_island_obelisk]),
|
||||
RegionData(Region.wizard_basement),
|
||||
RegionData(Region.tent),
|
||||
RegionData(Region.carpenter, [Entrance.enter_sebastian_room]),
|
||||
RegionData(Region.sebastian_room),
|
||||
RegionData(Region.adventurer_guild, [Entrance.adventurer_guild_to_bedroom]),
|
||||
RegionData(Region.adventurer_guild_bedroom),
|
||||
RegionData(Region.community_center,
|
||||
RegionData(RegionName.railroad, [Entrance.enter_bathhouse_entrance, Entrance.enter_witch_warp_cave]),
|
||||
RegionData(RegionName.ranch),
|
||||
RegionData(RegionName.leah_house),
|
||||
RegionData(RegionName.mastery_cave),
|
||||
RegionData(RegionName.sewer, [Entrance.enter_mutant_bug_lair]),
|
||||
RegionData(RegionName.mutant_bug_lair),
|
||||
RegionData(RegionName.wizard_tower, [Entrance.enter_wizard_basement, Entrance.use_desert_obelisk, Entrance.use_island_obelisk]),
|
||||
RegionData(RegionName.wizard_basement),
|
||||
RegionData(RegionName.tent),
|
||||
RegionData(RegionName.carpenter, [Entrance.enter_sebastian_room]),
|
||||
RegionData(RegionName.sebastian_room),
|
||||
RegionData(RegionName.adventurer_guild, [Entrance.adventurer_guild_to_bedroom]),
|
||||
RegionData(RegionName.adventurer_guild_bedroom),
|
||||
RegionData(RegionName.community_center,
|
||||
[Entrance.access_crafts_room, Entrance.access_pantry, Entrance.access_fish_tank,
|
||||
Entrance.access_boiler_room, Entrance.access_bulletin_board, Entrance.access_vault]),
|
||||
RegionData(Region.crafts_room),
|
||||
RegionData(Region.pantry),
|
||||
RegionData(Region.fish_tank),
|
||||
RegionData(Region.boiler_room),
|
||||
RegionData(Region.bulletin_board),
|
||||
RegionData(Region.vault),
|
||||
RegionData(Region.hospital, [Entrance.enter_harvey_room]),
|
||||
RegionData(Region.harvey_room),
|
||||
RegionData(Region.pierre_store, [Entrance.enter_sunroom]),
|
||||
RegionData(Region.sunroom),
|
||||
RegionData(Region.saloon, [Entrance.play_journey_of_the_prairie_king, Entrance.play_junimo_kart]),
|
||||
RegionData(Region.jotpk_world_1, [Entrance.reach_jotpk_world_2]),
|
||||
RegionData(Region.jotpk_world_2, [Entrance.reach_jotpk_world_3]),
|
||||
RegionData(Region.jotpk_world_3),
|
||||
RegionData(Region.junimo_kart_1, [Entrance.reach_junimo_kart_2]),
|
||||
RegionData(Region.junimo_kart_2, [Entrance.reach_junimo_kart_3]),
|
||||
RegionData(Region.junimo_kart_3, [Entrance.reach_junimo_kart_4]),
|
||||
RegionData(Region.junimo_kart_4),
|
||||
RegionData(Region.alex_house),
|
||||
RegionData(Region.trailer),
|
||||
RegionData(Region.mayor_house),
|
||||
RegionData(Region.sam_house),
|
||||
RegionData(Region.haley_house),
|
||||
RegionData(Region.blacksmith, [LogicEntrance.blacksmith_copper]),
|
||||
RegionData(Region.museum),
|
||||
RegionData(Region.jojamart, [Entrance.enter_abandoned_jojamart]),
|
||||
RegionData(Region.abandoned_jojamart, [Entrance.enter_movie_theater]),
|
||||
RegionData(Region.movie_ticket_stand),
|
||||
RegionData(Region.movie_theater),
|
||||
RegionData(Region.fish_shop, [Entrance.fish_shop_to_boat_tunnel]),
|
||||
RegionData(Region.boat_tunnel, [Entrance.boat_to_ginger_island], is_ginger_island=True),
|
||||
RegionData(Region.elliott_house),
|
||||
RegionData(Region.tide_pools),
|
||||
RegionData(Region.bathhouse_entrance, [Entrance.enter_locker_room]),
|
||||
RegionData(Region.locker_room, [Entrance.enter_public_bath]),
|
||||
RegionData(Region.public_bath),
|
||||
RegionData(Region.witch_warp_cave, [Entrance.enter_witch_swamp]),
|
||||
RegionData(Region.witch_swamp, [Entrance.enter_witch_hut]),
|
||||
RegionData(Region.witch_hut, [Entrance.witch_warp_to_wizard_basement]),
|
||||
RegionData(Region.quarry, [Entrance.enter_quarry_mine_entrance]),
|
||||
RegionData(Region.quarry_mine_entrance, [Entrance.enter_quarry_mine]),
|
||||
RegionData(Region.quarry_mine),
|
||||
RegionData(Region.secret_woods),
|
||||
RegionData(Region.desert, [Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis, LogicEntrance.attend_desert_festival]),
|
||||
RegionData(Region.oasis, [Entrance.enter_casino]),
|
||||
RegionData(Region.casino),
|
||||
RegionData(Region.skull_cavern_entrance, [Entrance.enter_skull_cavern]),
|
||||
RegionData(Region.skull_cavern, [Entrance.mine_to_skull_cavern_floor_25]),
|
||||
RegionData(Region.skull_cavern_25, [Entrance.mine_to_skull_cavern_floor_50]),
|
||||
RegionData(Region.skull_cavern_50, [Entrance.mine_to_skull_cavern_floor_75]),
|
||||
RegionData(Region.skull_cavern_75, [Entrance.mine_to_skull_cavern_floor_100]),
|
||||
RegionData(Region.skull_cavern_100, [Entrance.mine_to_skull_cavern_floor_125]),
|
||||
RegionData(Region.skull_cavern_125, [Entrance.mine_to_skull_cavern_floor_150]),
|
||||
RegionData(Region.skull_cavern_150, [Entrance.mine_to_skull_cavern_floor_175]),
|
||||
RegionData(Region.skull_cavern_175, [Entrance.mine_to_skull_cavern_floor_200]),
|
||||
RegionData(Region.skull_cavern_200, [Entrance.enter_dangerous_skull_cavern]),
|
||||
RegionData(Region.dangerous_skull_cavern, is_ginger_island=True),
|
||||
RegionData(Region.island_south,
|
||||
RegionData(RegionName.crafts_room),
|
||||
RegionData(RegionName.pantry),
|
||||
RegionData(RegionName.fish_tank),
|
||||
RegionData(RegionName.boiler_room),
|
||||
RegionData(RegionName.bulletin_board),
|
||||
RegionData(RegionName.vault),
|
||||
RegionData(RegionName.hospital, [Entrance.enter_harvey_room]),
|
||||
RegionData(RegionName.harvey_room),
|
||||
RegionData(RegionName.pierre_store, [Entrance.enter_sunroom]),
|
||||
RegionData(RegionName.sunroom),
|
||||
RegionData(RegionName.saloon, [Entrance.play_journey_of_the_prairie_king, Entrance.play_junimo_kart]),
|
||||
RegionData(RegionName.jotpk_world_1, [Entrance.reach_jotpk_world_2]),
|
||||
RegionData(RegionName.jotpk_world_2, [Entrance.reach_jotpk_world_3]),
|
||||
RegionData(RegionName.jotpk_world_3),
|
||||
RegionData(RegionName.junimo_kart_1, [Entrance.reach_junimo_kart_2]),
|
||||
RegionData(RegionName.junimo_kart_2, [Entrance.reach_junimo_kart_3]),
|
||||
RegionData(RegionName.junimo_kart_3, [Entrance.reach_junimo_kart_4]),
|
||||
RegionData(RegionName.junimo_kart_4),
|
||||
RegionData(RegionName.alex_house),
|
||||
RegionData(RegionName.trailer),
|
||||
RegionData(RegionName.mayor_house),
|
||||
RegionData(RegionName.sam_house),
|
||||
RegionData(RegionName.haley_house),
|
||||
RegionData(RegionName.blacksmith, [LogicEntrance.blacksmith_copper]),
|
||||
RegionData(RegionName.museum),
|
||||
RegionData(RegionName.jojamart, [Entrance.enter_abandoned_jojamart]),
|
||||
RegionData(RegionName.abandoned_jojamart, [Entrance.enter_movie_theater]),
|
||||
RegionData(RegionName.movie_ticket_stand),
|
||||
RegionData(RegionName.movie_theater),
|
||||
RegionData(RegionName.fish_shop, [Entrance.fish_shop_to_boat_tunnel]),
|
||||
RegionData(RegionName.boat_tunnel, [Entrance.boat_to_ginger_island], is_ginger_island=True),
|
||||
RegionData(RegionName.elliott_house),
|
||||
RegionData(RegionName.tide_pools),
|
||||
RegionData(RegionName.bathhouse_entrance, [Entrance.enter_locker_room]),
|
||||
RegionData(RegionName.locker_room, [Entrance.enter_public_bath]),
|
||||
RegionData(RegionName.public_bath),
|
||||
RegionData(RegionName.witch_warp_cave, [Entrance.enter_witch_swamp]),
|
||||
RegionData(RegionName.witch_swamp, [Entrance.enter_witch_hut]),
|
||||
RegionData(RegionName.witch_hut, [Entrance.witch_warp_to_wizard_basement]),
|
||||
RegionData(RegionName.quarry, [Entrance.enter_quarry_mine_entrance]),
|
||||
RegionData(RegionName.quarry_mine_entrance, [Entrance.enter_quarry_mine]),
|
||||
RegionData(RegionName.quarry_mine),
|
||||
RegionData(RegionName.secret_woods),
|
||||
RegionData(RegionName.desert, [Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis, LogicEntrance.attend_desert_festival]),
|
||||
RegionData(RegionName.oasis, [Entrance.enter_casino]),
|
||||
RegionData(RegionName.casino),
|
||||
RegionData(RegionName.skull_cavern_entrance, [Entrance.enter_skull_cavern]),
|
||||
RegionData(RegionName.skull_cavern, [Entrance.mine_to_skull_cavern_floor_25]),
|
||||
RegionData(RegionName.skull_cavern_25, [Entrance.mine_to_skull_cavern_floor_50]),
|
||||
RegionData(RegionName.skull_cavern_50, [Entrance.mine_to_skull_cavern_floor_75]),
|
||||
RegionData(RegionName.skull_cavern_75, [Entrance.mine_to_skull_cavern_floor_100]),
|
||||
RegionData(RegionName.skull_cavern_100, [Entrance.mine_to_skull_cavern_floor_125]),
|
||||
RegionData(RegionName.skull_cavern_125, [Entrance.mine_to_skull_cavern_floor_150]),
|
||||
RegionData(RegionName.skull_cavern_150, [Entrance.mine_to_skull_cavern_floor_175]),
|
||||
RegionData(RegionName.skull_cavern_175, [Entrance.mine_to_skull_cavern_floor_200]),
|
||||
RegionData(RegionName.skull_cavern_200, [Entrance.enter_dangerous_skull_cavern]),
|
||||
RegionData(RegionName.dangerous_skull_cavern, is_ginger_island=True),
|
||||
RegionData(RegionName.island_south,
|
||||
[Entrance.island_south_to_west, Entrance.island_south_to_north, Entrance.island_south_to_east, Entrance.island_south_to_southeast,
|
||||
Entrance.use_island_resort, Entrance.parrot_express_docks_to_volcano, Entrance.parrot_express_docks_to_dig_site,
|
||||
Entrance.parrot_express_docks_to_jungle],
|
||||
is_ginger_island=True),
|
||||
RegionData(Region.island_resort, is_ginger_island=True),
|
||||
RegionData(Region.island_west,
|
||||
RegionData(RegionName.island_resort, is_ginger_island=True),
|
||||
RegionData(RegionName.island_west,
|
||||
[Entrance.island_west_to_islandfarmhouse, Entrance.island_west_to_gourmand_cave, Entrance.island_west_to_crystals_cave,
|
||||
Entrance.island_west_to_shipwreck, Entrance.island_west_to_qi_walnut_room, Entrance.use_farm_obelisk, Entrance.parrot_express_jungle_to_docks,
|
||||
Entrance.parrot_express_jungle_to_dig_site, Entrance.parrot_express_jungle_to_volcano, LogicEntrance.grow_spring_crops_on_island,
|
||||
LogicEntrance.grow_summer_crops_on_island, LogicEntrance.grow_fall_crops_on_island, LogicEntrance.grow_winter_crops_on_island,
|
||||
LogicEntrance.grow_indoor_crops_on_island],
|
||||
is_ginger_island=True),
|
||||
RegionData(Region.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine], is_ginger_island=True),
|
||||
RegionData(Region.island_shrine, is_ginger_island=True),
|
||||
RegionData(Region.island_south_east, [Entrance.island_southeast_to_pirate_cove], is_ginger_island=True),
|
||||
RegionData(Region.island_north,
|
||||
RegionData(RegionName.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine], is_ginger_island=True),
|
||||
RegionData(RegionName.island_shrine, is_ginger_island=True),
|
||||
RegionData(RegionName.island_south_east, [Entrance.island_southeast_to_pirate_cove], is_ginger_island=True),
|
||||
RegionData(RegionName.island_north,
|
||||
[Entrance.talk_to_island_trader, Entrance.island_north_to_field_office, Entrance.island_north_to_dig_site, Entrance.island_north_to_volcano,
|
||||
Entrance.parrot_express_volcano_to_dig_site, Entrance.parrot_express_volcano_to_jungle, Entrance.parrot_express_volcano_to_docks],
|
||||
is_ginger_island=True),
|
||||
RegionData(Region.volcano, [Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach], is_ginger_island=True),
|
||||
RegionData(Region.volcano_secret_beach, is_ginger_island=True),
|
||||
RegionData(Region.volcano_floor_5, [Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10], is_ginger_island=True),
|
||||
RegionData(Region.volcano_dwarf_shop, is_ginger_island=True),
|
||||
RegionData(Region.volcano_floor_10, is_ginger_island=True),
|
||||
RegionData(Region.island_trader, is_ginger_island=True),
|
||||
RegionData(Region.island_farmhouse, [LogicEntrance.island_cooking], is_ginger_island=True),
|
||||
RegionData(Region.gourmand_frog_cave, is_ginger_island=True),
|
||||
RegionData(Region.colored_crystals_cave, is_ginger_island=True),
|
||||
RegionData(Region.shipwreck, is_ginger_island=True),
|
||||
RegionData(Region.qi_walnut_room, is_ginger_island=True),
|
||||
RegionData(Region.leo_hut, is_ginger_island=True),
|
||||
RegionData(Region.pirate_cove, is_ginger_island=True),
|
||||
RegionData(Region.field_office, is_ginger_island=True),
|
||||
RegionData(Region.dig_site,
|
||||
RegionData(RegionName.volcano, [Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach], is_ginger_island=True),
|
||||
RegionData(RegionName.volcano_secret_beach, is_ginger_island=True),
|
||||
RegionData(RegionName.volcano_floor_5, [Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10], is_ginger_island=True),
|
||||
RegionData(RegionName.volcano_dwarf_shop, is_ginger_island=True),
|
||||
RegionData(RegionName.volcano_floor_10, is_ginger_island=True),
|
||||
RegionData(RegionName.island_trader, is_ginger_island=True),
|
||||
RegionData(RegionName.island_farmhouse, [LogicEntrance.island_cooking], is_ginger_island=True),
|
||||
RegionData(RegionName.gourmand_frog_cave, is_ginger_island=True),
|
||||
RegionData(RegionName.colored_crystals_cave, is_ginger_island=True),
|
||||
RegionData(RegionName.shipwreck, is_ginger_island=True),
|
||||
RegionData(RegionName.qi_walnut_room, is_ginger_island=True),
|
||||
RegionData(RegionName.leo_hut, is_ginger_island=True),
|
||||
RegionData(RegionName.pirate_cove, is_ginger_island=True),
|
||||
RegionData(RegionName.field_office, is_ginger_island=True),
|
||||
RegionData(RegionName.dig_site,
|
||||
[Entrance.dig_site_to_professor_snail_cave, Entrance.parrot_express_dig_site_to_volcano,
|
||||
Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_dig_site_to_jungle],
|
||||
is_ginger_island=True),
|
||||
RegionData(Region.professor_snail_cave, is_ginger_island=True),
|
||||
RegionData(Region.coop),
|
||||
RegionData(Region.barn),
|
||||
RegionData(Region.shed),
|
||||
RegionData(Region.slime_hutch),
|
||||
RegionData(RegionName.professor_snail_cave, is_ginger_island=True),
|
||||
RegionData(RegionName.coop),
|
||||
RegionData(RegionName.barn),
|
||||
RegionData(RegionName.shed),
|
||||
RegionData(RegionName.slime_hutch),
|
||||
|
||||
RegionData(Region.mines, [LogicEntrance.talk_to_mines_dwarf,
|
||||
Entrance.dig_to_mines_floor_5]),
|
||||
RegionData(Region.mines_floor_5, [Entrance.dig_to_mines_floor_10]),
|
||||
RegionData(Region.mines_floor_10, [Entrance.dig_to_mines_floor_15]),
|
||||
RegionData(Region.mines_floor_15, [Entrance.dig_to_mines_floor_20]),
|
||||
RegionData(Region.mines_floor_20, [Entrance.dig_to_mines_floor_25]),
|
||||
RegionData(Region.mines_floor_25, [Entrance.dig_to_mines_floor_30]),
|
||||
RegionData(Region.mines_floor_30, [Entrance.dig_to_mines_floor_35]),
|
||||
RegionData(Region.mines_floor_35, [Entrance.dig_to_mines_floor_40]),
|
||||
RegionData(Region.mines_floor_40, [Entrance.dig_to_mines_floor_45]),
|
||||
RegionData(Region.mines_floor_45, [Entrance.dig_to_mines_floor_50]),
|
||||
RegionData(Region.mines_floor_50, [Entrance.dig_to_mines_floor_55]),
|
||||
RegionData(Region.mines_floor_55, [Entrance.dig_to_mines_floor_60]),
|
||||
RegionData(Region.mines_floor_60, [Entrance.dig_to_mines_floor_65]),
|
||||
RegionData(Region.mines_floor_65, [Entrance.dig_to_mines_floor_70]),
|
||||
RegionData(Region.mines_floor_70, [Entrance.dig_to_mines_floor_75]),
|
||||
RegionData(Region.mines_floor_75, [Entrance.dig_to_mines_floor_80]),
|
||||
RegionData(Region.mines_floor_80, [Entrance.dig_to_mines_floor_85]),
|
||||
RegionData(Region.mines_floor_85, [Entrance.dig_to_mines_floor_90]),
|
||||
RegionData(Region.mines_floor_90, [Entrance.dig_to_mines_floor_95]),
|
||||
RegionData(Region.mines_floor_95, [Entrance.dig_to_mines_floor_100]),
|
||||
RegionData(Region.mines_floor_100, [Entrance.dig_to_mines_floor_105]),
|
||||
RegionData(Region.mines_floor_105, [Entrance.dig_to_mines_floor_110]),
|
||||
RegionData(Region.mines_floor_110, [Entrance.dig_to_mines_floor_115]),
|
||||
RegionData(Region.mines_floor_115, [Entrance.dig_to_mines_floor_120]),
|
||||
RegionData(Region.mines_floor_120, [Entrance.dig_to_dangerous_mines_20, Entrance.dig_to_dangerous_mines_60, Entrance.dig_to_dangerous_mines_100]),
|
||||
RegionData(Region.dangerous_mines_20, is_ginger_island=True),
|
||||
RegionData(Region.dangerous_mines_60, is_ginger_island=True),
|
||||
RegionData(Region.dangerous_mines_100, is_ginger_island=True),
|
||||
RegionData(RegionName.mines, [LogicEntrance.talk_to_mines_dwarf,
|
||||
Entrance.dig_to_mines_floor_5]),
|
||||
RegionData(RegionName.mines_floor_5, [Entrance.dig_to_mines_floor_10]),
|
||||
RegionData(RegionName.mines_floor_10, [Entrance.dig_to_mines_floor_15]),
|
||||
RegionData(RegionName.mines_floor_15, [Entrance.dig_to_mines_floor_20]),
|
||||
RegionData(RegionName.mines_floor_20, [Entrance.dig_to_mines_floor_25]),
|
||||
RegionData(RegionName.mines_floor_25, [Entrance.dig_to_mines_floor_30]),
|
||||
RegionData(RegionName.mines_floor_30, [Entrance.dig_to_mines_floor_35]),
|
||||
RegionData(RegionName.mines_floor_35, [Entrance.dig_to_mines_floor_40]),
|
||||
RegionData(RegionName.mines_floor_40, [Entrance.dig_to_mines_floor_45]),
|
||||
RegionData(RegionName.mines_floor_45, [Entrance.dig_to_mines_floor_50]),
|
||||
RegionData(RegionName.mines_floor_50, [Entrance.dig_to_mines_floor_55]),
|
||||
RegionData(RegionName.mines_floor_55, [Entrance.dig_to_mines_floor_60]),
|
||||
RegionData(RegionName.mines_floor_60, [Entrance.dig_to_mines_floor_65]),
|
||||
RegionData(RegionName.mines_floor_65, [Entrance.dig_to_mines_floor_70]),
|
||||
RegionData(RegionName.mines_floor_70, [Entrance.dig_to_mines_floor_75]),
|
||||
RegionData(RegionName.mines_floor_75, [Entrance.dig_to_mines_floor_80]),
|
||||
RegionData(RegionName.mines_floor_80, [Entrance.dig_to_mines_floor_85]),
|
||||
RegionData(RegionName.mines_floor_85, [Entrance.dig_to_mines_floor_90]),
|
||||
RegionData(RegionName.mines_floor_90, [Entrance.dig_to_mines_floor_95]),
|
||||
RegionData(RegionName.mines_floor_95, [Entrance.dig_to_mines_floor_100]),
|
||||
RegionData(RegionName.mines_floor_100, [Entrance.dig_to_mines_floor_105]),
|
||||
RegionData(RegionName.mines_floor_105, [Entrance.dig_to_mines_floor_110]),
|
||||
RegionData(RegionName.mines_floor_110, [Entrance.dig_to_mines_floor_115]),
|
||||
RegionData(RegionName.mines_floor_115, [Entrance.dig_to_mines_floor_120]),
|
||||
RegionData(RegionName.mines_floor_120, [Entrance.dig_to_dangerous_mines_20, Entrance.dig_to_dangerous_mines_60, Entrance.dig_to_dangerous_mines_100]),
|
||||
RegionData(RegionName.dangerous_mines_20, is_ginger_island=True),
|
||||
RegionData(RegionName.dangerous_mines_60, is_ginger_island=True),
|
||||
RegionData(RegionName.dangerous_mines_100, is_ginger_island=True),
|
||||
|
||||
RegionData(LogicRegion.mines_dwarf_shop),
|
||||
RegionData(LogicRegion.blacksmith_copper, [LogicEntrance.blacksmith_iron]),
|
||||
@@ -256,206 +256,207 @@ vanilla_regions = [
|
||||
|
||||
# Exists and where they lead
|
||||
vanilla_connections = [
|
||||
ConnectionData(Entrance.to_stardew_valley, Region.stardew_valley),
|
||||
ConnectionData(Entrance.to_farmhouse, Region.farm_house),
|
||||
ConnectionData(Entrance.farmhouse_to_farm, Region.farm),
|
||||
ConnectionData(Entrance.downstairs_to_cellar, Region.cellar),
|
||||
ConnectionData(Entrance.farm_to_backwoods, Region.backwoods),
|
||||
ConnectionData(Entrance.farm_to_bus_stop, Region.bus_stop),
|
||||
ConnectionData(Entrance.farm_to_forest, Region.forest),
|
||||
ConnectionData(Entrance.farm_to_farmcave, Region.farm_cave, flag=RandomizationFlag.NON_PROGRESSION),
|
||||
ConnectionData(Entrance.enter_greenhouse, Region.greenhouse),
|
||||
ConnectionData(Entrance.enter_coop, Region.coop),
|
||||
ConnectionData(Entrance.enter_barn, Region.barn),
|
||||
ConnectionData(Entrance.enter_shed, Region.shed),
|
||||
ConnectionData(Entrance.enter_slime_hutch, Region.slime_hutch),
|
||||
ConnectionData(Entrance.use_desert_obelisk, Region.desert),
|
||||
ConnectionData(Entrance.use_island_obelisk, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.use_farm_obelisk, Region.farm),
|
||||
ConnectionData(Entrance.backwoods_to_mountain, Region.mountain),
|
||||
ConnectionData(Entrance.bus_stop_to_town, Region.town),
|
||||
ConnectionData(Entrance.bus_stop_to_tunnel_entrance, Region.tunnel_entrance),
|
||||
ConnectionData(Entrance.tunnel_entrance_to_bus_tunnel, Region.bus_tunnel, flag=RandomizationFlag.NON_PROGRESSION),
|
||||
ConnectionData(Entrance.take_bus_to_desert, Region.desert),
|
||||
ConnectionData(Entrance.forest_to_town, Region.town),
|
||||
ConnectionData(Entrance.forest_to_wizard_tower, Region.wizard_tower,
|
||||
ConnectionData(Entrance.to_stardew_valley, RegionName.stardew_valley),
|
||||
ConnectionData(Entrance.to_farmhouse, RegionName.farm_house),
|
||||
ConnectionData(Entrance.farmhouse_to_farm, RegionName.farm),
|
||||
ConnectionData(Entrance.downstairs_to_cellar, RegionName.cellar),
|
||||
ConnectionData(Entrance.farm_to_backwoods, RegionName.backwoods),
|
||||
ConnectionData(Entrance.farm_to_bus_stop, RegionName.bus_stop),
|
||||
ConnectionData(Entrance.farm_to_forest, RegionName.forest),
|
||||
ConnectionData(Entrance.farm_to_farmcave, RegionName.farm_cave, flag=RandomizationFlag.NON_PROGRESSION),
|
||||
ConnectionData(Entrance.enter_greenhouse, RegionName.greenhouse),
|
||||
ConnectionData(Entrance.enter_coop, RegionName.coop),
|
||||
ConnectionData(Entrance.enter_barn, RegionName.barn),
|
||||
ConnectionData(Entrance.enter_shed, RegionName.shed),
|
||||
ConnectionData(Entrance.enter_slime_hutch, RegionName.slime_hutch),
|
||||
ConnectionData(Entrance.use_desert_obelisk, RegionName.desert),
|
||||
ConnectionData(Entrance.use_island_obelisk, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.use_farm_obelisk, RegionName.farm),
|
||||
ConnectionData(Entrance.backwoods_to_mountain, RegionName.mountain),
|
||||
ConnectionData(Entrance.bus_stop_to_town, RegionName.town),
|
||||
ConnectionData(Entrance.bus_stop_to_tunnel_entrance, RegionName.tunnel_entrance),
|
||||
ConnectionData(Entrance.tunnel_entrance_to_bus_tunnel, RegionName.bus_tunnel, flag=RandomizationFlag.NON_PROGRESSION),
|
||||
ConnectionData(Entrance.take_bus_to_desert, RegionName.desert),
|
||||
ConnectionData(Entrance.forest_to_town, RegionName.town),
|
||||
ConnectionData(Entrance.forest_to_wizard_tower, RegionName.wizard_tower,
|
||||
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.enter_wizard_basement, Region.wizard_basement, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.forest_to_marnie_ranch, Region.ranch,
|
||||
ConnectionData(Entrance.enter_wizard_basement, RegionName.wizard_basement, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.forest_to_marnie_ranch, RegionName.ranch,
|
||||
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.forest_to_leah_cottage, Region.leah_house,
|
||||
ConnectionData(Entrance.forest_to_leah_cottage, RegionName.leah_house,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.enter_secret_woods, Region.secret_woods),
|
||||
ConnectionData(Entrance.forest_to_sewer, Region.sewer, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.forest_to_mastery_cave, Region.mastery_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.MASTERIES),
|
||||
ConnectionData(Entrance.town_to_sewer, Region.sewer, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.enter_mutant_bug_lair, Region.mutant_bug_lair, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.mountain_to_railroad, Region.railroad),
|
||||
ConnectionData(Entrance.mountain_to_tent, Region.tent,
|
||||
ConnectionData(Entrance.enter_secret_woods, RegionName.secret_woods),
|
||||
ConnectionData(Entrance.forest_to_sewer, RegionName.sewer, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.forest_to_mastery_cave, RegionName.mastery_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.MASTERIES),
|
||||
ConnectionData(Entrance.town_to_sewer, RegionName.sewer, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.enter_mutant_bug_lair, RegionName.mutant_bug_lair, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.mountain_to_railroad, RegionName.railroad),
|
||||
ConnectionData(Entrance.mountain_to_tent, RegionName.tent,
|
||||
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.mountain_to_leo_treehouse, Region.leo_treehouse,
|
||||
ConnectionData(Entrance.mountain_to_leo_treehouse, RegionName.leo_treehouse,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.mountain_to_carpenter_shop, Region.carpenter,
|
||||
ConnectionData(Entrance.mountain_to_carpenter_shop, RegionName.carpenter,
|
||||
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.mountain_to_maru_room, Region.maru_room,
|
||||
ConnectionData(Entrance.mountain_to_maru_room, RegionName.maru_room,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.enter_sebastian_room, Region.sebastian_room, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild,
|
||||
ConnectionData(Entrance.enter_sebastian_room, RegionName.sebastian_room, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.mountain_to_adventurer_guild, RegionName.adventurer_guild,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.adventurer_guild_to_bedroom, Region.adventurer_guild_bedroom),
|
||||
ConnectionData(Entrance.enter_quarry, Region.quarry),
|
||||
ConnectionData(Entrance.enter_quarry_mine_entrance, Region.quarry_mine_entrance,
|
||||
ConnectionData(Entrance.adventurer_guild_to_bedroom, RegionName.adventurer_guild_bedroom),
|
||||
ConnectionData(Entrance.enter_quarry, RegionName.quarry),
|
||||
ConnectionData(Entrance.enter_quarry_mine_entrance, RegionName.quarry_mine_entrance,
|
||||
flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.enter_quarry_mine, Region.quarry_mine),
|
||||
ConnectionData(Entrance.mountain_to_town, Region.town),
|
||||
ConnectionData(Entrance.town_to_community_center, Region.community_center,
|
||||
ConnectionData(Entrance.enter_quarry_mine, RegionName.quarry_mine),
|
||||
ConnectionData(Entrance.mountain_to_town, RegionName.town),
|
||||
ConnectionData(Entrance.town_to_community_center, RegionName.community_center,
|
||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.access_crafts_room, Region.crafts_room),
|
||||
ConnectionData(Entrance.access_pantry, Region.pantry),
|
||||
ConnectionData(Entrance.access_fish_tank, Region.fish_tank),
|
||||
ConnectionData(Entrance.access_boiler_room, Region.boiler_room),
|
||||
ConnectionData(Entrance.access_bulletin_board, Region.bulletin_board),
|
||||
ConnectionData(Entrance.access_vault, Region.vault),
|
||||
ConnectionData(Entrance.town_to_hospital, Region.hospital,
|
||||
ConnectionData(Entrance.access_crafts_room, RegionName.crafts_room),
|
||||
ConnectionData(Entrance.access_pantry, RegionName.pantry),
|
||||
ConnectionData(Entrance.access_fish_tank, RegionName.fish_tank),
|
||||
ConnectionData(Entrance.access_boiler_room, RegionName.boiler_room),
|
||||
ConnectionData(Entrance.access_bulletin_board, RegionName.bulletin_board),
|
||||
ConnectionData(Entrance.access_vault, RegionName.vault),
|
||||
ConnectionData(Entrance.town_to_hospital, RegionName.hospital,
|
||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.enter_harvey_room, Region.harvey_room, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.town_to_pierre_general_store, Region.pierre_store,
|
||||
ConnectionData(Entrance.enter_harvey_room, RegionName.harvey_room, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.town_to_pierre_general_store, RegionName.pierre_store,
|
||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.enter_sunroom, Region.sunroom, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.town_to_clint_blacksmith, Region.blacksmith,
|
||||
ConnectionData(Entrance.enter_sunroom, RegionName.sunroom, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.town_to_clint_blacksmith, RegionName.blacksmith,
|
||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.town_to_saloon, Region.saloon,
|
||||
ConnectionData(Entrance.town_to_saloon, RegionName.saloon,
|
||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.play_journey_of_the_prairie_king, Region.jotpk_world_1),
|
||||
ConnectionData(Entrance.reach_jotpk_world_2, Region.jotpk_world_2),
|
||||
ConnectionData(Entrance.reach_jotpk_world_3, Region.jotpk_world_3),
|
||||
ConnectionData(Entrance.play_junimo_kart, Region.junimo_kart_1),
|
||||
ConnectionData(Entrance.reach_junimo_kart_2, Region.junimo_kart_2),
|
||||
ConnectionData(Entrance.reach_junimo_kart_3, Region.junimo_kart_3),
|
||||
ConnectionData(Entrance.reach_junimo_kart_4, Region.junimo_kart_4),
|
||||
ConnectionData(Entrance.town_to_sam_house, Region.sam_house,
|
||||
ConnectionData(Entrance.play_journey_of_the_prairie_king, RegionName.jotpk_world_1),
|
||||
ConnectionData(Entrance.reach_jotpk_world_2, RegionName.jotpk_world_2),
|
||||
ConnectionData(Entrance.reach_jotpk_world_3, RegionName.jotpk_world_3),
|
||||
ConnectionData(Entrance.play_junimo_kart, RegionName.junimo_kart_1),
|
||||
ConnectionData(Entrance.reach_junimo_kart_2, RegionName.junimo_kart_2),
|
||||
ConnectionData(Entrance.reach_junimo_kart_3, RegionName.junimo_kart_3),
|
||||
ConnectionData(Entrance.reach_junimo_kart_4, RegionName.junimo_kart_4),
|
||||
ConnectionData(Entrance.town_to_sam_house, RegionName.sam_house,
|
||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.town_to_haley_house, Region.haley_house,
|
||||
ConnectionData(Entrance.town_to_haley_house, RegionName.haley_house,
|
||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.town_to_mayor_manor, Region.mayor_house,
|
||||
ConnectionData(Entrance.town_to_mayor_manor, RegionName.mayor_house,
|
||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.town_to_alex_house, Region.alex_house,
|
||||
ConnectionData(Entrance.town_to_alex_house, RegionName.alex_house,
|
||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.town_to_trailer, Region.trailer,
|
||||
ConnectionData(Entrance.town_to_trailer, RegionName.trailer,
|
||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.town_to_museum, Region.museum,
|
||||
ConnectionData(Entrance.town_to_museum, RegionName.museum,
|
||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.town_to_jojamart, Region.jojamart,
|
||||
ConnectionData(Entrance.town_to_jojamart, RegionName.jojamart,
|
||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.purchase_movie_ticket, Region.movie_ticket_stand),
|
||||
ConnectionData(Entrance.enter_abandoned_jojamart, Region.abandoned_jojamart),
|
||||
ConnectionData(Entrance.enter_movie_theater, Region.movie_theater),
|
||||
ConnectionData(Entrance.town_to_beach, Region.beach),
|
||||
ConnectionData(Entrance.enter_elliott_house, Region.elliott_house,
|
||||
ConnectionData(Entrance.purchase_movie_ticket, RegionName.movie_ticket_stand),
|
||||
ConnectionData(Entrance.enter_abandoned_jojamart, RegionName.abandoned_jojamart),
|
||||
ConnectionData(Entrance.enter_movie_theater, RegionName.movie_theater),
|
||||
ConnectionData(Entrance.town_to_beach, RegionName.beach),
|
||||
ConnectionData(Entrance.enter_elliott_house, RegionName.elliott_house,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.beach_to_willy_fish_shop, Region.fish_shop,
|
||||
ConnectionData(Entrance.beach_to_willy_fish_shop, RegionName.fish_shop,
|
||||
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.fish_shop_to_boat_tunnel, Region.boat_tunnel,
|
||||
ConnectionData(Entrance.fish_shop_to_boat_tunnel, RegionName.boat_tunnel,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.boat_to_ginger_island, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.enter_tide_pools, Region.tide_pools),
|
||||
ConnectionData(Entrance.mountain_to_the_mines, Region.mines,
|
||||
ConnectionData(Entrance.boat_to_ginger_island, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.enter_tide_pools, RegionName.tide_pools),
|
||||
ConnectionData(Entrance.mountain_to_the_mines, RegionName.mines,
|
||||
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_5, Region.mines_floor_5),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_10, Region.mines_floor_10),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_15, Region.mines_floor_15),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_20, Region.mines_floor_20),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_25, Region.mines_floor_25),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_30, Region.mines_floor_30),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_35, Region.mines_floor_35),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_40, Region.mines_floor_40),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_45, Region.mines_floor_45),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_50, Region.mines_floor_50),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_55, Region.mines_floor_55),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_60, Region.mines_floor_60),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_65, Region.mines_floor_65),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_70, Region.mines_floor_70),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_75, Region.mines_floor_75),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_80, Region.mines_floor_80),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_85, Region.mines_floor_85),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_90, Region.mines_floor_90),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_95, Region.mines_floor_95),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_100, Region.mines_floor_100),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_105, Region.mines_floor_105),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_110, Region.mines_floor_110),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_115, Region.mines_floor_115),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_120, Region.mines_floor_120),
|
||||
ConnectionData(Entrance.dig_to_dangerous_mines_20, Region.dangerous_mines_20, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.dig_to_dangerous_mines_60, Region.dangerous_mines_60, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.dig_to_dangerous_mines_100, Region.dangerous_mines_100, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.enter_skull_cavern_entrance, Region.skull_cavern_entrance,
|
||||
ConnectionData(Entrance.dig_to_mines_floor_5, RegionName.mines_floor_5),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_10, RegionName.mines_floor_10),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_15, RegionName.mines_floor_15),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_20, RegionName.mines_floor_20),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_25, RegionName.mines_floor_25),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_30, RegionName.mines_floor_30),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_35, RegionName.mines_floor_35),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_40, RegionName.mines_floor_40),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_45, RegionName.mines_floor_45),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_50, RegionName.mines_floor_50),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_55, RegionName.mines_floor_55),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_60, RegionName.mines_floor_60),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_65, RegionName.mines_floor_65),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_70, RegionName.mines_floor_70),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_75, RegionName.mines_floor_75),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_80, RegionName.mines_floor_80),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_85, RegionName.mines_floor_85),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_90, RegionName.mines_floor_90),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_95, RegionName.mines_floor_95),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_100, RegionName.mines_floor_100),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_105, RegionName.mines_floor_105),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_110, RegionName.mines_floor_110),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_115, RegionName.mines_floor_115),
|
||||
ConnectionData(Entrance.dig_to_mines_floor_120, RegionName.mines_floor_120),
|
||||
ConnectionData(Entrance.dig_to_dangerous_mines_20, RegionName.dangerous_mines_20, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.dig_to_dangerous_mines_60, RegionName.dangerous_mines_60, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.dig_to_dangerous_mines_100, RegionName.dangerous_mines_100, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.enter_skull_cavern_entrance, RegionName.skull_cavern_entrance,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.enter_oasis, Region.oasis,
|
||||
ConnectionData(Entrance.enter_oasis, RegionName.oasis,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.enter_casino, Region.casino, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.enter_skull_cavern, Region.skull_cavern),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_25, Region.skull_cavern_25),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_50, Region.skull_cavern_50),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_75, Region.skull_cavern_75),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_100, Region.skull_cavern_100),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_125, Region.skull_cavern_125),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_150, Region.skull_cavern_150),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_175, Region.skull_cavern_175),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_200, Region.skull_cavern_200),
|
||||
ConnectionData(Entrance.enter_dangerous_skull_cavern, Region.dangerous_skull_cavern, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.enter_witch_warp_cave, Region.witch_warp_cave, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.enter_witch_swamp, Region.witch_swamp, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.enter_witch_hut, Region.witch_hut, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.witch_warp_to_wizard_basement, Region.wizard_basement, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.enter_bathhouse_entrance, Region.bathhouse_entrance,
|
||||
ConnectionData(Entrance.enter_casino, RegionName.casino, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.enter_skull_cavern, RegionName.skull_cavern),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_25, RegionName.skull_cavern_25),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_50, RegionName.skull_cavern_50),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_75, RegionName.skull_cavern_75),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_100, RegionName.skull_cavern_100),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_125, RegionName.skull_cavern_125),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_150, RegionName.skull_cavern_150),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_175, RegionName.skull_cavern_175),
|
||||
ConnectionData(Entrance.mine_to_skull_cavern_floor_200, RegionName.skull_cavern_200),
|
||||
ConnectionData(Entrance.enter_dangerous_skull_cavern, RegionName.dangerous_skull_cavern, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.enter_witch_warp_cave, RegionName.witch_warp_cave, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.enter_witch_swamp, RegionName.witch_swamp, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.enter_witch_hut, RegionName.witch_hut, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.witch_warp_to_wizard_basement, RegionName.wizard_basement, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.enter_bathhouse_entrance, RegionName.bathhouse_entrance,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.enter_locker_room, Region.locker_room, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.enter_public_bath, Region.public_bath, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.island_south_to_west, Region.island_west, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_south_to_north, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_south_to_east, Region.island_east, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_south_to_southeast, Region.island_south_east,
|
||||
ConnectionData(Entrance.enter_locker_room, RegionName.locker_room, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.enter_public_bath, RegionName.public_bath, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(Entrance.island_south_to_west, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_south_to_north, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_south_to_east, RegionName.island_east, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_south_to_southeast, RegionName.island_south_east,
|
||||
flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.use_island_resort, Region.island_resort, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_west_to_islandfarmhouse, Region.island_farmhouse,
|
||||
ConnectionData(Entrance.use_island_resort, RegionName.island_resort, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_west_to_islandfarmhouse, RegionName.island_farmhouse,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_west_to_gourmand_cave, Region.gourmand_frog_cave,
|
||||
ConnectionData(Entrance.island_west_to_gourmand_cave, RegionName.gourmand_frog_cave,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_west_to_crystals_cave, Region.colored_crystals_cave,
|
||||
ConnectionData(Entrance.island_west_to_crystals_cave, RegionName.colored_crystals_cave,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_west_to_shipwreck, Region.shipwreck,
|
||||
ConnectionData(Entrance.island_west_to_shipwreck, RegionName.shipwreck,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_west_to_qi_walnut_room, Region.qi_walnut_room, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_east_to_leo_hut, Region.leo_hut,
|
||||
ConnectionData(Entrance.island_west_to_qi_walnut_room, RegionName.qi_walnut_room, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_east_to_leo_hut, RegionName.leo_hut,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_east_to_island_shrine, Region.island_shrine,
|
||||
ConnectionData(Entrance.island_east_to_island_shrine, RegionName.island_shrine,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_southeast_to_pirate_cove, Region.pirate_cove,
|
||||
ConnectionData(Entrance.island_southeast_to_pirate_cove, RegionName.pirate_cove,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_north_to_field_office, Region.field_office,
|
||||
ConnectionData(Entrance.island_north_to_field_office, RegionName.field_office,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_north_to_dig_site, Region.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.dig_site_to_professor_snail_cave, Region.professor_snail_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.island_north_to_volcano, Region.volcano,
|
||||
ConnectionData(Entrance.island_north_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.dig_site_to_professor_snail_cave, RegionName.professor_snail_cave,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.volcano_to_secret_beach, Region.volcano_secret_beach,
|
||||
ConnectionData(Entrance.island_north_to_volcano, RegionName.volcano,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.talk_to_island_trader, Region.island_trader, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.climb_to_volcano_5, Region.volcano_floor_5, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.talk_to_volcano_dwarf, Region.volcano_dwarf_shop, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.climb_to_volcano_10, Region.volcano_floor_10, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_jungle_to_docks, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_dig_site_to_docks, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_volcano_to_docks, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_volcano_to_jungle, Region.island_west, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_docks_to_jungle, Region.island_west, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_dig_site_to_jungle, Region.island_west, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_docks_to_dig_site, Region.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_volcano_to_dig_site, Region.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_jungle_to_dig_site, Region.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_dig_site_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_docks_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_jungle_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.volcano_to_secret_beach, RegionName.volcano_secret_beach,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.talk_to_island_trader, RegionName.island_trader, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.climb_to_volcano_5, RegionName.volcano_floor_5, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.talk_to_volcano_dwarf, RegionName.volcano_dwarf_shop, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.climb_to_volcano_10, RegionName.volcano_floor_10, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_jungle_to_docks, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_dig_site_to_docks, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_volcano_to_docks, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_volcano_to_jungle, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_docks_to_jungle, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_dig_site_to_jungle, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_docks_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_volcano_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_jungle_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_dig_site_to_volcano, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_docks_to_volcano, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
ConnectionData(Entrance.parrot_express_jungle_to_volcano, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND),
|
||||
|
||||
ConnectionData(LogicEntrance.talk_to_mines_dwarf, LogicRegion.mines_dwarf_shop),
|
||||
|
||||
@@ -708,7 +709,7 @@ def swap_connections_until_valid(regions_by_name, connections_by_name: Dict[str,
|
||||
|
||||
|
||||
def region_should_be_reachable(region_name: str, connections_in_slot: Iterable[ConnectionData]) -> bool:
|
||||
if region_name == Region.menu:
|
||||
if region_name == RegionName.menu:
|
||||
return True
|
||||
for connection in connections_in_slot:
|
||||
if region_name == connection.destination:
|
||||
@@ -718,11 +719,11 @@ def region_should_be_reachable(region_name: str, connections_in_slot: Iterable[C
|
||||
|
||||
def find_reachable_regions(regions_by_name, connections_by_name,
|
||||
randomized_connections: Dict[ConnectionData, ConnectionData]):
|
||||
reachable_regions = {Region.menu}
|
||||
reachable_regions = {RegionName.menu}
|
||||
unreachable_regions = {region for region in regions_by_name.keys()}
|
||||
# unreachable_regions = {region for region in regions_by_name.keys() if region_should_be_reachable(region, connections_by_name.values())}
|
||||
unreachable_regions.remove(Region.menu)
|
||||
exits_to_explore = list(regions_by_name[Region.menu].exits)
|
||||
unreachable_regions.remove(RegionName.menu)
|
||||
exits_to_explore = list(regions_by_name[RegionName.menu].exits)
|
||||
while exits_to_explore:
|
||||
exit_name = exits_to_explore.pop()
|
||||
# if exit_name not in connections_by_name:
|
||||
|
||||
@@ -12,7 +12,7 @@ from typing import List
|
||||
|
||||
from worlds.stardew_valley import LocationData
|
||||
from worlds.stardew_valley.items import load_item_csv, Group, ItemData
|
||||
from worlds.stardew_valley.locations import load_location_csv, LocationTags
|
||||
from worlds.stardew_valley.locations import load_location_csv
|
||||
|
||||
RESOURCE_PACK_CODE_OFFSET = 5000
|
||||
script_folder = Path(__file__)
|
||||
@@ -56,9 +56,9 @@ if __name__ == "__main__":
|
||||
and item.code_without_offset is not None) + 1)
|
||||
|
||||
resource_pack_counter = itertools.count(max(item.code_without_offset
|
||||
for item in loaded_items
|
||||
if Group.RESOURCE_PACK in item.groups
|
||||
and item.code_without_offset is not None) + 1)
|
||||
for item in loaded_items
|
||||
if Group.RESOURCE_PACK in item.groups
|
||||
and item.code_without_offset is not None) + 1)
|
||||
items_to_write = []
|
||||
for item in loaded_items:
|
||||
if item.code_without_offset is None:
|
||||
|
||||
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
||||
from functools import cached_property
|
||||
from itertools import chain
|
||||
from threading import Lock
|
||||
from typing import Iterable, Dict, List, Union, Sized, Hashable, Callable, Tuple, Set, Optional
|
||||
from typing import Iterable, Dict, List, Union, Sized, Hashable, Callable, Tuple, Set, Optional, cast
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from .literal import true_, false_, LiteralStardewRule
|
||||
@@ -318,6 +318,7 @@ class Or(AggregatingStardewRule):
|
||||
return Or(_combinable_rules=other.add_into(self.combinable_rules, self.combine), _simplification_state=self.simplification_state)
|
||||
|
||||
if type(other) is Or:
|
||||
other = cast(Or, other)
|
||||
return Or(_combinable_rules=self.merge(self.combinable_rules, other.combinable_rules),
|
||||
_simplification_state=self.simplification_state.merge(other.simplification_state))
|
||||
|
||||
@@ -344,6 +345,7 @@ class And(AggregatingStardewRule):
|
||||
return And(_combinable_rules=other.add_into(self.combinable_rules, self.combine), _simplification_state=self.simplification_state)
|
||||
|
||||
if type(other) is And:
|
||||
other = cast(And, other)
|
||||
return And(_combinable_rules=self.merge(self.combinable_rules, other.combinable_rules),
|
||||
_simplification_state=self.simplification_state.merge(other.simplification_state))
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ class TestBooksanityNone(SVTestBase):
|
||||
if item_to_ship not in power_books and item_to_ship not in skill_books:
|
||||
continue
|
||||
with self.subTest(location.name):
|
||||
self.assert_reach_location_true(location, self.multiworld.state)
|
||||
self.assert_can_reach_location(location, self.multiworld.state)
|
||||
|
||||
|
||||
class TestBooksanityPowers(SVTestBase):
|
||||
@@ -111,7 +111,7 @@ class TestBooksanityPowers(SVTestBase):
|
||||
if item_to_ship not in power_books and item_to_ship not in skill_books:
|
||||
continue
|
||||
with self.subTest(location.name):
|
||||
self.assert_reach_location_true(location, self.multiworld.state)
|
||||
self.assert_can_reach_location(location, self.multiworld.state)
|
||||
|
||||
|
||||
class TestBooksanityPowersAndSkills(SVTestBase):
|
||||
@@ -157,7 +157,7 @@ class TestBooksanityPowersAndSkills(SVTestBase):
|
||||
if item_to_ship not in power_books and item_to_ship not in skill_books:
|
||||
continue
|
||||
with self.subTest(location.name):
|
||||
self.assert_reach_location_true(location, self.multiworld.state)
|
||||
self.assert_can_reach_location(location, self.multiworld.state)
|
||||
|
||||
|
||||
class TestBooksanityAll(SVTestBase):
|
||||
@@ -203,4 +203,4 @@ class TestBooksanityAll(SVTestBase):
|
||||
if item_to_ship not in power_books and item_to_ship not in skill_books:
|
||||
continue
|
||||
with self.subTest(location.name):
|
||||
self.assert_reach_location_true(location, self.multiworld.state)
|
||||
self.assert_can_reach_location(location, self.multiworld.state)
|
||||
|
||||
@@ -53,8 +53,6 @@ class TestDifferentSettings(SVTestCase):
|
||||
def test_money_rule_caching(self):
|
||||
options_festivals_limited_money = {FestivalLocations.internal_name: FestivalLocations.option_easy,
|
||||
StartingMoney.internal_name: 5000}
|
||||
options_festivals_limited_money = {FestivalLocations.internal_name: FestivalLocations.option_easy,
|
||||
StartingMoney.internal_name: 5000}
|
||||
|
||||
multiplayer_options = [options_festivals_limited_money, options_festivals_limited_money]
|
||||
multiworld = setup_multiworld(multiplayer_options)
|
||||
|
||||
@@ -25,7 +25,7 @@ class TestWalnutsanityNone(SVTestBase):
|
||||
self.collect("Island Obelisk")
|
||||
self.collect("Island West Turtle")
|
||||
self.collect("Progressive House")
|
||||
items = self.collect("5 Golden Walnuts", 10)
|
||||
self.collect("5 Golden Walnuts", 10)
|
||||
|
||||
self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player))
|
||||
self.collect("Island North Turtle")
|
||||
@@ -81,10 +81,10 @@ class TestWalnutsanityPuzzles(SVTestBase):
|
||||
self.collect("Combat Level", 10)
|
||||
self.collect("Mining Level", 10)
|
||||
for location in locations:
|
||||
self.assert_reach_location_false(location, self.multiworld.state)
|
||||
self.assert_cannot_reach_location(location, self.multiworld.state)
|
||||
self.collect("Open Professor Snail Cave")
|
||||
for location in locations:
|
||||
self.assert_reach_location_true(location, self.multiworld.state)
|
||||
self.assert_can_reach_location(location, self.multiworld.state)
|
||||
|
||||
|
||||
class TestWalnutsanityBushes(SVTestBase):
|
||||
@@ -126,10 +126,10 @@ class TestWalnutsanityPuzzlesAndBushes(SVTestBase):
|
||||
# You need to receive 25, and collect 15
|
||||
self.collect("Island Obelisk")
|
||||
self.collect("Island West Turtle")
|
||||
items = self.collect("5 Golden Walnuts", 5)
|
||||
self.collect("5 Golden Walnuts", 5)
|
||||
|
||||
self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player))
|
||||
items = self.collect("Island North Turtle")
|
||||
self.collect("Island North Turtle")
|
||||
self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player))
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ class TestWalnutsanityAll(SVTestBase):
|
||||
self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player))
|
||||
self.remove(items)
|
||||
self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player))
|
||||
items = self.collect("5 Golden Walnuts", 4)
|
||||
items = self.collect("3 Golden Walnuts", 6)
|
||||
items = self.collect("Golden Walnut", 2)
|
||||
self.collect("5 Golden Walnuts", 4)
|
||||
self.collect("3 Golden Walnuts", 6)
|
||||
self.collect("Golden Walnut", 2)
|
||||
self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import List
|
||||
from unittest import TestCase
|
||||
|
||||
from BaseClasses import CollectionState, Location
|
||||
from BaseClasses import CollectionState, Location, Region
|
||||
from ...stardew_rule import StardewRule, false_, MISSING_ITEM, Reach
|
||||
from ...stardew_rule.rule_explain import explain
|
||||
|
||||
@@ -40,19 +40,42 @@ class RuleAssertMixin(TestCase):
|
||||
raise AssertionError(f"Error while checking rule {rule}: {e}"
|
||||
f"\nExplanation: {expl}")
|
||||
|
||||
def assert_reach_location_true(self, location: Location, state: CollectionState):
|
||||
expl = explain(Reach(location.name, "Location", 1), state)
|
||||
def assert_can_reach_location(self, location: Location | str, state: CollectionState) -> None:
|
||||
location_name = location.name if isinstance(location, Location) else location
|
||||
expl = explain(Reach(location_name, "Location", 1), state)
|
||||
try:
|
||||
can_reach = location.can_reach(state)
|
||||
can_reach = state.can_reach_location(location_name, 1)
|
||||
self.assertTrue(can_reach, expl)
|
||||
except KeyError as e:
|
||||
raise AssertionError(f"Error while checking location {location.name}: {e}"
|
||||
raise AssertionError(f"Error while checking location {location_name}: {e}"
|
||||
f"\nExplanation: {expl}")
|
||||
|
||||
def assert_reach_location_false(self, location: Location, state: CollectionState):
|
||||
expl = explain(Reach(location.name, "Location", 1), state, expected=False)
|
||||
def assert_cannot_reach_location(self, location: Location | str, state: CollectionState) -> None:
|
||||
location_name = location.name if isinstance(location, Location) else location
|
||||
expl = explain(Reach(location_name, "Location", 1), state, expected=False)
|
||||
try:
|
||||
self.assertFalse(location.can_reach(state), expl)
|
||||
can_reach = state.can_reach_location(location_name, 1)
|
||||
self.assertFalse(can_reach, expl)
|
||||
except KeyError as e:
|
||||
raise AssertionError(f"Error while checking location {location.name}: {e}"
|
||||
raise AssertionError(f"Error while checking location {location_name}: {e}"
|
||||
f"\nExplanation: {expl}")
|
||||
|
||||
def assert_can_reach_region(self, region: Region | str, state: CollectionState) -> None:
|
||||
region_name = region.name if isinstance(region, Region) else region
|
||||
expl = explain(Reach(region_name, "Region", 1), state)
|
||||
try:
|
||||
can_reach = state.can_reach_region(region_name, 1)
|
||||
self.assertTrue(can_reach, expl)
|
||||
except KeyError as e:
|
||||
raise AssertionError(f"Error while checking region {region_name}: {e}"
|
||||
f"\nExplanation: {expl}")
|
||||
|
||||
def assert_cannot_reach_region(self, region: Region | str, state: CollectionState) -> None:
|
||||
region_name = region.name if isinstance(region, Region) else region
|
||||
expl = explain(Reach(region_name, "Region", 1), state, expected=False)
|
||||
try:
|
||||
can_reach = state.can_reach_region(region_name, 1)
|
||||
self.assertFalse(can_reach, expl)
|
||||
except KeyError as e:
|
||||
raise AssertionError(f"Error while checking region {region_name}: {e}"
|
||||
f"\nExplanation: {expl}")
|
||||
|
||||
@@ -53,7 +53,7 @@ class WorldAssertMixin(RuleAssertMixin, TestCase):
|
||||
|
||||
def assert_can_reach_everything(self, multiworld: MultiWorld):
|
||||
for location in multiworld.get_locations():
|
||||
self.assert_reach_location_true(location, multiworld.state)
|
||||
self.assert_can_reach_location(location, multiworld.state)
|
||||
|
||||
def assert_basic_checks(self, multiworld: MultiWorld):
|
||||
self.assert_same_number_items_locations(multiworld)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import random
|
||||
|
||||
from BaseClasses import get_seed
|
||||
from .. import SVTestBase, SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, solo_multiworld, \
|
||||
fill_dataclass_with_default
|
||||
from .. import SVTestBase, SVTestCase, allsanity_mods_6_x_x, fill_dataclass_with_default
|
||||
from ..assertion import ModAssertMixin, WorldAssertMixin
|
||||
from ... import items, Group, ItemClassification, create_content
|
||||
from ... import options
|
||||
from ...items import items_by_group
|
||||
from ...mods.mod_data import ModNames
|
||||
from ...options import SkillProgression, Walnutsanity
|
||||
from ...options.options import all_mods
|
||||
from ...regions import RandomizationFlag, randomize_connections, create_final_connections_and_regions
|
||||
|
||||
|
||||
@@ -20,17 +20,58 @@ class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase):
|
||||
self.assert_basic_checks(multi_world)
|
||||
self.assert_stray_mod_items(mod, multi_world)
|
||||
|
||||
def test_given_mod_names_when_generate_paired_with_entrance_randomizer_then_basic_checks(self):
|
||||
for option in options.EntranceRandomization.options:
|
||||
for mod in options.Mods.valid_keys:
|
||||
world_options = {
|
||||
options.EntranceRandomization: options.EntranceRandomization.options[option],
|
||||
options.Mods: mod,
|
||||
options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false
|
||||
}
|
||||
with self.solo_world_sub_test(f"entrance_randomization: {option}, Mod: {mod}", world_options) as (multi_world, _):
|
||||
self.assert_basic_checks(multi_world)
|
||||
self.assert_stray_mod_items(mod, multi_world)
|
||||
# The following tests validate that ER still generates winnable and logically-sane games with given mods.
|
||||
# Mods that do not interact with entrances are skipped
|
||||
# Not all ER settings are tested, because 'buildings' is, essentially, a superset of all others
|
||||
def test_deepwoods_entrance_randomization_buildings(self):
|
||||
self.perform_basic_checks_on_mod_with_er(ModNames.deepwoods, options.EntranceRandomization.option_buildings)
|
||||
|
||||
def test_juna_entrance_randomization_buildings(self):
|
||||
self.perform_basic_checks_on_mod_with_er(ModNames.juna, options.EntranceRandomization.option_buildings)
|
||||
|
||||
def test_jasper_entrance_randomization_buildings(self):
|
||||
self.perform_basic_checks_on_mod_with_er(ModNames.jasper, options.EntranceRandomization.option_buildings)
|
||||
|
||||
def test_alec_entrance_randomization_buildings(self):
|
||||
self.perform_basic_checks_on_mod_with_er(ModNames.alec, options.EntranceRandomization.option_buildings)
|
||||
|
||||
def test_yoba_entrance_randomization_buildings(self):
|
||||
self.perform_basic_checks_on_mod_with_er(ModNames.yoba, options.EntranceRandomization.option_buildings)
|
||||
|
||||
def test_eugene_entrance_randomization_buildings(self):
|
||||
self.perform_basic_checks_on_mod_with_er(ModNames.eugene, options.EntranceRandomization.option_buildings)
|
||||
|
||||
def test_ayeisha_entrance_randomization_buildings(self):
|
||||
self.perform_basic_checks_on_mod_with_er(ModNames.ayeisha, options.EntranceRandomization.option_buildings)
|
||||
|
||||
def test_riley_entrance_randomization_buildings(self):
|
||||
self.perform_basic_checks_on_mod_with_er(ModNames.riley, options.EntranceRandomization.option_buildings)
|
||||
|
||||
def test_sve_entrance_randomization_buildings(self):
|
||||
self.perform_basic_checks_on_mod_with_er(ModNames.sve, options.EntranceRandomization.option_buildings)
|
||||
|
||||
def test_alecto_entrance_randomization_buildings(self):
|
||||
self.perform_basic_checks_on_mod_with_er(ModNames.alecto, options.EntranceRandomization.option_buildings)
|
||||
|
||||
def test_lacey_entrance_randomization_buildings(self):
|
||||
self.perform_basic_checks_on_mod_with_er(ModNames.lacey, options.EntranceRandomization.option_buildings)
|
||||
|
||||
def test_boarding_house_entrance_randomization_buildings(self):
|
||||
self.perform_basic_checks_on_mod_with_er(ModNames.boarding_house, options.EntranceRandomization.option_buildings)
|
||||
|
||||
def test_all_mods_entrance_randomization_buildings(self):
|
||||
self.perform_basic_checks_on_mod_with_er(all_mods, options.EntranceRandomization.option_buildings)
|
||||
|
||||
def perform_basic_checks_on_mod_with_er(self, mods: str | set[str], er_option: int) -> None:
|
||||
if isinstance(mods, str):
|
||||
mods = {mods}
|
||||
world_options = {
|
||||
options.EntranceRandomization: er_option,
|
||||
options.Mods: frozenset(mods),
|
||||
options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false
|
||||
}
|
||||
with self.solo_world_sub_test(f"entrance_randomization: {er_option}, Mods: {mods}", world_options) as (multi_world, _):
|
||||
self.assert_basic_checks(multi_world)
|
||||
|
||||
def test_allsanity_all_mods_when_generate_then_basic_checks(self):
|
||||
with self.solo_world_sub_test(world_options=allsanity_mods_6_x_x()) as (multi_world, _):
|
||||
@@ -147,19 +188,3 @@ class TestModEntranceRando(SVTestCase):
|
||||
|
||||
self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()),
|
||||
f"Connections are duplicated in randomization.")
|
||||
|
||||
|
||||
class TestModTraps(SVTestCase):
|
||||
def test_given_traps_when_generate_then_all_traps_in_pool(self):
|
||||
for value in options.TrapItems.options:
|
||||
if value == "no_traps":
|
||||
continue
|
||||
|
||||
world_options = allsanity_no_mods_6_x_x()
|
||||
world_options.update({options.TrapItems.internal_name: options.TrapItems.options[value], options.Mods.internal_name: "Magic"})
|
||||
with solo_multiworld(world_options) as (multi_world, _):
|
||||
trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups]
|
||||
multiworld_items = [item.name for item in multi_world.get_items()]
|
||||
for item in trap_items:
|
||||
with self.subTest(f"Option: {value}, Item: {item}"):
|
||||
self.assertIn(item, multiworld_items)
|
||||
|
||||
28
worlds/stardew_valley/test/mods/TestModsFill.py
Normal file
28
worlds/stardew_valley/test/mods/TestModsFill.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from .. import SVTestBase
|
||||
from ... import options
|
||||
|
||||
|
||||
class TestNoGingerIslandCraftingRecipesAreRequired(SVTestBase):
|
||||
options = {
|
||||
options.Goal.internal_name: options.Goal.option_craft_master,
|
||||
options.Craftsanity.internal_name: options.Craftsanity.option_all,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
|
||||
options.Mods.internal_name: frozenset(options.Mods.valid_keys)
|
||||
}
|
||||
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class TestNoGingerIslandCookingRecipesAreRequired(SVTestBase):
|
||||
options = {
|
||||
options.Goal.internal_name: options.Goal.option_gourmet_chef,
|
||||
options.Cooksanity.internal_name: options.Cooksanity.option_all,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
|
||||
options.Mods.internal_name: frozenset(options.Mods.valid_keys)
|
||||
}
|
||||
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
return True
|
||||
@@ -12,15 +12,13 @@ class TestBooksLogic(SVTestBase):
|
||||
|
||||
location = self.multiworld.get_location("Read Mapping Cave Systems", self.player)
|
||||
|
||||
self.assert_reach_location_false(location, self.multiworld.state)
|
||||
self.assert_cannot_reach_location(location, self.multiworld.state)
|
||||
|
||||
self.collect("Progressive Mine Elevator")
|
||||
self.collect("Progressive Mine Elevator")
|
||||
self.collect("Progressive Mine Elevator")
|
||||
self.collect("Progressive Mine Elevator")
|
||||
self.assert_reach_location_false(location, self.multiworld.state)
|
||||
self.assert_cannot_reach_location(location, self.multiworld.state)
|
||||
|
||||
self.collect("Progressive Weapon")
|
||||
self.assert_reach_location_true(location, self.multiworld.state)
|
||||
|
||||
|
||||
self.assert_can_reach_location(location, self.multiworld.state)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from ...options import SeasonRandomization, Friendsanity, FriendsanityHeartSize, Fishsanity, ExcludeGingerIsland, SkillProgression, ToolProgression, \
|
||||
ElevatorProgression, SpecialOrderLocations
|
||||
from ...options import SeasonRandomization, Fishsanity, ExcludeGingerIsland, SkillProgression, ToolProgression, ElevatorProgression, SpecialOrderLocations
|
||||
from ...strings.fish_names import Fish
|
||||
from ...test import SVTestBase
|
||||
|
||||
@@ -44,18 +43,18 @@ class TestNeedRegionToCatchFish(SVTestBase):
|
||||
self.collect_all_the_money()
|
||||
item_names = fish_and_items[fish]
|
||||
location = self.multiworld.get_location(f"Fishsanity: {fish}", self.player)
|
||||
self.assert_reach_location_false(location, self.multiworld.state)
|
||||
self.assert_cannot_reach_location(location, self.multiworld.state)
|
||||
items = []
|
||||
for item_name in item_names:
|
||||
items.append(self.collect(item_name))
|
||||
with self.subTest(f"{fish} can be reached with {item_names}"):
|
||||
self.assert_reach_location_true(location, self.multiworld.state)
|
||||
self.assert_can_reach_location(location, self.multiworld.state)
|
||||
for item_required in items:
|
||||
self.multiworld.state = self.original_state.copy()
|
||||
with self.subTest(f"{fish} requires {item_required.name}"):
|
||||
for item_to_collect in items:
|
||||
if item_to_collect.name != item_required.name:
|
||||
self.collect(item_to_collect)
|
||||
self.assert_reach_location_false(location, self.multiworld.state)
|
||||
self.assert_cannot_reach_location(location, self.multiworld.state)
|
||||
|
||||
self.multiworld.state = self.original_state.copy()
|
||||
|
||||
@@ -39,10 +39,10 @@ class TestSkillProgressionProgressive(SVTestBase):
|
||||
|
||||
with self.subTest(location_name):
|
||||
if level > 1:
|
||||
self.assert_reach_location_false(location, self.multiworld.state)
|
||||
self.assert_cannot_reach_location(location, self.multiworld.state)
|
||||
self.collect(f"{skill} Level")
|
||||
|
||||
self.assert_reach_location_true(location, self.multiworld.state)
|
||||
self.assert_can_reach_location(location, self.multiworld.state)
|
||||
|
||||
self.reset_collection_state()
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestSkillProgressionProgressiveWithMasteryWithoutMods(SVTestBase):
|
||||
for skill in all_vanilla_skills:
|
||||
with self.subTest(skill):
|
||||
location = self.multiworld.get_location(f"{skill} Mastery", self.player)
|
||||
self.assert_reach_location_true(location, self.multiworld.state)
|
||||
self.assert_can_reach_location(location, self.multiworld.state)
|
||||
|
||||
self.reset_collection_state()
|
||||
|
||||
@@ -99,7 +99,7 @@ class TestSkillProgressionProgressiveWithMasteryWithoutMods(SVTestBase):
|
||||
self.remove_one_by_name(f"{skill} Level")
|
||||
|
||||
location = self.multiworld.get_location(f"{skill} Mastery", self.player)
|
||||
self.assert_reach_location_false(location, self.multiworld.state)
|
||||
self.assert_cannot_reach_location(location, self.multiworld.state)
|
||||
|
||||
self.reset_collection_state()
|
||||
|
||||
@@ -108,6 +108,6 @@ class TestSkillProgressionProgressiveWithMasteryWithoutMods(SVTestBase):
|
||||
|
||||
self.remove_one_by_name(f"Progressive Pickaxe")
|
||||
location = self.multiworld.get_location("Mining Mastery", self.player)
|
||||
self.assert_reach_location_false(location, self.multiworld.state)
|
||||
self.assert_cannot_reach_location(location, self.multiworld.state)
|
||||
|
||||
self.reset_collection_state()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set
|
||||
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set, TextIO
|
||||
from logging import warning
|
||||
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
|
||||
from .items import (item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names,
|
||||
@@ -78,7 +78,8 @@ class TunicWorld(World):
|
||||
settings: ClassVar[TunicSettings]
|
||||
item_name_groups = item_name_groups
|
||||
location_name_groups = location_name_groups
|
||||
location_name_groups.update(grass_location_name_groups)
|
||||
for group_name, members in grass_location_name_groups.items():
|
||||
location_name_groups.setdefault(group_name, set()).update(members)
|
||||
|
||||
item_name_to_id = item_name_to_id
|
||||
location_name_to_id = standard_location_name_to_id.copy()
|
||||
@@ -241,10 +242,18 @@ class TunicWorld(World):
|
||||
|
||||
def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem:
|
||||
item_data = item_table[name]
|
||||
# if item_data.combat_ic is None, it'll take item_data.classification instead
|
||||
itemclass: ItemClassification = ((item_data.combat_ic if self.options.combat_logic else None)
|
||||
# evaluate alternate classifications based on options
|
||||
# it'll choose whichever classification isn't None first in this if else tree
|
||||
itemclass: ItemClassification = (classification
|
||||
or (item_data.combat_ic if self.options.combat_logic else None)
|
||||
or (ItemClassification.progression | ItemClassification.useful
|
||||
if name == "Glass Cannon" and self.options.grass_randomizer
|
||||
and not self.options.start_with_sword else None)
|
||||
or (ItemClassification.progression | ItemClassification.useful
|
||||
if name == "Shield" and self.options.ladder_storage
|
||||
and not self.options.ladder_storage_without_items else None)
|
||||
or item_data.classification)
|
||||
return TunicItem(name, classification or itemclass, self.item_name_to_id[name], self.player)
|
||||
return TunicItem(name, itemclass, self.item_name_to_id[name], self.player)
|
||||
|
||||
def create_items(self) -> None:
|
||||
tunic_items: List[TunicItem] = []
|
||||
@@ -277,8 +286,6 @@ class TunicWorld(World):
|
||||
|
||||
if self.options.grass_randomizer:
|
||||
items_to_create["Grass"] = len(grass_location_table)
|
||||
tunic_items.append(self.create_item("Glass Cannon", ItemClassification.progression))
|
||||
items_to_create["Glass Cannon"] = 0
|
||||
for grass_location in excluded_grass_locations:
|
||||
self.get_location(grass_location).place_locked_item(self.create_item("Grass"))
|
||||
items_to_create["Grass"] -= len(excluded_grass_locations)
|
||||
@@ -331,10 +338,11 @@ class TunicWorld(World):
|
||||
|
||||
remove_filler(items_to_create[gold_hexagon])
|
||||
|
||||
# Sort for deterministic order
|
||||
for hero_relic in sorted(item_name_groups["Hero Relics"]):
|
||||
tunic_items.append(self.create_item(hero_relic, ItemClassification.useful))
|
||||
items_to_create[hero_relic] = 0
|
||||
if not self.options.combat_logic:
|
||||
# Sort for deterministic order
|
||||
for hero_relic in sorted(item_name_groups["Hero Relics"]):
|
||||
tunic_items.append(self.create_item(hero_relic, ItemClassification.useful))
|
||||
items_to_create[hero_relic] = 0
|
||||
|
||||
if not self.options.ability_shuffling:
|
||||
# Sort for deterministic order
|
||||
@@ -349,11 +357,6 @@ class TunicWorld(World):
|
||||
tunic_items.append(self.create_item(page, ItemClassification.progression | ItemClassification.useful))
|
||||
items_to_create[page] = 0
|
||||
|
||||
# logically relevant if you have ladder storage enabled
|
||||
if self.options.ladder_storage and not self.options.ladder_storage_without_items:
|
||||
tunic_items.append(self.create_item("Shield", ItemClassification.progression))
|
||||
items_to_create["Shield"] = 0
|
||||
|
||||
if self.options.maskless:
|
||||
tunic_items.append(self.create_item("Scavenger Mask", ItemClassification.useful))
|
||||
items_to_create["Scavenger Mask"] = 0
|
||||
@@ -411,7 +414,7 @@ class TunicWorld(World):
|
||||
def stage_pre_fill(cls, multiworld: MultiWorld) -> None:
|
||||
tunic_fill_worlds: List[TunicWorld] = [world for world in multiworld.get_game_worlds("TUNIC")
|
||||
if world.options.local_fill.value > 0]
|
||||
if tunic_fill_worlds:
|
||||
if tunic_fill_worlds and multiworld.players > 1:
|
||||
grass_fill: List[TunicItem] = []
|
||||
non_grass_fill: List[TunicItem] = []
|
||||
grass_fill_locations: List[Location] = []
|
||||
@@ -500,6 +503,13 @@ class TunicWorld(World):
|
||||
state.tunic_need_to_reset_combat_from_remove[self.player] = True
|
||||
return change
|
||||
|
||||
def write_spoiler_header(self, spoiler_handle: TextIO):
|
||||
if self.options.hexagon_quest and self.options.ability_shuffling:
|
||||
spoiler_handle.write("\nAbility Unlocks (Hexagon Quest):\n")
|
||||
for ability in self.ability_unlocks:
|
||||
# Remove parentheses for better readability
|
||||
spoiler_handle.write(f'{ability[ability.find("(")+1:ability.find(")")]}: {self.ability_unlocks[ability]} Gold Questagons\n')
|
||||
|
||||
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
|
||||
if self.options.entrance_rando:
|
||||
hint_data.update({self.player: {}})
|
||||
|
||||
@@ -8,6 +8,7 @@ from worlds.AutoWorld import LogicMixin
|
||||
|
||||
# the vanilla stats you are expected to have to get through an area, based on where they are in vanilla
|
||||
class AreaStats(NamedTuple):
|
||||
"""Attack, Defense, Potion, HP, SP, MP, Flasks, Equipment, is_boss"""
|
||||
att_level: int
|
||||
def_level: int
|
||||
potion_level: int # all 3 are before your first bonfire after getting the upgrade page, third costs 1k
|
||||
@@ -41,7 +42,7 @@ area_data: Dict[str, AreaStats] = {
|
||||
"Rooted Ziggurat": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic"]),
|
||||
"Boss Scavenger": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic"], is_boss=True),
|
||||
"Swamp": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"]),
|
||||
"Cathedral": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"]),
|
||||
# Cathedral has the same requirements as Swamp
|
||||
# marked as boss because the garden knights can't get hurt by stick
|
||||
"Gauntlet": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"], is_boss=True),
|
||||
"The Heir": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic", "Laurels"], is_boss=True),
|
||||
@@ -49,8 +50,10 @@ area_data: Dict[str, AreaStats] = {
|
||||
|
||||
|
||||
# these are used for caching which areas can currently be reached in state
|
||||
# Gauntlet does not have exclusively higher stat requirements, so it will be checked separately
|
||||
boss_areas: List[str] = [name for name, data in area_data.items() if data.is_boss and name != "Gauntlet"]
|
||||
non_boss_areas: List[str] = [name for name, data in area_data.items() if not data.is_boss]
|
||||
# Swamp does not have exclusively higher stat requirements, so it will be checked separately
|
||||
non_boss_areas: List[str] = [name for name, data in area_data.items() if not data.is_boss and name != "Swamp"]
|
||||
|
||||
|
||||
class CombatState(IntEnum):
|
||||
@@ -89,6 +92,7 @@ def has_combat_reqs(area_name: str, state: CollectionState, player: int) -> bool
|
||||
elif area_name in non_boss_areas:
|
||||
area_list = non_boss_areas
|
||||
else:
|
||||
# this is to check Swamp and Gauntlet on their own
|
||||
area_list = [area_name]
|
||||
|
||||
if met_combat_reqs:
|
||||
@@ -114,88 +118,99 @@ def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_d
|
||||
extra_att_needed = 0
|
||||
extra_def_needed = 0
|
||||
extra_mp_needed = 0
|
||||
has_magic = state.has_any({"Magic Wand", "Gun"}, player)
|
||||
stick_bool = False
|
||||
sword_bool = False
|
||||
has_magic = state.has_any(("Magic Wand", "Gun"), player)
|
||||
sword_bool = has_sword(state, player)
|
||||
stick_bool = sword_bool or has_melee(state, player)
|
||||
equipment = data.equipment.copy()
|
||||
for item in data.equipment:
|
||||
if item == "Stick":
|
||||
if not has_melee(state, player):
|
||||
if not stick_bool:
|
||||
if has_magic:
|
||||
equipment.remove("Stick")
|
||||
if "Magic" not in equipment:
|
||||
equipment.append("Magic")
|
||||
# magic can make up for the lack of stick
|
||||
extra_mp_needed += 2
|
||||
extra_att_needed -= 16
|
||||
extra_att_needed -= 32
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
stick_bool = True
|
||||
|
||||
elif item == "Sword":
|
||||
if not has_sword(state, player):
|
||||
if not sword_bool:
|
||||
# need sword for bosses
|
||||
if data.is_boss:
|
||||
return False
|
||||
equipment.remove("Sword")
|
||||
if has_magic:
|
||||
if "Magic" not in equipment:
|
||||
equipment.append("Magic")
|
||||
# +4 mp pretty much makes up for the lack of sword, at least in Quarry
|
||||
extra_mp_needed += 4
|
||||
# stick is a backup plan, and doesn't scale well, so let's require a little less
|
||||
extra_att_needed -= 2
|
||||
elif has_melee(state, player):
|
||||
if stick_bool:
|
||||
# stick is a backup plan, and doesn't scale well, so let's require a little less
|
||||
equipment.append("Stick")
|
||||
extra_att_needed -= 2
|
||||
else:
|
||||
extra_mp_needed += 2
|
||||
extra_att_needed -= 32
|
||||
elif stick_bool:
|
||||
equipment.append("Stick")
|
||||
# may revise this later based on feedback
|
||||
extra_att_needed += 3
|
||||
extra_def_needed += 2
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
sword_bool = True
|
||||
|
||||
# just increase the stat requirement, we'll check for shield when calculating defense
|
||||
elif item == "Shield":
|
||||
if not state.has("Shield", player):
|
||||
extra_def_needed += 2
|
||||
equipment.remove("Shield")
|
||||
extra_def_needed += 2
|
||||
|
||||
elif item == "Laurels":
|
||||
if not state.has("Hero's Laurels", player):
|
||||
# these are entirely based on vibes
|
||||
extra_att_needed += 2
|
||||
extra_def_needed += 3
|
||||
# require Laurels for the Heir
|
||||
return False
|
||||
|
||||
elif item == "Magic":
|
||||
if not has_magic:
|
||||
equipment.remove("Magic")
|
||||
extra_att_needed += 2
|
||||
extra_def_needed += 2
|
||||
extra_mp_needed -= 16
|
||||
extra_mp_needed -= 32
|
||||
|
||||
modified_stats = AreaStats(data.att_level + extra_att_needed, data.def_level + extra_def_needed, data.potion_level,
|
||||
data.hp_level, data.sp_level, data.mp_level + extra_mp_needed, data.potion_count)
|
||||
if not has_required_stats(modified_stats, state, player):
|
||||
data.hp_level, data.sp_level, data.mp_level + extra_mp_needed, data.potion_count,
|
||||
equipment, data.is_boss)
|
||||
if has_required_stats(modified_stats, state, player):
|
||||
return True
|
||||
else:
|
||||
# we may need to check if you would have the required stats if you were missing a weapon
|
||||
# it's kinda janky, but these only get hit in less than once per 100 generations, so whatever
|
||||
if sword_bool and "Sword" in data.equipment and "Magic" in data.equipment:
|
||||
# we need to check if you would have the required stats if you didn't have melee
|
||||
equip_list = [item for item in data.equipment if item != "Sword"]
|
||||
more_modified_stats = AreaStats(data.att_level - 16, data.def_level, data.potion_level,
|
||||
data.hp_level, data.sp_level, data.mp_level + 4, data.potion_count,
|
||||
equip_list)
|
||||
if sword_bool and "Sword" in equipment and has_magic:
|
||||
# we need to check if you would have the required stats if you didn't have the sword
|
||||
equip_list = [item for item in equipment if item != "Sword"]
|
||||
if "Magic" not in equip_list:
|
||||
equip_list.append("Magic")
|
||||
more_modified_stats = AreaStats(modified_stats.att_level - 32, modified_stats.def_level,
|
||||
modified_stats.potion_level, modified_stats.hp_level,
|
||||
modified_stats.sp_level, modified_stats.mp_level + 4,
|
||||
modified_stats.potion_count, equip_list, data.is_boss)
|
||||
if check_combat_reqs("none", state, player, more_modified_stats):
|
||||
return True
|
||||
|
||||
# and we need to check if you would have the required stats if you didn't have magic
|
||||
equip_list = [item for item in data.equipment if item != "Magic"]
|
||||
more_modified_stats = AreaStats(data.att_level + 2, data.def_level + 2, data.potion_level,
|
||||
data.hp_level, data.sp_level, data.mp_level - 16, data.potion_count,
|
||||
equip_list)
|
||||
if check_combat_reqs("none", state, player, more_modified_stats):
|
||||
return True
|
||||
return False
|
||||
|
||||
elif stick_bool and "Stick" in data.equipment and "Magic" in data.equipment:
|
||||
elif stick_bool and "Stick" in equipment and has_magic:
|
||||
# we need to check if you would have the required stats if you didn't have the stick
|
||||
equip_list = [item for item in data.equipment if item != "Stick"]
|
||||
more_modified_stats = AreaStats(data.att_level - 16, data.def_level, data.potion_level,
|
||||
data.hp_level, data.sp_level, data.mp_level + 4, data.potion_count,
|
||||
equip_list)
|
||||
equip_list = [item for item in equipment if item != "Stick"]
|
||||
if "Magic" not in equip_list:
|
||||
equip_list.append("Magic")
|
||||
more_modified_stats = AreaStats(modified_stats.att_level - 32, modified_stats.def_level,
|
||||
modified_stats.potion_level, modified_stats.hp_level,
|
||||
modified_stats.sp_level, modified_stats.mp_level + 4,
|
||||
modified_stats.potion_count, equip_list, data.is_boss)
|
||||
if check_combat_reqs("none", state, player, more_modified_stats):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# check if you have the required stats, and the money to afford them
|
||||
@@ -203,72 +218,63 @@ def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_d
|
||||
# but that's fine -- it's already pretty generous to begin with
|
||||
def has_required_stats(data: AreaStats, state: CollectionState, player: int) -> bool:
|
||||
money_required = 0
|
||||
player_att = 0
|
||||
att_required = data.att_level
|
||||
player_att, att_offerings = get_att_level(state, player)
|
||||
|
||||
# check if we actually need the stat before checking state
|
||||
if data.att_level > 1:
|
||||
player_att, att_offerings = get_att_level(state, player)
|
||||
if player_att < data.att_level:
|
||||
return False
|
||||
# if you have 2 more attack than needed, we can forego needing mp
|
||||
if data.mp_level > 1:
|
||||
if player_att < data.att_level + 2:
|
||||
player_mp, mp_offerings = get_mp_level(state, player)
|
||||
if player_mp < data.mp_level:
|
||||
return False
|
||||
else:
|
||||
extra_mp = player_mp - data.mp_level
|
||||
paid_mp = max(0, mp_offerings - extra_mp)
|
||||
# mp costs 300 for the first, +50 for each additional
|
||||
money_per_mp = 300
|
||||
for _ in range(paid_mp):
|
||||
money_required += money_per_mp
|
||||
money_per_mp += 50
|
||||
else:
|
||||
extra_att = player_att - data.att_level
|
||||
paid_att = max(0, att_offerings - extra_att)
|
||||
# attack upgrades cost 100 for the first, +50 for each additional
|
||||
money_per_att = 100
|
||||
for _ in range(paid_att):
|
||||
money_required += money_per_att
|
||||
money_per_att += 50
|
||||
att_required += 2
|
||||
|
||||
if player_att < att_required:
|
||||
return False
|
||||
else:
|
||||
extra_att = player_att - att_required
|
||||
paid_att = max(0, att_offerings - extra_att)
|
||||
# attack upgrades cost 100 for the first, +50 for each additional
|
||||
money_per_att = 100
|
||||
for _ in range(paid_att):
|
||||
money_required += money_per_att
|
||||
money_per_att += 50
|
||||
|
||||
# adding defense and sp together since they accomplish similar things: making you take less damage
|
||||
if data.def_level + data.sp_level > 2:
|
||||
player_def, def_offerings = get_def_level(state, player)
|
||||
player_sp, sp_offerings = get_sp_level(state, player)
|
||||
if player_def + player_sp < data.def_level + data.sp_level:
|
||||
req_stats = data.def_level + data.sp_level
|
||||
if player_def + player_sp < req_stats:
|
||||
return False
|
||||
else:
|
||||
free_def = player_def - def_offerings
|
||||
free_sp = player_sp - sp_offerings
|
||||
paid_stats = data.def_level + data.sp_level - free_def - free_sp
|
||||
sp_to_buy = 0
|
||||
|
||||
if paid_stats <= 0:
|
||||
# if you don't have to pay for any stats, you don't need money for these upgrades
|
||||
def_to_buy = 0
|
||||
elif paid_stats <= def_offerings:
|
||||
# get the amount needed to buy these def offerings
|
||||
def_to_buy = paid_stats
|
||||
if free_sp + free_def >= req_stats:
|
||||
# you don't need to buy upgrades
|
||||
pass
|
||||
else:
|
||||
def_to_buy = def_offerings
|
||||
sp_to_buy = max(0, paid_stats - def_offerings)
|
||||
|
||||
# if you have to buy more than 3 def, it's cheaper to buy 1 extra sp
|
||||
if def_to_buy > 3 and sp_offerings > 0:
|
||||
def_to_buy -= 1
|
||||
sp_to_buy += 1
|
||||
# def costs 100 for the first, +50 for each additional
|
||||
money_per_def = 100
|
||||
for _ in range(def_to_buy):
|
||||
money_required += money_per_def
|
||||
money_per_def += 50
|
||||
# sp costs 200 for the first, +200 for each additional
|
||||
money_per_sp = 200
|
||||
for _ in range(sp_to_buy):
|
||||
money_required += money_per_sp
|
||||
money_per_sp += 200
|
||||
|
||||
# if you have 2 more attack than needed, we can forego needing mp
|
||||
if data.mp_level > 1 and player_att < data.att_level + 2:
|
||||
player_mp, mp_offerings = get_mp_level(state, player)
|
||||
if player_mp < data.mp_level:
|
||||
return False
|
||||
else:
|
||||
extra_mp = player_mp - data.mp_level
|
||||
paid_mp = max(0, mp_offerings - extra_mp)
|
||||
# mp costs 300 for the first, +50 for each additional
|
||||
money_per_mp = 300
|
||||
for _ in range(paid_mp):
|
||||
money_required += money_per_mp
|
||||
money_per_mp += 50
|
||||
# we need to pick the cheapest option that gets us above the stats we need
|
||||
# first number is def, second number is sp
|
||||
upgrade_options: set[tuple[int, int]] = set()
|
||||
stats_to_buy = req_stats - free_def - free_sp
|
||||
for paid_def in range(0, min(def_offerings + 1, stats_to_buy + 1)):
|
||||
sp_required = stats_to_buy - paid_def
|
||||
if sp_offerings >= sp_required:
|
||||
if sp_required < 0:
|
||||
break
|
||||
upgrade_options.add((paid_def, stats_to_buy - paid_def))
|
||||
costs = [calc_def_sp_cost(defense, sp) for defense, sp in upgrade_options]
|
||||
money_required += min(costs)
|
||||
|
||||
req_effective_hp = calc_effective_hp(data.hp_level, data.potion_level, data.potion_count)
|
||||
player_potion, potion_offerings = get_potion_level(state, player)
|
||||
@@ -279,53 +285,30 @@ def has_required_stats(data: AreaStats, state: CollectionState, player: int) ->
|
||||
return False
|
||||
else:
|
||||
# need a way to determine which of potion offerings or hp offerings you can reduce
|
||||
# your level if you didn't pay for offerings
|
||||
free_potion = player_potion - potion_offerings
|
||||
free_hp = player_hp - hp_offerings
|
||||
paid_hp_count = 0
|
||||
paid_potion_count = 0
|
||||
if calc_effective_hp(free_hp, free_potion, player_potion_count) >= req_effective_hp:
|
||||
# you don't need to buy upgrades
|
||||
pass
|
||||
# if you have no potions, or no potion upgrades, you only need to check your hp upgrades
|
||||
elif player_potion_count == 0 or potion_offerings == 0:
|
||||
# check if you have enough hp at each paid hp offering
|
||||
for i in range(hp_offerings):
|
||||
paid_hp_count = i + 1
|
||||
if calc_effective_hp(paid_hp_count, 0, player_potion_count) > req_effective_hp:
|
||||
break
|
||||
else:
|
||||
for i in range(potion_offerings):
|
||||
paid_potion_count = i + 1
|
||||
if calc_effective_hp(free_hp, free_potion + paid_potion_count, player_potion_count) > req_effective_hp:
|
||||
break
|
||||
for j in range(hp_offerings):
|
||||
paid_hp_count = j + 1
|
||||
if (calc_effective_hp(free_hp + paid_hp_count, free_potion + paid_potion_count, player_potion_count)
|
||||
> req_effective_hp):
|
||||
# we need to pick the cheapest option that gets us above the amount of effective HP we need
|
||||
# first number is hp, second number is potion
|
||||
upgrade_options: set[tuple[int, int]] = set()
|
||||
# filter out exclusively worse options
|
||||
lowest_hp_added = hp_offerings + 1
|
||||
for paid_potion in range(0, potion_offerings + 1):
|
||||
# check quantities of hp offerings for each potion offering
|
||||
for paid_hp in range(0, lowest_hp_added):
|
||||
if (calc_effective_hp(free_hp + paid_hp, free_potion + paid_potion, player_potion_count)
|
||||
>= req_effective_hp):
|
||||
upgrade_options.add((paid_hp, paid_potion))
|
||||
lowest_hp_added = paid_hp
|
||||
break
|
||||
# hp costs 200 for the first, +50 for each additional
|
||||
money_per_hp = 200
|
||||
for _ in range(paid_hp_count):
|
||||
money_required += money_per_hp
|
||||
money_per_hp += 50
|
||||
|
||||
# potion costs 100 for the first, 300 for the second, 1,000 for the third, and +200 for each additional
|
||||
# currently we assume you will not buy past the second potion upgrade, but we might change our minds later
|
||||
money_per_potion = 100
|
||||
for _ in range(paid_potion_count):
|
||||
money_required += money_per_potion
|
||||
if money_per_potion == 100:
|
||||
money_per_potion = 300
|
||||
elif money_per_potion == 300:
|
||||
money_per_potion = 1000
|
||||
else:
|
||||
money_per_potion += 200
|
||||
costs = [calc_hp_potion_cost(hp, potion) for hp, potion in upgrade_options]
|
||||
money_required += min(costs)
|
||||
|
||||
if money_required > get_money_count(state, player):
|
||||
return False
|
||||
|
||||
return True
|
||||
return get_money_count(state, player) >= money_required
|
||||
|
||||
|
||||
# returns a tuple of your max attack level, the number of attack offerings
|
||||
@@ -336,7 +319,8 @@ def get_att_level(state: CollectionState, player: int) -> Tuple[int, int]:
|
||||
if sword_level >= 3:
|
||||
att_upgrades += min(2, sword_level - 2)
|
||||
# attack falls off, can just cap it at 8 for simplicity
|
||||
return min(8, 1 + att_offerings + att_upgrades), att_offerings
|
||||
return (min(8, 1 + att_offerings + att_upgrades)
|
||||
+ (1 if state.has("Hero's Laurels", player) else 0), att_offerings)
|
||||
|
||||
|
||||
# returns a tuple of your max defense level, the number of defense offerings
|
||||
@@ -344,7 +328,9 @@ def get_def_level(state: CollectionState, player: int) -> Tuple[int, int]:
|
||||
def_offerings = state.count("DEF Offering", player)
|
||||
# defense falls off, can just cap it at 8 for simplicity
|
||||
return (min(8, 1 + def_offerings
|
||||
+ state.count_from_list({"Hero Relic - DEF", "Secret Legend", "Phonomath"}, player)),
|
||||
+ state.count_from_list({"Hero Relic - DEF", "Secret Legend", "Phonomath"}, player))
|
||||
+ (2 if state.has("Shield", player) else 0)
|
||||
+ (2 if state.has("Hero's Laurels", player) else 0),
|
||||
def_offerings)
|
||||
|
||||
|
||||
@@ -408,6 +394,46 @@ def get_money_count(state: CollectionState, player: int) -> int:
|
||||
return money
|
||||
|
||||
|
||||
def calc_hp_potion_cost(hp_upgrades: int, potion_upgrades: int) -> int:
|
||||
money = 0
|
||||
|
||||
# hp costs 200 for the first, +50 for each additional
|
||||
money_per_hp = 200
|
||||
for _ in range(hp_upgrades):
|
||||
money += money_per_hp
|
||||
money_per_hp += 50
|
||||
|
||||
# potion costs 100 for the first, 300 for the second, 1,000 for the third, and +200 for each additional
|
||||
# currently we assume you will not buy past the second potion upgrade, but we might change our minds later
|
||||
money_per_potion = 100
|
||||
for _ in range(potion_upgrades):
|
||||
money += money_per_potion
|
||||
if money_per_potion == 100:
|
||||
money_per_potion = 300
|
||||
elif money_per_potion == 300:
|
||||
money_per_potion = 1000
|
||||
else:
|
||||
money_per_potion += 200
|
||||
|
||||
return money
|
||||
|
||||
|
||||
def calc_def_sp_cost(def_upgrades: int, sp_upgrades: int) -> int:
|
||||
money = 0
|
||||
|
||||
money_per_def = 100
|
||||
for _ in range(def_upgrades):
|
||||
money += money_per_def
|
||||
money_per_def += 50
|
||||
|
||||
money_per_sp = 200
|
||||
for _ in range(sp_upgrades):
|
||||
money += money_per_sp
|
||||
money_per_sp += 200
|
||||
|
||||
return money
|
||||
|
||||
|
||||
class TunicState(LogicMixin):
|
||||
tunic_need_to_reset_combat_from_collect: Dict[int, bool]
|
||||
tunic_need_to_reset_combat_from_remove: Dict[int, bool]
|
||||
@@ -420,3 +446,5 @@ class TunicState(LogicMixin):
|
||||
self.tunic_need_to_reset_combat_from_remove = defaultdict(lambda: False)
|
||||
# the per-player, per-area state of combat checking -- unchecked, failed, or succeeded
|
||||
self.tunic_area_combat_state = defaultdict(lambda: defaultdict(lambda: CombatState.unchecked))
|
||||
# a copy_mixin was intentionally excluded because the empty state from init_mixin
|
||||
# will always be appropriate for recalculating the logic cache
|
||||
|
||||
@@ -1386,9 +1386,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
# need to fight through the rudelings and turret, or just laurels from near the windmill
|
||||
set_rule(ow_to_well_entry,
|
||||
lambda state: state.has(laurels, player)
|
||||
or has_combat_reqs("East Forest", state, player))
|
||||
or has_combat_reqs("Before Well", state, player))
|
||||
set_rule(ow_tunnel_beach,
|
||||
lambda state: has_combat_reqs("East Forest", state, player))
|
||||
lambda state: has_combat_reqs("Before Well", state, player))
|
||||
|
||||
add_rule(atoll_statue,
|
||||
lambda state: has_combat_reqs("Ruined Atoll", state, player))
|
||||
@@ -1467,12 +1467,12 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
set_rule(cath_entry_to_elev,
|
||||
lambda state: options.entrance_rando
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
|
||||
or (has_ability(prayer, state, world) and has_combat_reqs("Cathedral", state, player)))
|
||||
or (has_ability(prayer, state, world) and has_combat_reqs("Swamp", state, player)))
|
||||
|
||||
set_rule(cath_entry_to_main,
|
||||
lambda state: has_combat_reqs("Cathedral", state, player))
|
||||
lambda state: has_combat_reqs("Swamp", state, player))
|
||||
set_rule(cath_elev_to_main,
|
||||
lambda state: has_combat_reqs("Cathedral", state, player))
|
||||
lambda state: has_combat_reqs("Swamp", state, player))
|
||||
|
||||
# for spots where you can go into and come out of an entrance to reset enemy aggro
|
||||
if world.options.entrance_rando:
|
||||
@@ -1835,10 +1835,10 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
combat_logic_to_loc("Overworld - [Northeast] Chest Above Patrol Cave", "Garden Knight", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret", "Overworld", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret 2", "Overworld")
|
||||
combat_logic_to_loc("Overworld - [Southwest] Bombable Wall Near Fountain", "East Forest", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Southwest] Fountain Holy Cross", "East Forest", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Southwest] South Chest Near Guard", "East Forest", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Southwest] Tunnel Guarded By Turret", "East Forest", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Southwest] Bombable Wall Near Fountain", "Before Well", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Southwest] Fountain Holy Cross", "Before Well", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Southwest] South Chest Near Guard", "Before Well", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Southwest] Tunnel Guarded By Turret", "Before Well", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Northwest] Chest Near Turret", "Before Well")
|
||||
|
||||
add_rule(world.get_location("Hourglass Cave - Hourglass Chest"),
|
||||
@@ -1927,4 +1927,4 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
|
||||
# zip through the rubble to sneakily grab this chest, or just fight to it
|
||||
add_rule(world.get_location("Cathedral - [1F] Near Spikes"),
|
||||
lambda state: laurels_zip(state, world) or has_combat_reqs("Cathedral", state, player))
|
||||
lambda state: laurels_zip(state, world) or has_combat_reqs("Swamp", state, player))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user