mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-22 15:45:04 -07:00
Merge branch 'ArchipelagoMW:main' into Satisfactory
This commit is contained in:
@@ -31,6 +31,7 @@ import ssl
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import kvui
|
||||
import argparse
|
||||
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
@@ -708,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):
|
||||
@@ -1048,6 +1057,32 @@ def get_base_parser(description: typing.Optional[str] = None):
|
||||
return parser
|
||||
|
||||
|
||||
def handle_url_arg(args: "argparse.Namespace",
|
||||
parser: "typing.Optional[argparse.ArgumentParser]" = None) -> "argparse.Namespace":
|
||||
"""
|
||||
Parse the url arg "archipelago://name:pass@host:port" from launcher into correct launch args for CommonClient
|
||||
If alternate data is required the urlparse response is saved back to args.url if valid
|
||||
"""
|
||||
if not args.url:
|
||||
return args
|
||||
|
||||
url = urllib.parse.urlparse(args.url)
|
||||
if url.scheme != "archipelago":
|
||||
if not parser:
|
||||
parser = get_base_parser()
|
||||
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
|
||||
return args
|
||||
|
||||
args.url = url
|
||||
args.connect = url.netloc
|
||||
if url.username:
|
||||
args.name = urllib.parse.unquote(url.username)
|
||||
if url.password:
|
||||
args.password = urllib.parse.unquote(url.password)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def run_as_textclient(*args):
|
||||
class TextContext(CommonContext):
|
||||
# Text Mode to use !hint and such with games that have no text entry
|
||||
@@ -1089,17 +1124,7 @@ def run_as_textclient(*args):
|
||||
parser.add_argument("url", nargs="?", help="Archipelago connection url")
|
||||
args = parser.parse_args(args)
|
||||
|
||||
# handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost
|
||||
if args.url:
|
||||
url = urllib.parse.urlparse(args.url)
|
||||
if url.scheme == "archipelago":
|
||||
args.connect = url.netloc
|
||||
if url.username:
|
||||
args.name = urllib.parse.unquote(url.username)
|
||||
if url.password:
|
||||
args.password = urllib.parse.unquote(url.password)
|
||||
else:
|
||||
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
|
||||
args = handle_url_arg(args, parser=parser)
|
||||
|
||||
# use colorama to display colored text highlighting on windows
|
||||
colorama.init()
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ def mystery_argparse():
|
||||
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
|
||||
parser.add_argument('--race', action='store_true', default=defaults.race)
|
||||
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
|
||||
parser.add_argument('--log_level', default='info', help='Sets log level')
|
||||
parser.add_argument('--log_level', default=defaults.loglevel, help='Sets log level')
|
||||
parser.add_argument('--log_time', help="Add timestamps to STDOUT",
|
||||
default=defaults.logtime, action='store_true')
|
||||
parser.add_argument("--csv_output", action="store_true",
|
||||
help="Output rolled player options to csv (made for async multiworld).")
|
||||
parser.add_argument("--plando", default=defaults.plando_options,
|
||||
@@ -75,7 +77,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
||||
|
||||
seed = get_seed(args.seed)
|
||||
|
||||
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
|
||||
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level, add_timestamp=args.log_time)
|
||||
random.seed(seed)
|
||||
seed_name = get_seed_name(random)
|
||||
|
||||
|
||||
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)]
|
||||
@@ -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)
|
||||
@@ -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,7 +5,8 @@ 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import tkinter as tk
|
||||
import argparse
|
||||
import logging
|
||||
import random
|
||||
import os
|
||||
import zipfile
|
||||
from itertools import chain
|
||||
@@ -197,7 +196,6 @@ def set_icon(window):
|
||||
def adjust(args):
|
||||
# Create a fake multiworld and OOTWorld to use as a base
|
||||
multiworld = MultiWorld(1)
|
||||
multiworld.per_slot_randoms = {1: random}
|
||||
ootworld = OOTWorld(multiworld, 1)
|
||||
# Set options in the fake OOTWorld
|
||||
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,13 +3,13 @@ from typing import List, Tuple
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
from ..models import Seed
|
||||
from ..models import Seed, Slot
|
||||
|
||||
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
|
||||
|
||||
|
||||
def get_players(seed: Seed) -> List[Tuple[str, str]]:
|
||||
return [(slot.player_name, slot.game) for slot in seed.slots]
|
||||
return [(slot.player_name, slot.game) for slot in seed.slots.order_by(Slot.player_id)]
|
||||
|
||||
|
||||
from . import datapackage, generate, room, user # trigger registration
|
||||
|
||||
@@ -30,4 +30,4 @@ def get_seeds():
|
||||
"creation_time": seed.creation_time,
|
||||
"players": get_players(seed.slots),
|
||||
})
|
||||
return jsonify(response)
|
||||
return jsonify(response)
|
||||
|
||||
@@ -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"], ... }]
|
||||
@@ -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)`
|
||||
@@ -835,14 +840,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]:
|
||||
|
||||
4
kvui.py
4
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")
|
||||
|
||||
10
settings.py
10
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,11 +270,15 @@ 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)
|
||||
@@ -678,6 +682,8 @@ class GeneratorOptions(Group):
|
||||
race: Race = Race(0)
|
||||
plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")
|
||||
panic_method: PanicMethod = PanicMethod("swap")
|
||||
loglevel: str = "info"
|
||||
logtime: bool = False
|
||||
|
||||
|
||||
class SNIOptions(Group):
|
||||
|
||||
@@ -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(
|
||||
|
||||
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)
|
||||
@@ -39,7 +39,7 @@ class TestImplemented(unittest.TestCase):
|
||||
"""Tests that if a world creates slot data, it's json serializable."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
# has an await for generate_output which isn't being called
|
||||
if game_name in {"Ocarina of Time", "Zillion"}:
|
||||
if game_name in {"Ocarina of Time"}:
|
||||
continue
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
with self.subTest(game=game_name, seed=multiworld.seed):
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from Options import Choice, FreeText, Toggle, Range, PerGameCommonOptions
|
||||
from Options import Choice, FreeText, ItemsAccessibility, Toggle, Range, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@@ -324,6 +324,7 @@ class KaelisMomFightsMinotaur(Toggle):
|
||||
|
||||
@dataclass
|
||||
class FFMQOptions(PerGameCommonOptions):
|
||||
accessibility: ItemsAccessibility
|
||||
logic: Logic
|
||||
brown_boxes: BrownBoxes
|
||||
sky_coin_mode: SkyCoinMode
|
||||
|
||||
@@ -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,7 +6,7 @@ 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
|
||||
@@ -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),
|
||||
@@ -919,8 +919,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]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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,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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: {}})
|
||||
|
||||
@@ -7767,8 +7767,10 @@ grass_location_name_to_id: Dict[str, int] = {name: location_base_id + 302 + inde
|
||||
|
||||
grass_location_name_groups: Dict[str, Set[str]] = {}
|
||||
for loc_name, loc_data in grass_location_table.items():
|
||||
loc_group_name = loc_name.split(" - ", 1)[0] + " Grass"
|
||||
grass_location_name_groups.setdefault(loc_group_name, set()).add(loc_name)
|
||||
area_name = loc_name.split(" - ", 1)[0]
|
||||
# adding it to the normal location group and a grass-only one
|
||||
grass_location_name_groups.setdefault(area_name, set()).add(loc_name)
|
||||
grass_location_name_groups.setdefault(area_name + " Grass", set()).add(loc_name)
|
||||
|
||||
|
||||
def can_break_grass(state: CollectionState, world: "TunicWorld") -> bool:
|
||||
|
||||
@@ -48,6 +48,7 @@ item_table: Dict[str, TunicItemData] = {
|
||||
"Gun": TunicItemData(IC.progression | IC.useful, 1, 30, "Weapons"),
|
||||
"Shield": TunicItemData(IC.useful, 1, 31, combat_ic=IC.progression | IC.useful),
|
||||
"Dath Stone": TunicItemData(IC.useful, 1, 32),
|
||||
"Torch": TunicItemData(IC.useful, 0, 156),
|
||||
"Hourglass": TunicItemData(IC.useful, 1, 33),
|
||||
"Old House Key": TunicItemData(IC.progression, 1, 34, "Keys"),
|
||||
"Key": TunicItemData(IC.progression, 2, 35, "Keys"),
|
||||
|
||||
@@ -173,7 +173,7 @@ class LocalFill(NamedRange):
|
||||
internal_name = "local_fill"
|
||||
display_name = "Local Fill Percent"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
range_end = 98
|
||||
special_range_names = {
|
||||
"default": -1
|
||||
}
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
from typing import Dict, Set
|
||||
|
||||
tunic_regions: Dict[str, Set[str]] = {
|
||||
"Menu": {"Overworld"},
|
||||
"Overworld": {"Overworld Holy Cross", "East Forest", "Dark Tomb", "Beneath the Well", "West Garden",
|
||||
tunic_regions: dict[str, tuple[str]] = {
|
||||
"Menu": ("Overworld",),
|
||||
"Overworld": ("Overworld Holy Cross", "East Forest", "Dark Tomb", "Beneath the Well", "West Garden",
|
||||
"Ruined Atoll", "Eastern Vault Fortress", "Beneath the Vault", "Quarry Back", "Quarry", "Swamp",
|
||||
"Spirit Arena"},
|
||||
"Overworld Holy Cross": set(),
|
||||
"East Forest": set(),
|
||||
"Dark Tomb": {"West Garden"},
|
||||
"Beneath the Well": set(),
|
||||
"West Garden": set(),
|
||||
"Ruined Atoll": {"Frog's Domain", "Library"},
|
||||
"Frog's Domain": set(),
|
||||
"Library": set(),
|
||||
"Eastern Vault Fortress": {"Beneath the Vault"},
|
||||
"Beneath the Vault": {"Eastern Vault Fortress"},
|
||||
"Quarry Back": {"Quarry"},
|
||||
"Quarry": {"Monastery", "Lower Quarry"},
|
||||
"Monastery": set(),
|
||||
"Lower Quarry": {"Rooted Ziggurat"},
|
||||
"Rooted Ziggurat": set(),
|
||||
"Swamp": {"Cathedral"},
|
||||
"Cathedral": set(),
|
||||
"Spirit Arena": set()
|
||||
"Spirit Arena"),
|
||||
"Overworld Holy Cross": tuple(),
|
||||
"East Forest": tuple(),
|
||||
"Dark Tomb": ("West Garden",),
|
||||
"Beneath the Well": tuple(),
|
||||
"West Garden": tuple(),
|
||||
"Ruined Atoll": ("Frog's Domain", "Library"),
|
||||
"Frog's Domain": tuple(),
|
||||
"Library": tuple(),
|
||||
"Eastern Vault Fortress": ("Beneath the Vault",),
|
||||
"Beneath the Vault": ("Eastern Vault Fortress",),
|
||||
"Quarry Back": ("Quarry",),
|
||||
"Quarry": ("Monastery", "Lower Quarry"),
|
||||
"Monastery": tuple(),
|
||||
"Lower Quarry": ("Rooted Ziggurat",),
|
||||
"Rooted Ziggurat": tuple(),
|
||||
"Swamp": ("Cathedral",),
|
||||
"Cathedral": tuple(),
|
||||
"Spirit Arena": tuple()
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ import logging
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from BaseClasses import ItemClassification, LocationProgressType, \
|
||||
MultiWorld, Item, CollectionState, Entrance, Tutorial
|
||||
from BaseClasses import LocationProgressType, MultiWorld, Item, CollectionState, Entrance, Tutorial
|
||||
|
||||
from .gen_data import GenData
|
||||
from .logic import ZillionLogicCache
|
||||
@@ -19,12 +18,13 @@ from .options import ZillionOptions, validate, z_option_groups
|
||||
from .id_maps import ZillionSlotInfo, get_slot_info, item_name_to_id as _item_name_to_id, \
|
||||
loc_name_to_id as _loc_name_to_id, make_id_to_others, \
|
||||
zz_reg_name_to_reg_name, base_id
|
||||
from .item import ZillionItem
|
||||
from .item import ZillionItem, get_classification
|
||||
from .patch import ZillionPatch
|
||||
|
||||
from zilliandomizer.system import System
|
||||
from zilliandomizer.logic_components.items import RESCUE, items as zz_items, Item as ZzItem
|
||||
from zilliandomizer.logic_components.locations import Location as ZzLocation, Req
|
||||
from zilliandomizer.map_gen.region_maker import DEAD_END_SUFFIX
|
||||
from zilliandomizer.options import Chars
|
||||
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
@@ -119,8 +119,13 @@ class ZillionWorld(World):
|
||||
"""
|
||||
my_locations: list[ZillionLocation] = []
|
||||
""" This is kind of a cache to avoid iterating through all the multiworld locations in logic. """
|
||||
slot_data_ready: threading.Event
|
||||
""" This event is set in `generate_output` when the data is ready for `fill_slot_data` """
|
||||
finalized_gen_data: GenData | None
|
||||
""" Finalized generation data needed by `generate_output` and by `fill_slot_data`. """
|
||||
item_locations_finalization_lock: threading.Lock
|
||||
"""
|
||||
This lock is used in `generate_output` and `fill_slot_data` to ensure synchronized access to `finalized_gen_data`,
|
||||
so that whichever is run first can finalize the item locations while the other waits.
|
||||
"""
|
||||
logic_cache: ZillionLogicCache | None = None
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int) -> None:
|
||||
@@ -128,7 +133,8 @@ class ZillionWorld(World):
|
||||
self.logger = logging.getLogger("Zillion")
|
||||
self.lsi = ZillionWorld.LogStreamInterface(self.logger)
|
||||
self.zz_system = System()
|
||||
self.slot_data_ready = threading.Event()
|
||||
self.finalized_gen_data = None
|
||||
self.item_locations_finalization_lock = threading.Lock()
|
||||
|
||||
def _make_item_maps(self, start_char: Chars) -> None:
|
||||
_id_to_name, _id_to_zz_id, id_to_zz_item = make_id_to_others(start_char)
|
||||
@@ -167,6 +173,7 @@ class ZillionWorld(World):
|
||||
self.logic_cache = logic_cache
|
||||
w = self.multiworld
|
||||
self.my_locations = []
|
||||
dead_end_locations: list[ZillionLocation] = []
|
||||
|
||||
self.zz_system.randomizer.place_canister_gun_reqs()
|
||||
# low probability that place_canister_gun_reqs() results in empty 1st sphere
|
||||
@@ -219,6 +226,16 @@ class ZillionWorld(World):
|
||||
here.locations.append(loc)
|
||||
self.my_locations.append(loc)
|
||||
|
||||
if ((
|
||||
zz_here.name.endswith(DEAD_END_SUFFIX)
|
||||
) or (
|
||||
(self.options.map_gen.value != self.options.map_gen.option_full) and
|
||||
(loc.name in self.options.priority_dead_ends.vanilla_dead_ends)
|
||||
) or (
|
||||
loc.name in self.options.priority_dead_ends.always_dead_ends
|
||||
)):
|
||||
dead_end_locations.append(loc)
|
||||
|
||||
for zz_dest in zz_here.connections.keys():
|
||||
dest_name = "Menu" if zz_dest.name == "start" else zz_reg_name_to_reg_name(zz_dest.name)
|
||||
dest = all_regions[dest_name]
|
||||
@@ -228,6 +245,8 @@ class ZillionWorld(World):
|
||||
|
||||
queue.append(zz_dest)
|
||||
done.add(here.name)
|
||||
if self.options.priority_dead_ends.value:
|
||||
self.options.priority_locations.value |= {loc.name for loc in dead_end_locations}
|
||||
|
||||
@override
|
||||
def create_items(self) -> None:
|
||||
@@ -305,6 +324,19 @@ class ZillionWorld(World):
|
||||
|
||||
self.zz_system.post_fill()
|
||||
|
||||
def finalize_item_locations_thread_safe(self) -> GenData:
|
||||
"""
|
||||
Call self.finalize_item_locations() and cache the result in a thread-safe manner so that either
|
||||
`generate_output` or `fill_slot_data` can finalize item locations without concern for which of the two functions
|
||||
is called first.
|
||||
"""
|
||||
# The lock is acquired when entering the context manager and released when exiting the context manager.
|
||||
with self.item_locations_finalization_lock:
|
||||
# If generation data has yet to be finalized, finalize it.
|
||||
if self.finalized_gen_data is None:
|
||||
self.finalized_gen_data = self.finalize_item_locations()
|
||||
return self.finalized_gen_data
|
||||
|
||||
def finalize_item_locations(self) -> GenData:
|
||||
"""
|
||||
sync zilliandomizer item locations with AP item locations
|
||||
@@ -363,12 +395,7 @@ class ZillionWorld(World):
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
"""This method gets called from a threadpool, do not use multiworld.random here.
|
||||
If you need any last-second randomization, use self.random instead."""
|
||||
try:
|
||||
gen_data = self.finalize_item_locations()
|
||||
except BaseException:
|
||||
raise
|
||||
finally:
|
||||
self.slot_data_ready.set()
|
||||
gen_data = self.finalize_item_locations_thread_safe()
|
||||
|
||||
out_file_base = self.multiworld.get_out_file_name_base(self.player)
|
||||
|
||||
@@ -392,9 +419,7 @@ class ZillionWorld(World):
|
||||
# TODO: tell client which canisters are keywords
|
||||
# so it can open and get those when restoring doors
|
||||
|
||||
self.slot_data_ready.wait()
|
||||
assert self.zz_system.randomizer, "didn't get randomizer from generate_early"
|
||||
game = self.zz_system.get_game()
|
||||
game = self.finalize_item_locations_thread_safe().zz_game
|
||||
return get_slot_info(game.regions, game.char_order[0], game.loc_name_2_pretty)
|
||||
|
||||
# end of ordered Main.py calls
|
||||
@@ -410,12 +435,8 @@ class ZillionWorld(World):
|
||||
self.logger.warning("warning: called `create_item` without calling `generate_early` first")
|
||||
assert self.id_to_zz_item, "failed to get item maps"
|
||||
|
||||
classification = ItemClassification.filler
|
||||
zz_item = self.id_to_zz_item[item_id]
|
||||
if zz_item.required:
|
||||
classification = ItemClassification.progression
|
||||
if not zz_item.is_progression:
|
||||
classification = ItemClassification.progression_skip_balancing
|
||||
classification = get_classification(name, zz_item, self._item_counts)
|
||||
|
||||
z_item = ZillionItem(name, classification, item_id, self.player, zz_item)
|
||||
return z_item
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
from typing import Counter
|
||||
from BaseClasses import Item, ItemClassification as IC
|
||||
from zilliandomizer.logic_components.items import Item as ZzItem
|
||||
|
||||
_useful_thresholds = {
|
||||
"Apple": 9999,
|
||||
"Champ": 9999,
|
||||
"JJ": 9999,
|
||||
"Win": 9999,
|
||||
"Empty": 0,
|
||||
"ID Card": 10,
|
||||
"Red ID Card": 2,
|
||||
"Floppy Disk": 7,
|
||||
"Bread": 0,
|
||||
"Opa-Opa": 20,
|
||||
"Zillion": 8,
|
||||
"Scope": 8,
|
||||
}
|
||||
""" make the item useful if the number in the item pool is below this number """
|
||||
|
||||
|
||||
def get_classification(name: str, zz_item: ZzItem, item_counts: Counter[str]) -> IC:
|
||||
classification = IC.filler
|
||||
if zz_item.required:
|
||||
classification = IC.progression
|
||||
if not zz_item.is_progression:
|
||||
classification = IC.progression_skip_balancing
|
||||
if item_counts[name] < _useful_thresholds.get(name, 0):
|
||||
classification |= IC.useful
|
||||
return classification
|
||||
|
||||
|
||||
class ZillionItem(Item):
|
||||
game = "Zillion"
|
||||
|
||||
@@ -272,6 +272,20 @@ class ZillionMapGen(Choice):
|
||||
return "full"
|
||||
|
||||
|
||||
class ZillionPriorityDeadEnds(DefaultOnToggle):
|
||||
"""
|
||||
Single locations that are in a dead end behind a door
|
||||
(example: vanilla Apple location)
|
||||
are prioritized for progression items.
|
||||
"""
|
||||
display_name = "priority dead ends"
|
||||
|
||||
vanilla_dead_ends: ClassVar = frozenset(("E-5 top far right", "J-4 top left"))
|
||||
""" dead ends when not generating these rooms """
|
||||
always_dead_ends: ClassVar = frozenset(("A-6 top right",))
|
||||
""" dead ends in rooms that never get generated """
|
||||
|
||||
|
||||
@dataclass
|
||||
class ZillionOptions(PerGameCommonOptions):
|
||||
continues: ZillionContinues
|
||||
@@ -293,6 +307,7 @@ class ZillionOptions(PerGameCommonOptions):
|
||||
skill: ZillionSkill
|
||||
starting_cards: ZillionStartingCards
|
||||
map_gen: ZillionMapGen
|
||||
priority_dead_ends: ZillionPriorityDeadEnds
|
||||
|
||||
room_gen: Removed
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@33045067f626266850f91c8045b9d3a9f52d02b0#0.9.0
|
||||
zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@96d9a20f8278cee64bb4db859fbd874e0f332d36#0.9.1
|
||||
typing-extensions>=4.7, <5
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from . import ZillionTestBase
|
||||
|
||||
from ..options import ZillionJumpLevels, ZillionGunLevels, ZillionOptions, validate
|
||||
from .. import ZillionWorld
|
||||
from ..options import ZillionJumpLevels, ZillionGunLevels, ZillionOptions, ZillionPriorityDeadEnds, validate
|
||||
from zilliandomizer.options import VBLR_CHOICES
|
||||
|
||||
|
||||
@@ -28,3 +29,17 @@ class OptionsTest(ZillionTestBase):
|
||||
assert getattr(zz_options, option_name) in VBLR_CHOICES
|
||||
|
||||
# TODO: test validate with invalid combinations of options
|
||||
|
||||
|
||||
class DeadEndsTest(ZillionTestBase):
|
||||
def test_vanilla_dead_end_names(self) -> None:
|
||||
z_world = self.multiworld.worlds[1]
|
||||
assert isinstance(z_world, ZillionWorld)
|
||||
for loc_name in ZillionPriorityDeadEnds.vanilla_dead_ends:
|
||||
assert any(loc.name == loc_name for loc in z_world.my_locations), f"{loc_name=} {z_world.my_locations=}"
|
||||
|
||||
def test_always_dead_end_names(self) -> None:
|
||||
z_world = self.multiworld.worlds[1]
|
||||
assert isinstance(z_world, ZillionWorld)
|
||||
for loc_name in ZillionPriorityDeadEnds.always_dead_ends:
|
||||
assert any(loc.name == loc_name for loc in z_world.my_locations), f"{loc_name=} {z_world.my_locations=}"
|
||||
|
||||
Reference in New Issue
Block a user