mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-27 16:03:23 -07:00
Compare commits
38 Commits
0.6.2-rc2
...
plando-cou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86a6939f02 | ||
|
|
51254948aa | ||
|
|
52b11083fe | ||
|
|
a8c87ce54b | ||
|
|
ddb3240591 | ||
|
|
f25ef639f2 | ||
|
|
ab7d3ce4aa | ||
|
|
50db922cef | ||
|
|
a2708edc37 | ||
|
|
603a5005e2 | ||
|
|
b4f68bce76 | ||
|
|
a76cec1539 | ||
|
|
694e6bcae3 | ||
|
|
b85b18cf5f | ||
|
|
04c707f874 | ||
|
|
99142fd662 | ||
|
|
0c5cb17d96 | ||
|
|
cabde313b5 | ||
|
|
8f68bb342d | ||
|
|
fab75d3a32 | ||
|
|
d19bf98dc4 | ||
|
|
b0f41c0360 | ||
|
|
6ebd60feaa | ||
|
|
dd6007b309 | ||
|
|
fde203379d | ||
|
|
fcb3efee01 | ||
|
|
19a21099ed | ||
|
|
20ca7e71c7 | ||
|
|
002202ff5f | ||
|
|
32487137e8 | ||
|
|
f327ab30a6 | ||
|
|
e7545cbc28 | ||
|
|
eba757d2cd | ||
|
|
4119763e23 | ||
|
|
e830a6d6f5 | ||
|
|
704cd97f21 | ||
|
|
47a0dd696f | ||
|
|
c64791e3a8 |
2
.github/workflows/label-pull-requests.yml
vendored
2
.github/workflows/label-pull-requests.yml
vendored
@@ -6,6 +6,8 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
env:
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
labeler:
|
labeler:
|
||||||
|
|||||||
7
Fill.py
7
Fill.py
@@ -937,13 +937,16 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
|
|||||||
|
|
||||||
count = block.count
|
count = block.count
|
||||||
if not count:
|
if not count:
|
||||||
count = len(new_block.items)
|
count = (min(len(new_block.items), len(new_block.resolved_locations))
|
||||||
|
if new_block.resolved_locations else len(new_block.items))
|
||||||
if isinstance(count, int):
|
if isinstance(count, int):
|
||||||
count = {"min": count, "max": count}
|
count = {"min": count, "max": count}
|
||||||
if "min" not in count:
|
if "min" not in count:
|
||||||
count["min"] = 0
|
count["min"] = 0
|
||||||
if "max" not in count:
|
if "max" not in count:
|
||||||
count["max"] = len(new_block.items)
|
count["max"] = (min(len(new_block.items), len(new_block.resolved_locations))
|
||||||
|
if new_block.resolved_locations else len(new_block.items))
|
||||||
|
|
||||||
|
|
||||||
new_block.count = count
|
new_block.count = count
|
||||||
plando_blocks[player].append(new_block)
|
plando_blocks[player].append(new_block)
|
||||||
|
|||||||
39
Launcher.py
39
Launcher.py
@@ -11,6 +11,7 @@ Additional components can be added to worlds.LauncherComponents.components.
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -41,13 +42,17 @@ def open_host_yaml():
|
|||||||
if is_linux:
|
if is_linux:
|
||||||
exe = which('sensible-editor') or which('gedit') or \
|
exe = which('sensible-editor') or which('gedit') or \
|
||||||
which('xdg-open') or which('gnome-open') or which('kde-open')
|
which('xdg-open') or which('gnome-open') or which('kde-open')
|
||||||
subprocess.Popen([exe, file])
|
|
||||||
elif is_macos:
|
elif is_macos:
|
||||||
exe = which("open")
|
exe = which("open")
|
||||||
subprocess.Popen([exe, file])
|
|
||||||
else:
|
else:
|
||||||
webbrowser.open(file)
|
webbrowser.open(file)
|
||||||
|
return
|
||||||
|
|
||||||
|
env = os.environ
|
||||||
|
if "LD_LIBRARY_PATH" in env:
|
||||||
|
env = env.copy()
|
||||||
|
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||||
|
subprocess.Popen([exe, file], env=env)
|
||||||
|
|
||||||
def open_patch():
|
def open_patch():
|
||||||
suffixes = []
|
suffixes = []
|
||||||
@@ -92,7 +97,11 @@ def open_folder(folder_path):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if exe:
|
if exe:
|
||||||
subprocess.Popen([exe, folder_path])
|
env = os.environ
|
||||||
|
if "LD_LIBRARY_PATH" in env:
|
||||||
|
env = env.copy()
|
||||||
|
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||||
|
subprocess.Popen([exe, folder_path], env=env)
|
||||||
else:
|
else:
|
||||||
logging.warning(f"No file browser available to open {folder_path}")
|
logging.warning(f"No file browser available to open {folder_path}")
|
||||||
|
|
||||||
@@ -104,14 +113,21 @@ def update_settings():
|
|||||||
|
|
||||||
components.extend([
|
components.extend([
|
||||||
# Functions
|
# Functions
|
||||||
Component("Open host.yaml", func=open_host_yaml),
|
Component("Open host.yaml", func=open_host_yaml,
|
||||||
Component("Open Patch", func=open_patch),
|
description="Open the host.yaml file to change settings for generation, games, and more."),
|
||||||
Component("Generate Template Options", func=generate_yamls),
|
Component("Open Patch", func=open_patch,
|
||||||
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")),
|
description="Open a patch file, downloaded from the room page or provided by the host."),
|
||||||
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
Component("Generate Template Options", func=generate_yamls,
|
||||||
|
description="Generate template YAMLs for currently installed games."),
|
||||||
|
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/"),
|
||||||
|
description="Open archipelago.gg in your browser."),
|
||||||
|
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2"),
|
||||||
|
description="Join the Discord server to play public multiworlds, report issues, or just chat!"),
|
||||||
Component("Unrated/18+ Discord Server", icon="discord",
|
Component("Unrated/18+ Discord Server", icon="discord",
|
||||||
func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4"),
|
||||||
Component("Browse Files", func=browse_files),
|
description="Find unrated and 18+ games in the After Dark Discord server."),
|
||||||
|
Component("Browse Files", func=browse_files,
|
||||||
|
description="Open the Archipelago installation folder in your file browser."),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@@ -180,7 +196,8 @@ def get_exe(component: str | Component) -> Sequence[str] | None:
|
|||||||
def launch(exe, in_terminal=False):
|
def launch(exe, in_terminal=False):
|
||||||
if in_terminal:
|
if in_terminal:
|
||||||
if is_windows:
|
if is_windows:
|
||||||
subprocess.Popen(['start', *exe], shell=True)
|
# intentionally using a window title with a space so it gets quoted and treated as a title
|
||||||
|
subprocess.Popen(["start", "Running Archipelago", *exe], shell=True)
|
||||||
return
|
return
|
||||||
elif is_linux:
|
elif is_linux:
|
||||||
terminal = which('x-terminal-emulator') or which('gnome-terminal') or which('xterm')
|
terminal = which('x-terminal-emulator') or which('gnome-terminal') or which('xterm')
|
||||||
|
|||||||
@@ -1524,9 +1524,11 @@ class PlandoItems(Option[typing.List[PlandoItem]]):
|
|||||||
f"dictionary, not {type(items)}")
|
f"dictionary, not {type(items)}")
|
||||||
locations = item.get("locations", [])
|
locations = item.get("locations", [])
|
||||||
if not locations:
|
if not locations:
|
||||||
locations = item.get("location", ["Everywhere"])
|
locations = item.get("location", [])
|
||||||
if locations:
|
if locations:
|
||||||
count = 1
|
count = 1
|
||||||
|
else:
|
||||||
|
locations = ["Everywhere"]
|
||||||
if isinstance(locations, str):
|
if isinstance(locations, str):
|
||||||
locations = [locations]
|
locations = [locations]
|
||||||
if not isinstance(locations, list):
|
if not isinstance(locations, list):
|
||||||
|
|||||||
36
Utils.py
36
Utils.py
@@ -226,7 +226,12 @@ def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
|
|||||||
from shutil import which
|
from shutil import which
|
||||||
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
|
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
|
||||||
assert open_command, "Didn't find program for open_file! Please report this together with system details."
|
assert open_command, "Didn't find program for open_file! Please report this together with system details."
|
||||||
subprocess.call([open_command, filename])
|
|
||||||
|
env = os.environ
|
||||||
|
if "LD_LIBRARY_PATH" in env:
|
||||||
|
env = env.copy()
|
||||||
|
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||||
|
subprocess.call([open_command, filename], env=env)
|
||||||
|
|
||||||
|
|
||||||
# from https://gist.github.com/pypt/94d747fe5180851196eb#gistcomment-4015118 with some changes
|
# from https://gist.github.com/pypt/94d747fe5180851196eb#gistcomment-4015118 with some changes
|
||||||
@@ -708,25 +713,30 @@ def _mp_open_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args:
|
|||||||
res.put(open_filename(*args))
|
res.put(open_filename(*args))
|
||||||
|
|
||||||
|
|
||||||
|
def _run_for_stdout(*args: str):
|
||||||
|
env = os.environ
|
||||||
|
if "LD_LIBRARY_PATH" in env:
|
||||||
|
env = env.copy()
|
||||||
|
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||||
|
return subprocess.run(args, capture_output=True, text=True, env=env).stdout.split("\n", 1)[0] or None
|
||||||
|
|
||||||
|
|
||||||
def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \
|
def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \
|
||||||
-> typing.Optional[str]:
|
-> typing.Optional[str]:
|
||||||
logging.info(f"Opening file input dialog for {title}.")
|
logging.info(f"Opening file input dialog for {title}.")
|
||||||
|
|
||||||
def run(*args: str):
|
|
||||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
|
||||||
|
|
||||||
if is_linux:
|
if is_linux:
|
||||||
# prefer native dialog
|
# prefer native dialog
|
||||||
from shutil import which
|
from shutil import which
|
||||||
kdialog = which("kdialog")
|
kdialog = which("kdialog")
|
||||||
if kdialog:
|
if kdialog:
|
||||||
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
|
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
|
||||||
return run(kdialog, f"--title={title}", "--getopenfilename", suggest or ".", k_filters)
|
return _run_for_stdout(kdialog, f"--title={title}", "--getopenfilename", suggest or ".", k_filters)
|
||||||
zenity = which("zenity")
|
zenity = which("zenity")
|
||||||
if zenity:
|
if zenity:
|
||||||
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
|
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
|
||||||
selection = (f"--filename={suggest}",) if suggest else ()
|
selection = (f"--filename={suggest}",) if suggest else ()
|
||||||
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
return _run_for_stdout(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
||||||
|
|
||||||
# fall back to tk
|
# fall back to tk
|
||||||
try:
|
try:
|
||||||
@@ -760,21 +770,18 @@ def _mp_open_directory(res: "multiprocessing.Queue[typing.Optional[str]]", *args
|
|||||||
|
|
||||||
|
|
||||||
def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
||||||
def run(*args: str):
|
|
||||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
|
||||||
|
|
||||||
if is_linux:
|
if is_linux:
|
||||||
# prefer native dialog
|
# prefer native dialog
|
||||||
from shutil import which
|
from shutil import which
|
||||||
kdialog = which("kdialog")
|
kdialog = which("kdialog")
|
||||||
if kdialog:
|
if kdialog:
|
||||||
return run(kdialog, f"--title={title}", "--getexistingdirectory",
|
return _run_for_stdout(kdialog, f"--title={title}", "--getexistingdirectory",
|
||||||
os.path.abspath(suggest) if suggest else ".")
|
os.path.abspath(suggest) if suggest else ".")
|
||||||
zenity = which("zenity")
|
zenity = which("zenity")
|
||||||
if zenity:
|
if zenity:
|
||||||
z_filters = ("--directory",)
|
z_filters = ("--directory",)
|
||||||
selection = (f"--filename={os.path.abspath(suggest)}/",) if suggest else ()
|
selection = (f"--filename={os.path.abspath(suggest)}/",) if suggest else ()
|
||||||
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
return _run_for_stdout(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
||||||
|
|
||||||
# fall back to tk
|
# fall back to tk
|
||||||
try:
|
try:
|
||||||
@@ -801,9 +808,6 @@ def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
|||||||
|
|
||||||
|
|
||||||
def messagebox(title: str, text: str, error: bool = False) -> None:
|
def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||||
def run(*args: str):
|
|
||||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
|
||||||
|
|
||||||
if is_kivy_running():
|
if is_kivy_running():
|
||||||
from kvui import MessageBox
|
from kvui import MessageBox
|
||||||
MessageBox(title, text, error).open()
|
MessageBox(title, text, error).open()
|
||||||
@@ -814,10 +818,10 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
|||||||
from shutil import which
|
from shutil import which
|
||||||
kdialog = which("kdialog")
|
kdialog = which("kdialog")
|
||||||
if kdialog:
|
if kdialog:
|
||||||
return run(kdialog, f"--title={title}", "--error" if error else "--msgbox", text)
|
return _run_for_stdout(kdialog, f"--title={title}", "--error" if error else "--msgbox", text)
|
||||||
zenity = which("zenity")
|
zenity = which("zenity")
|
||||||
if zenity:
|
if zenity:
|
||||||
return run(zenity, f"--title={title}", f"--text={text}", "--error" if error else "--info")
|
return _run_for_stdout(zenity, f"--title={title}", f"--text={text}", "--error" if error else "--info")
|
||||||
|
|
||||||
elif is_windows:
|
elif is_windows:
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
flask>=3.1.0
|
flask>=3.1.1
|
||||||
werkzeug>=3.1.3
|
werkzeug>=3.1.3
|
||||||
pony>=0.7.19
|
pony>=0.7.19
|
||||||
waitress>=3.0.2
|
waitress>=3.0.2
|
||||||
|
|||||||
@@ -119,9 +119,9 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
|||||||
# AP Container
|
# AP Container
|
||||||
elif handler:
|
elif handler:
|
||||||
data = zfile.open(file, "r").read()
|
data = zfile.open(file, "r").read()
|
||||||
patch = handler(BytesIO(data))
|
with zipfile.ZipFile(BytesIO(data)) as container:
|
||||||
patch.read()
|
player = json.loads(container.open("archipelago.json").read())["player"]
|
||||||
files[patch.player] = data
|
files[player] = data
|
||||||
|
|
||||||
# Spoiler
|
# Spoiler
|
||||||
elif file.filename.endswith(".txt"):
|
elif file.filename.endswith(".txt"):
|
||||||
|
|||||||
@@ -365,18 +365,14 @@ request_handlers = {
|
|||||||
["PREFERRED_CORES"] = function (req)
|
["PREFERRED_CORES"] = function (req)
|
||||||
local res = {}
|
local res = {}
|
||||||
local preferred_cores = client.getconfig().PreferredCores
|
local preferred_cores = client.getconfig().PreferredCores
|
||||||
|
local systems_enumerator = preferred_cores.Keys:GetEnumerator()
|
||||||
|
|
||||||
res["type"] = "PREFERRED_CORES_RESPONSE"
|
res["type"] = "PREFERRED_CORES_RESPONSE"
|
||||||
res["value"] = {}
|
res["value"] = {}
|
||||||
res["value"]["NES"] = preferred_cores.NES
|
|
||||||
res["value"]["SNES"] = preferred_cores.SNES
|
while systems_enumerator:MoveNext() do
|
||||||
res["value"]["GB"] = preferred_cores.GB
|
res["value"][systems_enumerator.Current] = preferred_cores[systems_enumerator.Current]
|
||||||
res["value"]["GBC"] = preferred_cores.GBC
|
end
|
||||||
res["value"]["DGB"] = preferred_cores.DGB
|
|
||||||
res["value"]["SGB"] = preferred_cores.SGB
|
|
||||||
res["value"]["PCE"] = preferred_cores.PCE
|
|
||||||
res["value"]["PCECD"] = preferred_cores.PCECD
|
|
||||||
res["value"]["SGX"] = preferred_cores.SGX
|
|
||||||
|
|
||||||
return res
|
return res
|
||||||
end,
|
end,
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ class APContainer:
|
|||||||
class APPlayerContainer(APContainer):
|
class APPlayerContainer(APContainer):
|
||||||
"""A zipfile containing at least archipelago.json meant for a player"""
|
"""A zipfile containing at least archipelago.json meant for a player"""
|
||||||
game: ClassVar[Optional[str]] = None
|
game: ClassVar[Optional[str]] = None
|
||||||
|
patch_file_ending: str = ""
|
||||||
|
|
||||||
player: Optional[int]
|
player: Optional[int]
|
||||||
player_name: str
|
player_name: str
|
||||||
@@ -184,6 +185,7 @@ class APPlayerContainer(APContainer):
|
|||||||
"player": self.player,
|
"player": self.player,
|
||||||
"player_name": self.player_name,
|
"player_name": self.player_name,
|
||||||
"game": self.game,
|
"game": self.game,
|
||||||
|
"patch_file_ending": self.patch_file_ending,
|
||||||
})
|
})
|
||||||
return manifest
|
return manifest
|
||||||
|
|
||||||
@@ -223,7 +225,6 @@ class APProcedurePatch(APAutoPatchInterface):
|
|||||||
"""
|
"""
|
||||||
hash: Optional[str] # base checksum of source file
|
hash: Optional[str] # base checksum of source file
|
||||||
source_data: bytes
|
source_data: bytes
|
||||||
patch_file_ending: str = ""
|
|
||||||
files: Dict[str, bytes]
|
files: Dict[str, bytes]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -245,7 +246,6 @@ class APProcedurePatch(APAutoPatchInterface):
|
|||||||
manifest = super(APProcedurePatch, self).get_manifest()
|
manifest = super(APProcedurePatch, self).get_manifest()
|
||||||
manifest["base_checksum"] = self.hash
|
manifest["base_checksum"] = self.hash
|
||||||
manifest["result_file_ending"] = self.result_file_ending
|
manifest["result_file_ending"] = self.result_file_ending
|
||||||
manifest["patch_file_ending"] = self.patch_file_ending
|
|
||||||
manifest["procedure"] = self.procedure
|
manifest["procedure"] = self.procedure
|
||||||
if self.procedure == APDeltaPatch.procedure:
|
if self.procedure == APDeltaPatch.procedure:
|
||||||
manifest["compatible_version"] = 5
|
manifest["compatible_version"] = 5
|
||||||
|
|||||||
@@ -210,10 +210,14 @@ components: List[Component] = [
|
|||||||
Component('Launcher', 'Launcher', component_type=Type.HIDDEN),
|
Component('Launcher', 'Launcher', component_type=Type.HIDDEN),
|
||||||
# Core
|
# Core
|
||||||
Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True,
|
Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True,
|
||||||
file_identifier=SuffixIdentifier('.archipelago', '.zip')),
|
file_identifier=SuffixIdentifier('.archipelago', '.zip'),
|
||||||
Component('Generate', 'Generate', cli=True),
|
description="Host a generated multiworld on your computer."),
|
||||||
Component("Install APWorld", func=install_apworld, file_identifier=SuffixIdentifier(".apworld")),
|
Component('Generate', 'Generate', cli=True,
|
||||||
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient),
|
description="Generate a multiworld with the YAMLs in the players folder."),
|
||||||
|
Component("Install APWorld", func=install_apworld, file_identifier=SuffixIdentifier(".apworld"),
|
||||||
|
description="Install an APWorld to play games not included with Archipelago by default."),
|
||||||
|
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient,
|
||||||
|
description="Connect to a multiworld using the text client."),
|
||||||
Component('Links Awakening DX Client', 'LinksAwakeningClient',
|
Component('Links Awakening DX Client', 'LinksAwakeningClient',
|
||||||
file_identifier=SuffixIdentifier('.apladx')),
|
file_identifier=SuffixIdentifier('.apladx')),
|
||||||
Component('LttP Adjuster', 'LttPAdjuster'),
|
Component('LttP Adjuster', 'LttPAdjuster'),
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ def launch_client(*args) -> None:
|
|||||||
|
|
||||||
|
|
||||||
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
||||||
file_identifier=SuffixIdentifier())
|
file_identifier=SuffixIdentifier(),
|
||||||
|
description="Open the BizHawk client, to play games using the Bizhawk emulator.")
|
||||||
components.append(component)
|
components.append(component)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import os
|
import os
|
||||||
import io
|
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, cast
|
from typing import TYPE_CHECKING, Dict, List, Optional, cast
|
||||||
import zipfile
|
import zipfile
|
||||||
from BaseClasses import Location
|
from BaseClasses import Location
|
||||||
from worlds.Files import APContainer, AutoPatchRegister
|
from worlds.Files import APPlayerContainer
|
||||||
|
|
||||||
from .Enum import CivVICheckType
|
from .Enum import CivVICheckType
|
||||||
from .Locations import CivVILocation, CivVILocationData
|
from .Locations import CivVILocation, CivVILocationData
|
||||||
@@ -26,22 +25,19 @@ class CivTreeItem:
|
|||||||
ui_tree_row: int
|
ui_tree_row: int
|
||||||
|
|
||||||
|
|
||||||
class CivVIContainer(APContainer, metaclass=AutoPatchRegister):
|
class CivVIContainer(APPlayerContainer):
|
||||||
"""
|
"""
|
||||||
Responsible for generating the dynamic mod files for the Civ VI multiworld
|
Responsible for generating the dynamic mod files for the Civ VI multiworld
|
||||||
"""
|
"""
|
||||||
game: Optional[str] = "Civilization VI"
|
game: Optional[str] = "Civilization VI"
|
||||||
patch_file_ending = ".apcivvi"
|
patch_file_ending = ".apcivvi"
|
||||||
|
|
||||||
def __init__(self, patch_data: Dict[str, str] | io.BytesIO, base_path: str = "", output_directory: str = "",
|
def __init__(self, patch_data: Dict[str, str], base_path: str = "", output_directory: str = "",
|
||||||
player: Optional[int] = None, player_name: str = "", server: str = ""):
|
player: Optional[int] = None, player_name: str = "", server: str = ""):
|
||||||
if isinstance(patch_data, io.BytesIO):
|
self.patch_data = patch_data
|
||||||
super().__init__(patch_data, player, player_name, server)
|
self.file_path = base_path
|
||||||
else:
|
container_path = os.path.join(output_directory, base_path + ".apcivvi")
|
||||||
self.patch_data = patch_data
|
super().__init__(container_path, player, player_name, server)
|
||||||
self.file_path = base_path
|
|
||||||
container_path = os.path.join(output_directory, base_path + ".apcivvi")
|
|
||||||
super().__init__(container_path, player, player_name, server)
|
|
||||||
|
|
||||||
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||||
for filename, yml in self.patch_data.items():
|
for filename, yml in self.patch_data.items():
|
||||||
|
|||||||
@@ -2893,3 +2893,18 @@ dog_bite_ice_trap_fix = [
|
|||||||
0x25291CB8, # ADDIU T1, T1, 0x1CB8
|
0x25291CB8, # ADDIU T1, T1, 0x1CB8
|
||||||
0x01200008 # JR T1
|
0x01200008 # JR T1
|
||||||
]
|
]
|
||||||
|
|
||||||
|
shimmy_speed_modifier = [
|
||||||
|
# Increases the player's speed while shimmying as long as they are not holding down Z. If they are holding Z, it
|
||||||
|
# will be the normal speed, allowing it to still be used to set up any tricks that might require the normal speed
|
||||||
|
# (like Left Tower Skip).
|
||||||
|
0x3C088038, # LUI T0, 0x8038
|
||||||
|
0x91087D7E, # LBU T0, 0x7D7E (T0)
|
||||||
|
0x31090020, # ANDI T1, T0, 0x0020
|
||||||
|
0x3C0A800A, # LUI T2, 0x800A
|
||||||
|
0x240B005A, # ADDIU T3, R0, 0x005A
|
||||||
|
0x55200001, # BNEZL T1, [forward 0x01]
|
||||||
|
0x240B0032, # ADDIU T3, R0, 0x0032
|
||||||
|
0xA14B3641, # SB T3, 0x3641 (T2)
|
||||||
|
0x0800B7C3 # J 0x8002DF0C
|
||||||
|
]
|
||||||
|
|||||||
@@ -424,6 +424,7 @@ class PantherDash(Choice):
|
|||||||
class IncreaseShimmySpeed(Toggle):
|
class IncreaseShimmySpeed(Toggle):
|
||||||
"""
|
"""
|
||||||
Increases the speed at which characters shimmy left and right while hanging on ledges.
|
Increases the speed at which characters shimmy left and right while hanging on ledges.
|
||||||
|
Hold Z to use the regular speed in case it's needed to do something.
|
||||||
"""
|
"""
|
||||||
display_name = "Increase Shimmy Speed"
|
display_name = "Increase Shimmy Speed"
|
||||||
|
|
||||||
|
|||||||
@@ -607,9 +607,10 @@ class CV64PatchExtensions(APPatchExtension):
|
|||||||
rom_data.write_int32(0xAA530, 0x080FF880) # J 0x803FE200
|
rom_data.write_int32(0xAA530, 0x080FF880) # J 0x803FE200
|
||||||
rom_data.write_int32s(0xBFE200, patches.coffin_cutscene_skipper)
|
rom_data.write_int32s(0xBFE200, patches.coffin_cutscene_skipper)
|
||||||
|
|
||||||
# Increase shimmy speed
|
# Shimmy speed increase hack
|
||||||
if options["increase_shimmy_speed"]:
|
if options["increase_shimmy_speed"]:
|
||||||
rom_data.write_byte(0xA4241, 0x5A)
|
rom_data.write_int32(0x97EB4, 0x803FE9F0)
|
||||||
|
rom_data.write_int32s(0xBFE9F0, patches.shimmy_speed_modifier)
|
||||||
|
|
||||||
# Disable landing fall damage
|
# Disable landing fall damage
|
||||||
if options["fall_guard"]:
|
if options["fall_guard"]:
|
||||||
|
|||||||
@@ -211,7 +211,8 @@ class CVCotMWorld(World):
|
|||||||
"ignore_cleansing": self.options.ignore_cleansing.value,
|
"ignore_cleansing": self.options.ignore_cleansing.value,
|
||||||
"skip_tutorials": self.options.skip_tutorials.value,
|
"skip_tutorials": self.options.skip_tutorials.value,
|
||||||
"required_last_keys": self.required_last_keys,
|
"required_last_keys": self.required_last_keys,
|
||||||
"completion_goal": self.options.completion_goal.value}
|
"completion_goal": self.options.completion_goal.value,
|
||||||
|
"nerf_roc_wing": self.options.nerf_roc_wing.value}
|
||||||
|
|
||||||
def get_filler_item_name(self) -> str:
|
def get_filler_item_name(self) -> str:
|
||||||
return self.random.choice(FILLER_ITEM_NAMES)
|
return self.random.choice(FILLER_ITEM_NAMES)
|
||||||
|
|||||||
@@ -48,11 +48,17 @@ class OtherGameAppearancesInfo(TypedDict):
|
|||||||
|
|
||||||
|
|
||||||
other_game_item_appearances: Dict[str, Dict[str, OtherGameAppearancesInfo]] = {
|
other_game_item_appearances: Dict[str, Dict[str, OtherGameAppearancesInfo]] = {
|
||||||
# NOTE: Symphony of the Night is currently an unsupported world not in main.
|
# NOTE: Symphony of the Night and Harmony of Dissonance are custom worlds that are not core verified.
|
||||||
"Symphony of the Night": {"Life Vessel": {"type": 0xE4,
|
"Symphony of the Night": {"Life Vessel": {"type": 0xE4,
|
||||||
"appearance": 0x01},
|
"appearance": 0x01},
|
||||||
"Heart Vessel": {"type": 0xE4,
|
"Heart Vessel": {"type": 0xE4,
|
||||||
"appearance": 0x00}},
|
"appearance": 0x00}},
|
||||||
|
|
||||||
|
"Castlevania - Harmony of Dissonance": {"Life Max Up": {"type": 0xE4,
|
||||||
|
"appearance": 0x01},
|
||||||
|
"Heart Max Up": {"type": 0xE4,
|
||||||
|
"appearance": 0x00}},
|
||||||
|
|
||||||
"Timespinner": {"Max HP": {"type": 0xE4,
|
"Timespinner": {"Max HP": {"type": 0xE4,
|
||||||
"appearance": 0x01},
|
"appearance": 0x01},
|
||||||
"Max Aura": {"type": 0xE4,
|
"Max Aura": {"type": 0xE4,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Quick Links
|
## Quick Links
|
||||||
- [Setup](/tutorial/Castlevania%20-%20Circle%20of%20the%20Moon/setup/en)
|
- [Setup](/tutorial/Castlevania%20-%20Circle%20of%20the%20Moon/setup/en)
|
||||||
- [Options Page](/games/Castlevania%20-%20Circle%20of%20the%20Moon/player-options)
|
- [Options Page](/games/Castlevania%20-%20Circle%20of%20the%20Moon/player-options)
|
||||||
- [PopTracker Pack](https://github.com/sassyvania/Circle-of-the-Moon-Rando-AP-Map-Tracker-/releases/latest)
|
- [PopTracker Pack](https://github.com/BowserCrusher/Circle-of-the-Moon-AP-Tracker/releases/latest)
|
||||||
- [Repo for the original, standalone CotMR](https://github.com/calm-palm/cotm-randomizer)
|
- [Repo for the original, standalone CotMR](https://github.com/calm-palm/cotm-randomizer)
|
||||||
- [Web version of the above randomizer](https://rando.circleofthemoon.com/)
|
- [Web version of the above randomizer](https://rando.circleofthemoon.com/)
|
||||||
- [A more in-depth guide to CotMR's nuances](https://docs.google.com/document/d/1uot4BD9XW7A--A8ecgoY8mLK_vSoQRpY5XCkzgas87c/view?usp=sharing)
|
- [A more in-depth guide to CotMR's nuances](https://docs.google.com/document/d/1uot4BD9XW7A--A8ecgoY8mLK_vSoQRpY5XCkzgas87c/view?usp=sharing)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ clear it.
|
|||||||
|
|
||||||
## Optional Software
|
## Optional Software
|
||||||
|
|
||||||
- [Castlevania: Circle of the Moon AP Tracker](https://github.com/sassyvania/Circle-of-the-Moon-Rando-AP-Map-Tracker-/releases/latest), for use with
|
- [Castlevania: Circle of the Moon AP Tracker](https://github.com/BowserCrusher/Circle-of-the-Moon-AP-Tracker/releases/latest), for use with
|
||||||
[PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
[PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
||||||
|
|
||||||
## Generating and Patching a Game
|
## Generating and Patching a Game
|
||||||
@@ -64,7 +64,7 @@ perfectly safe to make progress offline; everything will re-sync when you reconn
|
|||||||
|
|
||||||
Castlevania: Circle of the Moon has a fully functional map tracker that supports auto-tracking.
|
Castlevania: Circle of the Moon has a fully functional map tracker that supports auto-tracking.
|
||||||
|
|
||||||
1. Download [Castlevania: Circle of the Moon AP Tracker](https://github.com/sassyvania/Circle-of-the-Moon-Rando-AP-Map-Tracker-/releases/latest) and
|
1. Download [Castlevania: Circle of the Moon AP Tracker](https://github.com/BowserCrusher/Circle-of-the-Moon-AP-Tracker/releases/latest) and
|
||||||
[PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
[PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
||||||
2. Put the tracker pack into `packs/` in your PopTracker install.
|
2. Put the tracker pack into `packs/` in your PopTracker install.
|
||||||
3. Open PopTracker, and load the Castlevania: Circle of the Moon pack.
|
3. Open PopTracker, and load the Castlevania: Circle of the Moon pack.
|
||||||
|
|||||||
@@ -884,7 +884,7 @@ location_tables: Dict[str, List[DS3LocationData]] = {
|
|||||||
DS3LocationData("RS: Homeward Bone - balcony by Farron Keep", "Homeward Bone x2"),
|
DS3LocationData("RS: Homeward Bone - balcony by Farron Keep", "Homeward Bone x2"),
|
||||||
DS3LocationData("RS: Titanite Shard - woods, surrounded by enemies", "Titanite Shard"),
|
DS3LocationData("RS: Titanite Shard - woods, surrounded by enemies", "Titanite Shard"),
|
||||||
DS3LocationData("RS: Twin Dragon Greatshield - woods by Crucifixion Woods bonfire",
|
DS3LocationData("RS: Twin Dragon Greatshield - woods by Crucifixion Woods bonfire",
|
||||||
"Twin Dragon Greatshield"),
|
"Twin Dragon Greatshield", missable=True), # After Eclipse
|
||||||
DS3LocationData("RS: Sorcerer Hood - water beneath stronghold", "Sorcerer Hood",
|
DS3LocationData("RS: Sorcerer Hood - water beneath stronghold", "Sorcerer Hood",
|
||||||
hidden=True), # Hidden fall
|
hidden=True), # Hidden fall
|
||||||
DS3LocationData("RS: Sorcerer Robe - water beneath stronghold", "Sorcerer Robe",
|
DS3LocationData("RS: Sorcerer Robe - water beneath stronghold", "Sorcerer Robe",
|
||||||
@@ -1887,7 +1887,7 @@ location_tables: Dict[str, List[DS3LocationData]] = {
|
|||||||
DS3LocationData("AL: Twinkling Titanite - lizard after light cathedral #2",
|
DS3LocationData("AL: Twinkling Titanite - lizard after light cathedral #2",
|
||||||
"Twinkling Titanite", lizard=True),
|
"Twinkling Titanite", lizard=True),
|
||||||
DS3LocationData("AL: Aldrich's Ruby - dark cathedral, miniboss", "Aldrich's Ruby",
|
DS3LocationData("AL: Aldrich's Ruby - dark cathedral, miniboss", "Aldrich's Ruby",
|
||||||
miniboss=True), # Deep Accursed drop
|
miniboss=True, missable=True), # Deep Accursed drop, missable after defeating Aldrich
|
||||||
DS3LocationData("AL: Aldrich Faithful - water reserves, talk to McDonnel", "Aldrich Faithful",
|
DS3LocationData("AL: Aldrich Faithful - water reserves, talk to McDonnel", "Aldrich Faithful",
|
||||||
hidden=True), # Behind illusory wall
|
hidden=True), # Behind illusory wall
|
||||||
|
|
||||||
|
|||||||
@@ -705,7 +705,7 @@ class DarkSouls3World(World):
|
|||||||
if self._is_location_available("US: Young White Branch - by white tree #2"):
|
if self._is_location_available("US: Young White Branch - by white tree #2"):
|
||||||
self._add_item_rule(
|
self._add_item_rule(
|
||||||
"US: Young White Branch - by white tree #2",
|
"US: Young White Branch - by white tree #2",
|
||||||
lambda item: item.player == self.player and not item.data.unique
|
lambda item: item.player != self.player or not item.data.unique
|
||||||
)
|
)
|
||||||
|
|
||||||
# Make sure the Storm Ruler is available BEFORE Yhorm the Giant
|
# Make sure the Storm Ruler is available BEFORE Yhorm the Giant
|
||||||
|
|||||||
@@ -802,8 +802,10 @@ def connect_regions(world: World, level_list):
|
|||||||
for i in range(0, len(kremwood_forest_levels) - 1):
|
for i in range(0, len(kremwood_forest_levels) - 1):
|
||||||
connect(world, world.player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[i])
|
connect(world, world.player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[i])
|
||||||
|
|
||||||
connect(world, world.player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[-1],
|
connection = connect(world, world.player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[-1],
|
||||||
lambda state: (state.can_reach(LocationName.riverside_race_flag, "Location", world.player)))
|
lambda state: (state.can_reach(LocationName.riverside_race_flag, "Location", world.player)))
|
||||||
|
world.multiworld.register_indirect_condition(world.get_location(LocationName.riverside_race_flag).parent_region,
|
||||||
|
connection)
|
||||||
|
|
||||||
# Cotton-Top Cove Connections
|
# Cotton-Top Cove Connections
|
||||||
cotton_top_cove_levels = [
|
cotton_top_cove_levels = [
|
||||||
@@ -837,8 +839,11 @@ def connect_regions(world: World, level_list):
|
|||||||
connect(world, world.player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
|
connect(world, world.player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
|
||||||
lambda state: (state.has(ItemName.bowling_ball, world.player, 1)))
|
lambda state: (state.has(ItemName.bowling_ball, world.player, 1)))
|
||||||
else:
|
else:
|
||||||
connect(world, world.player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
|
connection = connect(world, world.player, names, LocationName.mekanos_region,
|
||||||
lambda state: (state.can_reach(LocationName.bleaks_house, "Location", world.player)))
|
LocationName.sky_high_secret_region,
|
||||||
|
lambda state: (state.can_reach(LocationName.bleaks_house, "Location", world.player)))
|
||||||
|
world.multiworld.register_indirect_condition(world.get_location(LocationName.bleaks_house).parent_region,
|
||||||
|
connection)
|
||||||
|
|
||||||
# K3 Connections
|
# K3 Connections
|
||||||
k3_levels = [
|
k3_levels = [
|
||||||
@@ -946,3 +951,4 @@ def connect(world: World, player: int, used_names: typing.Dict[str, int], source
|
|||||||
|
|
||||||
source_region.exits.append(connection)
|
source_region.exits.append(connection)
|
||||||
connection.connect(target_region)
|
connection.connect(target_region)
|
||||||
|
return connection
|
||||||
|
|||||||
@@ -280,16 +280,19 @@ def set_boss_door_requirements_rules(player, world):
|
|||||||
set_rule(world.get_entrance("Boss Door", player), has_3_swords)
|
set_rule(world.get_entrance("Boss Door", player), has_3_swords)
|
||||||
|
|
||||||
|
|
||||||
def set_lfod_self_obtained_items_rules(world_options, player, world):
|
def set_lfod_self_obtained_items_rules(world_options, player, multiworld):
|
||||||
if world_options.item_shuffle != Options.ItemShuffle.option_disabled:
|
if world_options.item_shuffle != Options.ItemShuffle.option_disabled:
|
||||||
return
|
return
|
||||||
set_rule(world.get_entrance("Vines", player),
|
world = multiworld.worlds[player]
|
||||||
|
set_rule(world.get_entrance("Vines"),
|
||||||
lambda state: state.has("Incredibly Important Pack", player))
|
lambda state: state.has("Incredibly Important Pack", player))
|
||||||
set_rule(world.get_entrance("Behind Rocks", player),
|
set_rule(world.get_entrance("Behind Rocks"),
|
||||||
lambda state: state.can_reach("Cut Content", 'region', player))
|
lambda state: state.can_reach("Cut Content", 'region', player))
|
||||||
set_rule(world.get_entrance("Pickaxe Hard Cave", player),
|
multiworld.register_indirect_condition(world.get_region("Cut Content"), world.get_entrance("Behind Rocks"))
|
||||||
|
set_rule(world.get_entrance("Pickaxe Hard Cave"),
|
||||||
lambda state: state.can_reach("Cut Content", 'region', player) and
|
lambda state: state.can_reach("Cut Content", 'region', player) and
|
||||||
state.has("Name Change Pack", player))
|
state.has("Name Change Pack", player))
|
||||||
|
multiworld.register_indirect_condition(world.get_region("Cut Content"), world.get_entrance("Pickaxe Hard Cave"))
|
||||||
|
|
||||||
|
|
||||||
def set_lfod_shuffled_items_rules(world_options, player, world):
|
def set_lfod_shuffled_items_rules(world_options, player, world):
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ class FactorioContext(CommonContext):
|
|||||||
# updated by spinup server
|
# updated by spinup server
|
||||||
mod_version: Version = Version(0, 0, 0)
|
mod_version: Version = Version(0, 0, 0)
|
||||||
|
|
||||||
def __init__(self, server_address, password, filter_item_sends: bool, bridge_chat_out: bool):
|
def __init__(self, server_address, password, filter_item_sends: bool, bridge_chat_out: bool,
|
||||||
|
rcon_port: int, rcon_password: str, server_settings_path: str | None,
|
||||||
|
factorio_server_args: tuple[str, ...]):
|
||||||
super(FactorioContext, self).__init__(server_address, password)
|
super(FactorioContext, self).__init__(server_address, password)
|
||||||
self.send_index: int = 0
|
self.send_index: int = 0
|
||||||
self.rcon_client = None
|
self.rcon_client = None
|
||||||
@@ -82,6 +84,10 @@ class FactorioContext(CommonContext):
|
|||||||
self.filter_item_sends: bool = filter_item_sends
|
self.filter_item_sends: bool = filter_item_sends
|
||||||
self.multiplayer: bool = False # whether multiple different players have connected
|
self.multiplayer: bool = False # whether multiple different players have connected
|
||||||
self.bridge_chat_out: bool = bridge_chat_out
|
self.bridge_chat_out: bool = bridge_chat_out
|
||||||
|
self.rcon_port: int = rcon_port
|
||||||
|
self.rcon_password: str = rcon_password
|
||||||
|
self.server_settings_path: str = server_settings_path
|
||||||
|
self.additional_factorio_server_args = factorio_server_args
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def energylink_key(self) -> str:
|
def energylink_key(self) -> str:
|
||||||
@@ -126,6 +132,18 @@ class FactorioContext(CommonContext):
|
|||||||
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
|
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
|
||||||
f"{text}")
|
f"{text}")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_args(self) -> tuple[str, ...]:
|
||||||
|
if self.server_settings_path:
|
||||||
|
return (
|
||||||
|
"--rcon-port", str(self.rcon_port),
|
||||||
|
"--rcon-password", self.rcon_password,
|
||||||
|
"--server-settings", self.server_settings_path,
|
||||||
|
*self.additional_factorio_server_args)
|
||||||
|
else:
|
||||||
|
return ("--rcon-port", str(self.rcon_port), "--rcon-password", self.rcon_password,
|
||||||
|
*self.additional_factorio_server_args)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def energy_link_status(self) -> str:
|
def energy_link_status(self) -> str:
|
||||||
if not self.energy_link_increment:
|
if not self.energy_link_increment:
|
||||||
@@ -311,7 +329,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
|||||||
executable, "--create", savegame_name, "--preset", "archipelago"
|
executable, "--create", savegame_name, "--preset", "archipelago"
|
||||||
))
|
))
|
||||||
factorio_process = subprocess.Popen((executable, "--start-server", savegame_name,
|
factorio_process = subprocess.Popen((executable, "--start-server", savegame_name,
|
||||||
*(str(elem) for elem in server_args)),
|
*ctx.server_args),
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stdin=subprocess.DEVNULL,
|
stdin=subprocess.DEVNULL,
|
||||||
@@ -331,7 +349,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
|||||||
factorio_queue.task_done()
|
factorio_queue.task_done()
|
||||||
|
|
||||||
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
||||||
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password,
|
ctx.rcon_client = factorio_rcon.RCONClient("localhost", ctx.rcon_port, ctx.rcon_password,
|
||||||
timeout=5)
|
timeout=5)
|
||||||
if not ctx.server:
|
if not ctx.server:
|
||||||
logger.info("Established bridge to Factorio Server. "
|
logger.info("Established bridge to Factorio Server. "
|
||||||
@@ -422,7 +440,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
|||||||
executable, "--create", savegame_name
|
executable, "--create", savegame_name
|
||||||
))
|
))
|
||||||
factorio_process = subprocess.Popen(
|
factorio_process = subprocess.Popen(
|
||||||
(executable, "--start-server", savegame_name, *(str(elem) for elem in server_args)),
|
(executable, "--start-server", savegame_name, *ctx.server_args),
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stdin=subprocess.DEVNULL,
|
stdin=subprocess.DEVNULL,
|
||||||
@@ -451,7 +469,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
|||||||
"or a Factorio sharing data directories is already running. "
|
"or a Factorio sharing data directories is already running. "
|
||||||
"Server could not start up.")
|
"Server could not start up.")
|
||||||
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
||||||
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
rcon_client = factorio_rcon.RCONClient("localhost", ctx.rcon_port, ctx.rcon_password)
|
||||||
if ctx.mod_version == ctx.__class__.mod_version:
|
if ctx.mod_version == ctx.__class__.mod_version:
|
||||||
raise Exception("No Archipelago mod was loaded. Aborting.")
|
raise Exception("No Archipelago mod was loaded. Aborting.")
|
||||||
await get_info(ctx, rcon_client)
|
await get_info(ctx, rcon_client)
|
||||||
@@ -474,9 +492,8 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def main(args, filter_item_sends: bool, filter_bridge_chat_out: bool):
|
async def main(make_context):
|
||||||
ctx = FactorioContext(args.connect, args.password, filter_item_sends, filter_bridge_chat_out)
|
ctx = make_context()
|
||||||
|
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||||
|
|
||||||
if gui_enabled:
|
if gui_enabled:
|
||||||
@@ -509,38 +526,42 @@ class FactorioJSONtoTextParser(JSONtoTextParser):
|
|||||||
return self._handle_text(node)
|
return self._handle_text(node)
|
||||||
|
|
||||||
|
|
||||||
parser = get_base_parser(description="Optional arguments to FactorioClient follow. "
|
|
||||||
"Remaining arguments get passed into bound Factorio instance."
|
|
||||||
"Refer to Factorio --help for those.")
|
|
||||||
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
|
|
||||||
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
|
|
||||||
parser.add_argument('--server-settings', help='Factorio server settings configuration file.')
|
|
||||||
|
|
||||||
args, rest = parser.parse_known_args()
|
|
||||||
rcon_port = args.rcon_port
|
|
||||||
rcon_password = args.rcon_password if args.rcon_password else ''.join(
|
|
||||||
random.choice(string.ascii_letters) for x in range(32))
|
|
||||||
factorio_server_logger = logging.getLogger("FactorioServer")
|
factorio_server_logger = logging.getLogger("FactorioServer")
|
||||||
settings: FactorioSettings = get_settings().factorio_options
|
settings: FactorioSettings = get_settings().factorio_options
|
||||||
if os.path.samefile(settings.executable, sys.executable):
|
if os.path.samefile(settings.executable, sys.executable):
|
||||||
selected_executable = settings.executable
|
selected_executable = settings.executable
|
||||||
settings.executable = FactorioSettings.executable # reset to default
|
settings.executable = FactorioSettings.executable # reset to default
|
||||||
raise Exception(f"FactorioClient was set to run itself {selected_executable}, aborting process bomb.")
|
raise Exception(f"Factorio Client was set to run itself {selected_executable}, aborting process bomb.")
|
||||||
|
|
||||||
executable = settings.executable
|
executable = settings.executable
|
||||||
|
|
||||||
server_settings = args.server_settings if args.server_settings \
|
|
||||||
else getattr(settings, "server_settings", None)
|
|
||||||
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password)
|
|
||||||
|
|
||||||
|
def launch(*new_args: str):
|
||||||
def launch():
|
|
||||||
import colorama
|
import colorama
|
||||||
global executable, server_settings, server_args
|
global executable
|
||||||
colorama.just_fix_windows_console()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
|
# args handling
|
||||||
|
parser = get_base_parser(description="Optional arguments to Factorio Client follow. "
|
||||||
|
"Remaining arguments get passed into bound Factorio instance."
|
||||||
|
"Refer to Factorio --help for those.")
|
||||||
|
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
|
||||||
|
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
|
||||||
|
parser.add_argument('--server-settings', help='Factorio server settings configuration file.')
|
||||||
|
|
||||||
|
args, rest = parser.parse_known_args(args=new_args)
|
||||||
|
rcon_port = args.rcon_port
|
||||||
|
rcon_password = args.rcon_password if args.rcon_password else ''.join(
|
||||||
|
random.choice(string.ascii_letters) for _ in range(32))
|
||||||
|
|
||||||
|
server_settings = args.server_settings if args.server_settings \
|
||||||
|
else getattr(settings, "server_settings", None)
|
||||||
|
|
||||||
if server_settings:
|
if server_settings:
|
||||||
server_settings = os.path.abspath(server_settings)
|
server_settings = os.path.abspath(server_settings)
|
||||||
|
if not os.path.isfile(server_settings):
|
||||||
|
raise FileNotFoundError(f"Could not find file {server_settings} for server_settings. Aborting.")
|
||||||
|
|
||||||
initial_filter_item_sends = bool(settings.filter_item_sends)
|
initial_filter_item_sends = bool(settings.filter_item_sends)
|
||||||
initial_bridge_chat_out = bool(settings.bridge_chat_out)
|
initial_bridge_chat_out = bool(settings.bridge_chat_out)
|
||||||
|
|
||||||
@@ -554,14 +575,9 @@ def launch():
|
|||||||
else:
|
else:
|
||||||
raise FileNotFoundError(f"Path {executable} is not an executable file.")
|
raise FileNotFoundError(f"Path {executable} is not an executable file.")
|
||||||
|
|
||||||
if server_settings and os.path.isfile(server_settings):
|
asyncio.run(main(lambda: FactorioContext(
|
||||||
server_args = (
|
args.connect, args.password,
|
||||||
"--rcon-port", rcon_port,
|
initial_filter_item_sends, initial_bridge_chat_out,
|
||||||
"--rcon-password", rcon_password,
|
rcon_port, rcon_password, server_settings, rest
|
||||||
"--server-settings", server_settings,
|
)))
|
||||||
*rest)
|
|
||||||
else:
|
|
||||||
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest)
|
|
||||||
|
|
||||||
asyncio.run(main(args, initial_filter_item_sends, initial_bridge_chat_out))
|
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class FactorioModFile(worlds.Files.APPlayerContainer):
|
|||||||
game = "Factorio"
|
game = "Factorio"
|
||||||
compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives
|
compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives
|
||||||
writing_tasks: List[Callable[[], Tuple[str, Union[str, bytes]]]]
|
writing_tasks: List[Callable[[], Tuple[str, Union[str, bytes]]]]
|
||||||
|
patch_file_ending = ".zip"
|
||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any):
|
def __init__(self, *args: Any, **kwargs: Any):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
|
|||||||
from .settings import FactorioSettings
|
from .settings import FactorioSettings
|
||||||
|
|
||||||
|
|
||||||
def launch_client():
|
def launch_client(*args: str):
|
||||||
from .Client import launch
|
from .Client import launch
|
||||||
launch_component(launch, name="FactorioClient")
|
launch_component(launch, name="Factorio Client", args=args)
|
||||||
|
|
||||||
|
|
||||||
components.append(Component("Factorio Client", func=launch_client, component_type=Type.CLIENT))
|
components.append(Component("Factorio Client", func=launch_client, component_type=Type.CLIENT))
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ from .locations import (JakAndDaxterLocation,
|
|||||||
cache_location_table,
|
cache_location_table,
|
||||||
orb_location_table)
|
orb_location_table)
|
||||||
from .regions import create_regions
|
from .regions import create_regions
|
||||||
from .rules import (enforce_multiplayer_limits,
|
from .rules import (enforce_mp_absolute_limits,
|
||||||
enforce_singleplayer_limits,
|
enforce_mp_friendly_limits,
|
||||||
verify_orb_trade_amounts,
|
enforce_sp_limits,
|
||||||
set_orb_trade_rule)
|
set_orb_trade_rule)
|
||||||
from .locs import (cell_locations as cells,
|
from .locs import (cell_locations as cells,
|
||||||
scout_locations as scouts,
|
scout_locations as scouts,
|
||||||
@@ -258,18 +258,31 @@ class JakAndDaxterWorld(World):
|
|||||||
self.options.mountain_pass_cell_count.value = self.power_cell_thresholds[1]
|
self.options.mountain_pass_cell_count.value = self.power_cell_thresholds[1]
|
||||||
self.options.lava_tube_cell_count.value = self.power_cell_thresholds[2]
|
self.options.lava_tube_cell_count.value = self.power_cell_thresholds[2]
|
||||||
|
|
||||||
# Store this for remove function.
|
# We would have done this earlier, but we needed to sort the power cell thresholds first. Don't worry, we'll
|
||||||
self.power_cell_thresholds_minus_one = [x - 1 for x in self.power_cell_thresholds]
|
# come back to them.
|
||||||
|
|
||||||
# For the fairness of other players in a multiworld game, enforce some friendly limitations on our options,
|
|
||||||
# so we don't cause chaos during seed generation. These friendly limits should **guarantee** a successful gen.
|
|
||||||
# We would have done this earlier, but we needed to sort the power cell thresholds first.
|
|
||||||
enforce_friendly_options = self.settings.enforce_friendly_options
|
enforce_friendly_options = self.settings.enforce_friendly_options
|
||||||
if enforce_friendly_options:
|
if self.multiworld.players == 1:
|
||||||
if self.multiworld.players > 1:
|
# For singleplayer games, always enforce/clamp the cell counts to valid values.
|
||||||
enforce_multiplayer_limits(self)
|
enforce_sp_limits(self)
|
||||||
|
else:
|
||||||
|
if enforce_friendly_options:
|
||||||
|
# For multiplayer games, we have a host setting to make options fair/sane for other players.
|
||||||
|
# If this setting is enabled, enforce/clamp some friendly limitations on our options.
|
||||||
|
enforce_mp_friendly_limits(self)
|
||||||
else:
|
else:
|
||||||
enforce_singleplayer_limits(self)
|
# Even if the setting is disabled, some values must be clamped to avoid generation errors.
|
||||||
|
enforce_mp_absolute_limits(self)
|
||||||
|
|
||||||
|
# That's right, set the collection of thresholds again. Don't just clamp the values without updating this list!
|
||||||
|
self.power_cell_thresholds = [
|
||||||
|
self.options.fire_canyon_cell_count.value,
|
||||||
|
self.options.mountain_pass_cell_count.value,
|
||||||
|
self.options.lava_tube_cell_count.value,
|
||||||
|
100, # The 100 Power Cell Door.
|
||||||
|
]
|
||||||
|
|
||||||
|
# Now that the threshold list is finalized, store this for the remove function.
|
||||||
|
self.power_cell_thresholds_minus_one = [x - 1 for x in self.power_cell_thresholds]
|
||||||
|
|
||||||
# Calculate the number of power cells needed for full region access, the number being replaced by traps,
|
# Calculate the number of power cells needed for full region access, the number being replaced by traps,
|
||||||
# and the number of remaining filler.
|
# and the number of remaining filler.
|
||||||
@@ -282,11 +295,6 @@ class JakAndDaxterWorld(World):
|
|||||||
self.options.filler_power_cells_replaced_with_traps.value = self.total_trap_cells
|
self.options.filler_power_cells_replaced_with_traps.value = self.total_trap_cells
|
||||||
self.total_filler_cells = non_prog_cells - self.total_trap_cells
|
self.total_filler_cells = non_prog_cells - self.total_trap_cells
|
||||||
|
|
||||||
# Verify that we didn't overload the trade amounts with more orbs than exist in the world.
|
|
||||||
# This is easy to do by accident even in a singleplayer world.
|
|
||||||
self.total_trade_orbs = (9 * self.options.citizen_orb_trade_amount) + (6 * self.options.oracle_orb_trade_amount)
|
|
||||||
verify_orb_trade_amounts(self)
|
|
||||||
|
|
||||||
# Cache the orb bundle size and item name for quicker reference.
|
# Cache the orb bundle size and item name for quicker reference.
|
||||||
if self.options.enable_orbsanity == options.EnableOrbsanity.option_per_level:
|
if self.options.enable_orbsanity == options.EnableOrbsanity.option_per_level:
|
||||||
self.orb_bundle_size = self.options.level_orbsanity_bundle_size.value
|
self.orb_bundle_size = self.options.level_orbsanity_bundle_size.value
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
- [What do Traps do?](#what-do-traps-do)
|
- [What do Traps do?](#what-do-traps-do)
|
||||||
- [What kind of Traps are there?](#what-kind-of-traps-are-there)
|
- [What kind of Traps are there?](#what-kind-of-traps-are-there)
|
||||||
- [I got soft-locked and cannot leave, how do I get out of here?](#i-got-soft-locked-and-cannot-leave-how-do-i-get-out-of-here)
|
- [I got soft-locked and cannot leave, how do I get out of here?](#i-got-soft-locked-and-cannot-leave-how-do-i-get-out-of-here)
|
||||||
- [Why did I get an Option Error when generating a seed, and how do I fix it?](#why-did-i-get-an-option-error-when-generating-a-seed-and-how-do-i-fix-it)
|
- [How do I generate seeds with 1 Orb Orbsanity and other extreme options?](#how-do-i-generate-seeds-with-1-orb-orbsanity-and-other-extreme-options)
|
||||||
- [How do I check my player options in-game?](#how-do-i-check-my-player-options-in-game)
|
- [How do I check my player options in-game?](#how-do-i-check-my-player-options-in-game)
|
||||||
- [How does the HUD work?](#how-does-the-hud-work)
|
- [How does the HUD work?](#how-does-the-hud-work)
|
||||||
- [I think I found a bug, where should I report it?](#i-think-i-found-a-bug-where-should-i-report-it)
|
- [I think I found a bug, where should I report it?](#i-think-i-found-a-bug-where-should-i-report-it)
|
||||||
@@ -201,16 +201,19 @@ Open the game's menu, navigate to `Options`, then `Archipelago Options`, then `W
|
|||||||
Selecting this option will ask if you want to be teleported to Geyser Rock. From there, you can teleport back
|
Selecting this option will ask if you want to be teleported to Geyser Rock. From there, you can teleport back
|
||||||
to the nearest sage's hut to continue your journey.
|
to the nearest sage's hut to continue your journey.
|
||||||
|
|
||||||
## Why did I get an Option Error when generating a seed and how do I fix it
|
## How do I generate seeds with 1 orb orbsanity and other extreme options?
|
||||||
Depending on your player YAML, Jak and Daxter can have a lot of items, which can sometimes be overwhelming or
|
Depending on your player YAML, Jak and Daxter can have a lot of items, which can sometimes be overwhelming or
|
||||||
disruptive to multiworld games. There are also options that are mutually incompatible with each other, even in a solo
|
disruptive to multiworld games. There are also options that are mutually incompatible with each other, even in a solo
|
||||||
game. To prevent the game from disrupting multiworlds, or generating an impossible solo seed, some options have
|
game. To prevent the game from disrupting multiworlds, or generating an impossible solo seed, some options have
|
||||||
Singleplayer and Multiplayer Minimums and Maximums, collectively called "friendly limits."
|
"friendly limits" that prevent you from choosing more extreme values.
|
||||||
|
|
||||||
If you're generating a solo game, or your multiworld host agrees to your request, you can override those limits by
|
You can override **some**, not all, of those limits by editing the `host.yaml`. In the Archipelago Launcher, click
|
||||||
editing the `host.yaml`. In the Archipelago Launcher, click `Open host.yaml`, then search for `jakanddaxter_options`,
|
`Open host.yaml`, then search for `jakanddaxter_options`, then search for `enforce_friendly_options`, then change this
|
||||||
then search for `enforce_friendly_options`, then change this value from `true` to `false`. Disabling this allows for
|
value from `true` to `false`. You can then generate a seed locally, and upload that to the Archipelago website to host
|
||||||
more disruptive and challenging options, but it may cause seed generation to fail. **Use at your own risk!**
|
for you (or host it yourself).
|
||||||
|
|
||||||
|
**Remember:** disabling this setting allows for more disruptive and challenging options, but it may cause seed
|
||||||
|
generation to fail. **Use at your own risk!**
|
||||||
|
|
||||||
## How do I check my player options in-game
|
## How do I check my player options in-game
|
||||||
When you connect your text client to the Archipelago Server, the server will tell the game what options were chosen
|
When you connect your text client to the Archipelago Server, the server will tell the game what options were chosen
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
- A legally purchased copy of *Jak And Daxter: The Precursor Legacy.*
|
- A legally purchased copy of *Jak And Daxter: The Precursor Legacy.*
|
||||||
- [The OpenGOAL Launcher](https://opengoal.dev/)
|
- [The OpenGOAL Launcher](https://opengoal.dev/)
|
||||||
- [The Jak and Daxter .APWORLD package](https://github.com/ArchipelaGOAL/Archipelago/releases)
|
|
||||||
|
|
||||||
At this time, this method of setup works on Windows only, but Linux support is a strong likelihood in the near future as OpenGOAL itself supports Linux.
|
At this time, this method of setup works on Windows only, but Linux support is a strong likelihood in the near future as OpenGOAL itself supports Linux.
|
||||||
|
|
||||||
@@ -75,7 +74,7 @@ If you are in the middle of an async game, and you do not want to update the mod
|
|||||||
### New Game
|
### New Game
|
||||||
|
|
||||||
- Run the Archipelago Launcher.
|
- Run the Archipelago Launcher.
|
||||||
- From the right-most list, find and click `Jak and Daxter Client`.
|
- From the client list, find and click `Jak and Daxter Client`.
|
||||||
- 3 new windows should appear:
|
- 3 new windows should appear:
|
||||||
- The OpenGOAL compiler will launch and compile the game. They should take about 30 seconds to compile.
|
- The OpenGOAL compiler will launch and compile the game. They should take about 30 seconds to compile.
|
||||||
- You should hear a musical cue to indicate the compilation was a success. If you do not, see the Troubleshooting section.
|
- You should hear a musical cue to indicate the compilation was a success. If you do not, see the Troubleshooting section.
|
||||||
|
|||||||
@@ -1,22 +1,78 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from Options import PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Range, DefaultOnToggle, OptionCounter
|
from Options import PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Range, DefaultOnToggle, OptionCounter, \
|
||||||
|
AssembleOptions
|
||||||
from .items import trap_item_table
|
from .items import trap_item_table
|
||||||
|
|
||||||
|
|
||||||
class StaticGetter:
|
class readonly_classproperty:
|
||||||
def __init__(self, func):
|
"""This decorator is used for getting friendly or unfriendly range_end values for options like FireCanyonCellCount
|
||||||
self.fget = func
|
and CitizenOrbTradeAmount. We only need to provide a getter as we will only be setting a single int to one of two
|
||||||
|
values."""
|
||||||
|
def __init__(self, getter):
|
||||||
|
self.getter = getter
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
def __get__(self, instance, owner):
|
||||||
return self.fget(owner)
|
return self.getter(owner)
|
||||||
|
|
||||||
|
|
||||||
@StaticGetter
|
@readonly_classproperty
|
||||||
def determine_range_end(cls) -> int:
|
def determine_range_end(cls) -> int:
|
||||||
from . import JakAndDaxterWorld
|
from . import JakAndDaxterWorld # Avoid circular imports.
|
||||||
enforce_friendly_options = JakAndDaxterWorld.settings.enforce_friendly_options
|
friendly = JakAndDaxterWorld.settings.enforce_friendly_options
|
||||||
return cls.friendly_maximum if enforce_friendly_options else cls.absolute_maximum
|
return cls.friendly_maximum if friendly else cls.absolute_maximum
|
||||||
|
|
||||||
|
|
||||||
|
class classproperty:
|
||||||
|
"""This decorator (?) is used for getting and setting friendly or unfriendly option values for the Orbsanity
|
||||||
|
options."""
|
||||||
|
def __init__(self, getter, setter):
|
||||||
|
self.getter = getter
|
||||||
|
self.setter = setter
|
||||||
|
|
||||||
|
def __get__(self, obj, value):
|
||||||
|
return self.getter(obj)
|
||||||
|
|
||||||
|
def __set__(self, obj, value):
|
||||||
|
self.setter(obj, value)
|
||||||
|
|
||||||
|
|
||||||
|
class AllowedChoiceMeta(AssembleOptions):
|
||||||
|
"""This metaclass overrides AssembleOptions and provides inheriting classes a way to filter out "disallowed" values
|
||||||
|
by way of implementing get_disallowed_options. This function is used by Jak and Daxter to check host.yaml settings
|
||||||
|
without circular imports or breaking the settings API."""
|
||||||
|
_name_lookup: dict[int, str]
|
||||||
|
_options: dict[str, int]
|
||||||
|
|
||||||
|
def __new__(mcs, name, bases, attrs):
|
||||||
|
ret = super().__new__(mcs, name, bases, attrs)
|
||||||
|
ret._name_lookup = attrs["name_lookup"]
|
||||||
|
ret._options = attrs["options"]
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def set_name_lookup(cls, value : dict[int, str]):
|
||||||
|
cls._name_lookup = value
|
||||||
|
|
||||||
|
def get_name_lookup(cls) -> dict[int, str]:
|
||||||
|
cls._name_lookup = {k: v for k, v in cls._name_lookup.items() if k not in cls.get_disallowed_options()}
|
||||||
|
return cls._name_lookup
|
||||||
|
|
||||||
|
def set_options(cls, value: dict[str, int]):
|
||||||
|
cls._options = value
|
||||||
|
|
||||||
|
def get_options(cls) -> dict[str, int]:
|
||||||
|
cls._options = {k: v for k, v in cls._options.items() if v not in cls.get_disallowed_options()}
|
||||||
|
return cls._options
|
||||||
|
|
||||||
|
def get_disallowed_options(cls):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
name_lookup = classproperty(get_name_lookup, set_name_lookup)
|
||||||
|
options = classproperty(get_options, set_options)
|
||||||
|
|
||||||
|
|
||||||
|
class AllowedChoice(Choice, metaclass=AllowedChoiceMeta):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class EnableMoveRandomizer(Toggle):
|
class EnableMoveRandomizer(Toggle):
|
||||||
@@ -44,12 +100,13 @@ class EnableOrbsanity(Choice):
|
|||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
class GlobalOrbsanityBundleSize(Choice):
|
class GlobalOrbsanityBundleSize(AllowedChoice):
|
||||||
"""The orb bundle size for Global Orbsanity. This only applies if "Enable Orbsanity" is set to "Global."
|
"""The orb bundle size for Global Orbsanity. This only applies if "Enable Orbsanity" is set to "Global."
|
||||||
There are 2000 orbs in the game, so your bundle size must be a factor of 2000.
|
There are 2000 orbs in the game, so your bundle size must be a factor of 2000.
|
||||||
|
|
||||||
Multiplayer Minimum: 10
|
This value is restricted to safe minimum and maximum values to ensure valid singleplayer games and
|
||||||
Multiplayer Maximum: 200"""
|
non-disruptive multiplayer games, but the host can remove this restriction by turning off enforce_friendly_options
|
||||||
|
in host.yaml."""
|
||||||
display_name = "Global Orbsanity Bundle Size"
|
display_name = "Global Orbsanity Bundle Size"
|
||||||
option_1_orb = 1
|
option_1_orb = 1
|
||||||
option_2_orbs = 2
|
option_2_orbs = 2
|
||||||
@@ -75,12 +132,33 @@ class GlobalOrbsanityBundleSize(Choice):
|
|||||||
friendly_maximum = 200
|
friendly_maximum = 200
|
||||||
default = 20
|
default = 20
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_disallowed_options(cls) -> set[int]:
|
||||||
|
try:
|
||||||
|
from . import JakAndDaxterWorld
|
||||||
|
if JakAndDaxterWorld.settings.enforce_friendly_options:
|
||||||
|
return {cls.option_1_orb,
|
||||||
|
cls.option_2_orbs,
|
||||||
|
cls.option_4_orbs,
|
||||||
|
cls.option_5_orbs,
|
||||||
|
cls.option_8_orbs,
|
||||||
|
cls.option_250_orbs,
|
||||||
|
cls.option_400_orbs,
|
||||||
|
cls.option_500_orbs,
|
||||||
|
cls.option_1000_orbs,
|
||||||
|
cls.option_2000_orbs}
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
return set()
|
||||||
|
|
||||||
class PerLevelOrbsanityBundleSize(Choice):
|
|
||||||
|
class PerLevelOrbsanityBundleSize(AllowedChoice):
|
||||||
"""The orb bundle size for Per Level Orbsanity. This only applies if "Enable Orbsanity" is set to "Per Level."
|
"""The orb bundle size for Per Level Orbsanity. This only applies if "Enable Orbsanity" is set to "Per Level."
|
||||||
There are 50, 150, or 200 orbs per level, so your bundle size must be a factor of 50.
|
There are 50, 150, or 200 orbs per level, so your bundle size must be a factor of 50.
|
||||||
|
|
||||||
Multiplayer Minimum: 10"""
|
This value is restricted to safe minimum and maximum values to ensure valid singleplayer games and
|
||||||
|
non-disruptive multiplayer games, but the host can remove this restriction by turning off enforce_friendly_options
|
||||||
|
in host.yaml."""
|
||||||
display_name = "Per Level Orbsanity Bundle Size"
|
display_name = "Per Level Orbsanity Bundle Size"
|
||||||
option_1_orb = 1
|
option_1_orb = 1
|
||||||
option_2_orbs = 2
|
option_2_orbs = 2
|
||||||
@@ -91,6 +169,18 @@ class PerLevelOrbsanityBundleSize(Choice):
|
|||||||
friendly_minimum = 10
|
friendly_minimum = 10
|
||||||
default = 25
|
default = 25
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_disallowed_options(cls) -> set[int]:
|
||||||
|
try:
|
||||||
|
from . import JakAndDaxterWorld
|
||||||
|
if JakAndDaxterWorld.settings.enforce_friendly_options:
|
||||||
|
return {cls.option_1_orb,
|
||||||
|
cls.option_2_orbs,
|
||||||
|
cls.option_5_orbs}
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
class FireCanyonCellCount(Range):
|
class FireCanyonCellCount(Range):
|
||||||
"""The number of power cells you need to cross Fire Canyon. This value is restricted to a safe maximum value to
|
"""The number of power cells you need to cross Fire Canyon. This value is restricted to a safe maximum value to
|
||||||
@@ -234,7 +324,7 @@ class CompletionCondition(Choice):
|
|||||||
option_cross_fire_canyon = 69
|
option_cross_fire_canyon = 69
|
||||||
option_cross_mountain_pass = 87
|
option_cross_mountain_pass = 87
|
||||||
option_cross_lava_tube = 89
|
option_cross_lava_tube = 89
|
||||||
option_defeat_dark_eco_plant = 6
|
# option_defeat_dark_eco_plant = 6
|
||||||
option_defeat_klaww = 86
|
option_defeat_klaww = 86
|
||||||
option_defeat_gol_and_maia = 112
|
option_defeat_gol_and_maia = 112
|
||||||
option_open_100_cell_door = 116
|
option_open_100_cell_door = 116
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ def create_regions(world: "JakAndDaxterWorld"):
|
|||||||
elif options.jak_completion_condition == CompletionCondition.option_cross_lava_tube:
|
elif options.jak_completion_condition == CompletionCondition.option_cross_lava_tube:
|
||||||
multiworld.completion_condition[player] = lambda state: state.can_reach(gmc, "Region", player)
|
multiworld.completion_condition[player] = lambda state: state.can_reach(gmc, "Region", player)
|
||||||
|
|
||||||
elif options.jak_completion_condition == CompletionCondition.option_defeat_dark_eco_plant:
|
# elif options.jak_completion_condition == CompletionCondition.option_defeat_dark_eco_plant:
|
||||||
multiworld.completion_condition[player] = lambda state: state.can_reach(fjp, "Region", player)
|
# multiworld.completion_condition[player] = lambda state: state.can_reach(fjp, "Region", player)
|
||||||
|
|
||||||
elif options.jak_completion_condition == CompletionCondition.option_defeat_klaww:
|
elif options.jak_completion_condition == CompletionCondition.option_defeat_klaww:
|
||||||
multiworld.completion_condition[player] = lambda state: state.can_reach(mp, "Region", player)
|
multiworld.completion_condition[player] = lambda state: state.can_reach(mp, "Region", player)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import logging
|
||||||
|
import math
|
||||||
import typing
|
import typing
|
||||||
from BaseClasses import CollectionState
|
from BaseClasses import CollectionState
|
||||||
from Options import OptionError
|
from Options import OptionError
|
||||||
@@ -131,100 +133,138 @@ def can_fight(state: CollectionState, player: int) -> bool:
|
|||||||
return state.has_any(("Jump Dive", "Jump Kick", "Punch", "Kick"), player)
|
return state.has_any(("Jump Dive", "Jump Kick", "Punch", "Kick"), player)
|
||||||
|
|
||||||
|
|
||||||
def enforce_multiplayer_limits(world: "JakAndDaxterWorld"):
|
def clamp_cell_limits(world: "JakAndDaxterWorld") -> str:
|
||||||
options = world.options
|
options = world.options
|
||||||
friendly_message = ""
|
friendly_message = ""
|
||||||
|
|
||||||
if (options.enable_orbsanity == EnableOrbsanity.option_global
|
|
||||||
and (options.global_orbsanity_bundle_size.value < GlobalOrbsanityBundleSize.friendly_minimum
|
|
||||||
or options.global_orbsanity_bundle_size.value > GlobalOrbsanityBundleSize.friendly_maximum)):
|
|
||||||
friendly_message += (f" "
|
|
||||||
f"{options.global_orbsanity_bundle_size.display_name} must be no less than "
|
|
||||||
f"{GlobalOrbsanityBundleSize.friendly_minimum} and no greater than "
|
|
||||||
f"{GlobalOrbsanityBundleSize.friendly_maximum} (currently "
|
|
||||||
f"{options.global_orbsanity_bundle_size.value}).\n")
|
|
||||||
|
|
||||||
if (options.enable_orbsanity == EnableOrbsanity.option_per_level
|
|
||||||
and options.level_orbsanity_bundle_size.value < PerLevelOrbsanityBundleSize.friendly_minimum):
|
|
||||||
friendly_message += (f" "
|
|
||||||
f"{options.level_orbsanity_bundle_size.display_name} must be no less than "
|
|
||||||
f"{PerLevelOrbsanityBundleSize.friendly_minimum} (currently "
|
|
||||||
f"{options.level_orbsanity_bundle_size.value}).\n")
|
|
||||||
|
|
||||||
if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum:
|
if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum:
|
||||||
|
old_value = options.fire_canyon_cell_count.value
|
||||||
|
options.fire_canyon_cell_count.value = FireCanyonCellCount.friendly_maximum
|
||||||
friendly_message += (f" "
|
friendly_message += (f" "
|
||||||
f"{options.fire_canyon_cell_count.display_name} must be no greater than "
|
f"{options.fire_canyon_cell_count.display_name} must be no greater than "
|
||||||
f"{FireCanyonCellCount.friendly_maximum} (currently "
|
f"{FireCanyonCellCount.friendly_maximum} (was {old_value}), "
|
||||||
f"{options.fire_canyon_cell_count.value}).\n")
|
f"changed option to appropriate value.\n")
|
||||||
|
|
||||||
if options.mountain_pass_cell_count.value > MountainPassCellCount.friendly_maximum:
|
if options.mountain_pass_cell_count.value > MountainPassCellCount.friendly_maximum:
|
||||||
|
old_value = options.mountain_pass_cell_count.value
|
||||||
|
options.mountain_pass_cell_count.value = MountainPassCellCount.friendly_maximum
|
||||||
friendly_message += (f" "
|
friendly_message += (f" "
|
||||||
f"{options.mountain_pass_cell_count.display_name} must be no greater than "
|
f"{options.mountain_pass_cell_count.display_name} must be no greater than "
|
||||||
f"{MountainPassCellCount.friendly_maximum} (currently "
|
f"{MountainPassCellCount.friendly_maximum} (was {old_value}), "
|
||||||
f"{options.mountain_pass_cell_count.value}).\n")
|
f"changed option to appropriate value.\n")
|
||||||
|
|
||||||
if options.lava_tube_cell_count.value > LavaTubeCellCount.friendly_maximum:
|
if options.lava_tube_cell_count.value > LavaTubeCellCount.friendly_maximum:
|
||||||
|
old_value = options.lava_tube_cell_count.value
|
||||||
|
options.lava_tube_cell_count.value = LavaTubeCellCount.friendly_maximum
|
||||||
friendly_message += (f" "
|
friendly_message += (f" "
|
||||||
f"{options.lava_tube_cell_count.display_name} must be no greater than "
|
f"{options.lava_tube_cell_count.display_name} must be no greater than "
|
||||||
f"{LavaTubeCellCount.friendly_maximum} (currently "
|
f"{LavaTubeCellCount.friendly_maximum} (was {old_value}), "
|
||||||
f"{options.lava_tube_cell_count.value}).\n")
|
f"changed option to appropriate value.\n")
|
||||||
|
|
||||||
|
return friendly_message
|
||||||
|
|
||||||
|
|
||||||
|
def clamp_trade_total_limits(world: "JakAndDaxterWorld"):
|
||||||
|
"""Check if we need to recalculate the 2 trade orb options so the total fits under 2000. If so let's keep them
|
||||||
|
proportional relative to each other. Then we'll recalculate total_trade_orbs. Remember this situation is
|
||||||
|
only possible if both values are greater than 0, otherwise the absolute maximums would keep them under 2000."""
|
||||||
|
options = world.options
|
||||||
|
friendly_message = ""
|
||||||
|
|
||||||
|
world.total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount)
|
||||||
|
if world.total_trade_orbs > 2000:
|
||||||
|
old_total = world.total_trade_orbs
|
||||||
|
old_citizen_value = options.citizen_orb_trade_amount.value
|
||||||
|
old_oracle_value = options.oracle_orb_trade_amount.value
|
||||||
|
|
||||||
|
coefficient = old_oracle_value / old_citizen_value
|
||||||
|
|
||||||
|
options.citizen_orb_trade_amount.value = math.floor(2000 / (9 + (6 * coefficient)))
|
||||||
|
options.oracle_orb_trade_amount.value = math.floor(coefficient * options.citizen_orb_trade_amount.value)
|
||||||
|
world.total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount)
|
||||||
|
|
||||||
|
friendly_message += (f" "
|
||||||
|
f"Required number of orbs ({old_total}) must be no greater than total orbs in the game "
|
||||||
|
f"(2000). Reduced the value of {world.options.citizen_orb_trade_amount.display_name} "
|
||||||
|
f"from {old_citizen_value} to {options.citizen_orb_trade_amount.value} and "
|
||||||
|
f"{world.options.oracle_orb_trade_amount.display_name} from {old_oracle_value} to "
|
||||||
|
f"{options.oracle_orb_trade_amount.value}.\n")
|
||||||
|
|
||||||
|
return friendly_message
|
||||||
|
|
||||||
|
|
||||||
|
def enforce_mp_friendly_limits(world: "JakAndDaxterWorld"):
|
||||||
|
options = world.options
|
||||||
|
friendly_message = ""
|
||||||
|
|
||||||
|
if options.enable_orbsanity == EnableOrbsanity.option_global:
|
||||||
|
if options.global_orbsanity_bundle_size.value < GlobalOrbsanityBundleSize.friendly_minimum:
|
||||||
|
old_value = options.global_orbsanity_bundle_size.value
|
||||||
|
options.global_orbsanity_bundle_size.value = GlobalOrbsanityBundleSize.friendly_minimum
|
||||||
|
friendly_message += (f" "
|
||||||
|
f"{options.global_orbsanity_bundle_size.display_name} must be no less than "
|
||||||
|
f"{GlobalOrbsanityBundleSize.friendly_minimum} (was {old_value}), "
|
||||||
|
f"changed option to appropriate value.\n")
|
||||||
|
|
||||||
|
if options.global_orbsanity_bundle_size.value > GlobalOrbsanityBundleSize.friendly_maximum:
|
||||||
|
old_value = options.global_orbsanity_bundle_size.value
|
||||||
|
options.global_orbsanity_bundle_size.value = GlobalOrbsanityBundleSize.friendly_maximum
|
||||||
|
friendly_message += (f" "
|
||||||
|
f"{options.global_orbsanity_bundle_size.display_name} must be no greater than "
|
||||||
|
f"{GlobalOrbsanityBundleSize.friendly_maximum} (was {old_value}), "
|
||||||
|
f"changed option to appropriate value.\n")
|
||||||
|
|
||||||
|
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||||
|
if options.level_orbsanity_bundle_size.value < PerLevelOrbsanityBundleSize.friendly_minimum:
|
||||||
|
old_value = options.level_orbsanity_bundle_size.value
|
||||||
|
options.level_orbsanity_bundle_size.value = PerLevelOrbsanityBundleSize.friendly_minimum
|
||||||
|
friendly_message += (f" "
|
||||||
|
f"{options.level_orbsanity_bundle_size.display_name} must be no less than "
|
||||||
|
f"{PerLevelOrbsanityBundleSize.friendly_minimum} (was {old_value}), "
|
||||||
|
f"changed option to appropriate value.\n")
|
||||||
|
|
||||||
if options.citizen_orb_trade_amount.value > CitizenOrbTradeAmount.friendly_maximum:
|
if options.citizen_orb_trade_amount.value > CitizenOrbTradeAmount.friendly_maximum:
|
||||||
|
old_value = options.citizen_orb_trade_amount.value
|
||||||
|
options.citizen_orb_trade_amount.value = CitizenOrbTradeAmount.friendly_maximum
|
||||||
friendly_message += (f" "
|
friendly_message += (f" "
|
||||||
f"{options.citizen_orb_trade_amount.display_name} must be no greater than "
|
f"{options.citizen_orb_trade_amount.display_name} must be no greater than "
|
||||||
f"{CitizenOrbTradeAmount.friendly_maximum} (currently "
|
f"{CitizenOrbTradeAmount.friendly_maximum} (was {old_value}), "
|
||||||
f"{options.citizen_orb_trade_amount.value}).\n")
|
f"changed option to appropriate value.\n")
|
||||||
|
|
||||||
if options.oracle_orb_trade_amount.value > OracleOrbTradeAmount.friendly_maximum:
|
if options.oracle_orb_trade_amount.value > OracleOrbTradeAmount.friendly_maximum:
|
||||||
|
old_value = options.oracle_orb_trade_amount.value
|
||||||
|
options.oracle_orb_trade_amount.value = OracleOrbTradeAmount.friendly_maximum
|
||||||
friendly_message += (f" "
|
friendly_message += (f" "
|
||||||
f"{options.oracle_orb_trade_amount.display_name} must be no greater than "
|
f"{options.oracle_orb_trade_amount.display_name} must be no greater than "
|
||||||
f"{OracleOrbTradeAmount.friendly_maximum} (currently "
|
f"{OracleOrbTradeAmount.friendly_maximum} (was {old_value}), "
|
||||||
f"{options.oracle_orb_trade_amount.value}).\n")
|
f"changed option to appropriate value.\n")
|
||||||
|
|
||||||
|
friendly_message += clamp_cell_limits(world)
|
||||||
|
friendly_message += clamp_trade_total_limits(world)
|
||||||
|
|
||||||
if friendly_message != "":
|
if friendly_message != "":
|
||||||
raise OptionError(f"{world.player_name}: The options you have chosen may disrupt the multiworld. \n"
|
logging.warning(f"{world.player_name}: Your options have been modified to avoid disrupting the multiworld.\n"
|
||||||
f"Please adjust the following Options for a multiplayer game. \n"
|
f"{friendly_message}"
|
||||||
f"{friendly_message}"
|
f"You can access more advanced options by setting 'enforce_friendly_options' in the seed "
|
||||||
f"Or use 'random-range-x-y' instead of 'random' in your player yaml.\n"
|
f"generator's host.yaml to false and generating locally. (Use at your own risk!)")
|
||||||
f"Or set 'enforce_friendly_options' in the seed generator's host.yaml to false. "
|
|
||||||
f"(Use at your own risk!)")
|
|
||||||
|
|
||||||
|
|
||||||
def enforce_singleplayer_limits(world: "JakAndDaxterWorld"):
|
def enforce_mp_absolute_limits(world: "JakAndDaxterWorld"):
|
||||||
options = world.options
|
|
||||||
friendly_message = ""
|
friendly_message = ""
|
||||||
|
|
||||||
if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum:
|
friendly_message += clamp_trade_total_limits(world)
|
||||||
friendly_message += (f" "
|
|
||||||
f"{options.fire_canyon_cell_count.display_name} must be no greater than "
|
|
||||||
f"{FireCanyonCellCount.friendly_maximum} (currently "
|
|
||||||
f"{options.fire_canyon_cell_count.value}).\n")
|
|
||||||
|
|
||||||
if options.mountain_pass_cell_count.value > MountainPassCellCount.friendly_maximum:
|
|
||||||
friendly_message += (f" "
|
|
||||||
f"{options.mountain_pass_cell_count.display_name} must be no greater than "
|
|
||||||
f"{MountainPassCellCount.friendly_maximum} (currently "
|
|
||||||
f"{options.mountain_pass_cell_count.value}).\n")
|
|
||||||
|
|
||||||
if options.lava_tube_cell_count.value > LavaTubeCellCount.friendly_maximum:
|
|
||||||
friendly_message += (f" "
|
|
||||||
f"{options.lava_tube_cell_count.display_name} must be no greater than "
|
|
||||||
f"{LavaTubeCellCount.friendly_maximum} (currently "
|
|
||||||
f"{options.lava_tube_cell_count.value}).\n")
|
|
||||||
|
|
||||||
if friendly_message != "":
|
if friendly_message != "":
|
||||||
raise OptionError(f"The options you have chosen may result in seed generation failures. \n"
|
logging.warning(f"{world.player_name}: Your options have been modified to avoid seed generation failures.\n"
|
||||||
f"Please adjust the following Options for a singleplayer game. \n"
|
f"{friendly_message}")
|
||||||
f"{friendly_message}"
|
|
||||||
f"Or use 'random-range-x-y' instead of 'random' in your player yaml.\n"
|
|
||||||
f"Or set 'enforce_friendly_options' in your host.yaml to false. "
|
|
||||||
f"(Use at your own risk!)")
|
|
||||||
|
|
||||||
|
|
||||||
def verify_orb_trade_amounts(world: "JakAndDaxterWorld"):
|
def enforce_sp_limits(world: "JakAndDaxterWorld"):
|
||||||
|
friendly_message = ""
|
||||||
|
|
||||||
if world.total_trade_orbs > 2000:
|
friendly_message += clamp_cell_limits(world)
|
||||||
raise OptionError(f"{world.player_name}: Required number of orbs for all trades ({world.total_trade_orbs}) "
|
friendly_message += clamp_trade_total_limits(world)
|
||||||
f"is more than all the orbs in the game (2000). Reduce the value of either "
|
|
||||||
f"{world.options.citizen_orb_trade_amount.display_name} "
|
if friendly_message != "":
|
||||||
f"or {world.options.oracle_orb_trade_amount.display_name}.")
|
logging.warning(f"{world.player_name}: Your options have been modified to avoid seed generation failures.\n"
|
||||||
|
f"{friendly_message}")
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ from .bases import JakAndDaxterTestBase
|
|||||||
class TradesCostNothingTest(JakAndDaxterTestBase):
|
class TradesCostNothingTest(JakAndDaxterTestBase):
|
||||||
options = {
|
options = {
|
||||||
"enable_orbsanity": 2,
|
"enable_orbsanity": 2,
|
||||||
"global_orbsanity_bundle_size": 5,
|
"global_orbsanity_bundle_size": 10,
|
||||||
"citizen_orb_trade_amount": 0,
|
"citizen_orb_trade_amount": 0,
|
||||||
"oracle_orb_trade_amount": 0
|
"oracle_orb_trade_amount": 0
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_orb_items_are_filler(self):
|
def test_orb_items_are_filler(self):
|
||||||
self.collect_all_but("")
|
self.collect_all_but("")
|
||||||
self.assertNotIn("5 Precursor Orbs", self.multiworld.state.prog_items)
|
self.assertNotIn("10 Precursor Orbs", self.multiworld.state.prog_items)
|
||||||
|
|
||||||
def test_trades_are_accessible(self):
|
def test_trades_are_accessible(self):
|
||||||
self.assertTrue(self.multiworld
|
self.assertTrue(self.multiworld
|
||||||
@@ -22,15 +22,15 @@ class TradesCostNothingTest(JakAndDaxterTestBase):
|
|||||||
class TradesCostEverythingTest(JakAndDaxterTestBase):
|
class TradesCostEverythingTest(JakAndDaxterTestBase):
|
||||||
options = {
|
options = {
|
||||||
"enable_orbsanity": 2,
|
"enable_orbsanity": 2,
|
||||||
"global_orbsanity_bundle_size": 5,
|
"global_orbsanity_bundle_size": 10,
|
||||||
"citizen_orb_trade_amount": 120,
|
"citizen_orb_trade_amount": 120,
|
||||||
"oracle_orb_trade_amount": 150
|
"oracle_orb_trade_amount": 150
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_orb_items_are_progression(self):
|
def test_orb_items_are_progression(self):
|
||||||
self.collect_all_but("")
|
self.collect_all_but("")
|
||||||
self.assertIn("5 Precursor Orbs", self.multiworld.state.prog_items[self.player])
|
self.assertIn("10 Precursor Orbs", self.multiworld.state.prog_items[self.player])
|
||||||
self.assertEqual(396, self.multiworld.state.prog_items[self.player]["5 Precursor Orbs"])
|
self.assertEqual(198, self.multiworld.state.prog_items[self.player]["10 Precursor Orbs"])
|
||||||
|
|
||||||
def test_trades_are_accessible(self):
|
def test_trades_are_accessible(self):
|
||||||
self.collect_all_but("")
|
self.collect_all_but("")
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class KH2Context(CommonContext):
|
|||||||
self.growthlevel = None
|
self.growthlevel = None
|
||||||
self.kh2connected = False
|
self.kh2connected = False
|
||||||
self.kh2_finished_game = False
|
self.kh2_finished_game = False
|
||||||
self.serverconneced = False
|
self.serverconnected = False
|
||||||
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
|
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
|
||||||
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
|
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
|
||||||
self.kh2_data_package = {}
|
self.kh2_data_package = {}
|
||||||
@@ -47,6 +47,8 @@ class KH2Context(CommonContext):
|
|||||||
self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()}
|
self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()}
|
||||||
|
|
||||||
self.sending = []
|
self.sending = []
|
||||||
|
self.slot_name = None
|
||||||
|
self.disconnect_from_server = False
|
||||||
# list used to keep track of locations+items player has. Used for disoneccting
|
# list used to keep track of locations+items player has. Used for disoneccting
|
||||||
self.kh2_seed_save_cache = {
|
self.kh2_seed_save_cache = {
|
||||||
"itemIndex": -1,
|
"itemIndex": -1,
|
||||||
@@ -185,11 +187,20 @@ class KH2Context(CommonContext):
|
|||||||
if password_requested and not self.password:
|
if password_requested and not self.password:
|
||||||
await super(KH2Context, self).server_auth(password_requested)
|
await super(KH2Context, self).server_auth(password_requested)
|
||||||
await self.get_username()
|
await self.get_username()
|
||||||
await self.send_connect()
|
# if slot name != first time login or previous name
|
||||||
|
# and seed name is none or saved seed name
|
||||||
|
if not self.slot_name and not self.kh2seedname:
|
||||||
|
await self.send_connect()
|
||||||
|
elif self.slot_name == self.auth and self.kh2seedname:
|
||||||
|
await self.send_connect()
|
||||||
|
else:
|
||||||
|
logger.info(f"You are trying to connect with data still cached in the client. Close client or connect to the correct slot: {self.slot_name}")
|
||||||
|
self.serverconnected = False
|
||||||
|
self.disconnect_from_server = True
|
||||||
|
|
||||||
async def connection_closed(self):
|
async def connection_closed(self):
|
||||||
self.kh2connected = False
|
self.kh2connected = False
|
||||||
self.serverconneced = False
|
self.serverconnected = False
|
||||||
if self.kh2seedname is not None and self.auth is not None:
|
if self.kh2seedname is not None and self.auth is not None:
|
||||||
with open(self.kh2_seed_save_path_join, 'w') as f:
|
with open(self.kh2_seed_save_path_join, 'w') as f:
|
||||||
f.write(json.dumps(self.kh2_seed_save, indent=4))
|
f.write(json.dumps(self.kh2_seed_save, indent=4))
|
||||||
@@ -197,7 +208,8 @@ class KH2Context(CommonContext):
|
|||||||
|
|
||||||
async def disconnect(self, allow_autoreconnect: bool = False):
|
async def disconnect(self, allow_autoreconnect: bool = False):
|
||||||
self.kh2connected = False
|
self.kh2connected = False
|
||||||
self.serverconneced = False
|
self.serverconnected = False
|
||||||
|
self.locations_checked = []
|
||||||
if self.kh2seedname not in {None} and self.auth not in {None}:
|
if self.kh2seedname not in {None} and self.auth not in {None}:
|
||||||
with open(self.kh2_seed_save_path_join, 'w') as f:
|
with open(self.kh2_seed_save_path_join, 'w') as f:
|
||||||
f.write(json.dumps(self.kh2_seed_save, indent=4))
|
f.write(json.dumps(self.kh2_seed_save, indent=4))
|
||||||
@@ -239,7 +251,15 @@ class KH2Context(CommonContext):
|
|||||||
|
|
||||||
def on_package(self, cmd: str, args: dict):
|
def on_package(self, cmd: str, args: dict):
|
||||||
if cmd == "RoomInfo":
|
if cmd == "RoomInfo":
|
||||||
self.kh2seedname = args['seed_name']
|
if not self.kh2seedname:
|
||||||
|
self.kh2seedname = args['seed_name']
|
||||||
|
elif self.kh2seedname != args['seed_name']:
|
||||||
|
self.disconnect_from_server = True
|
||||||
|
self.serverconnected = False
|
||||||
|
self.kh2connected = False
|
||||||
|
logger.info("Connection to the wrong seed, connect to the correct seed or close the client.")
|
||||||
|
return
|
||||||
|
|
||||||
self.kh2_seed_save_path = f"kh2save2{self.kh2seedname}{self.auth}.json"
|
self.kh2_seed_save_path = f"kh2save2{self.kh2seedname}{self.auth}.json"
|
||||||
self.kh2_seed_save_path_join = os.path.join(self.game_communication_path, self.kh2_seed_save_path)
|
self.kh2_seed_save_path_join = os.path.join(self.game_communication_path, self.kh2_seed_save_path)
|
||||||
|
|
||||||
@@ -338,7 +358,7 @@ class KH2Context(CommonContext):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if start_index > self.kh2_seed_save_cache["itemIndex"] and self.serverconneced:
|
if start_index > self.kh2_seed_save_cache["itemIndex"] and self.serverconnected:
|
||||||
self.kh2_seed_save_cache["itemIndex"] = start_index
|
self.kh2_seed_save_cache["itemIndex"] = start_index
|
||||||
for item in args['items']:
|
for item in args['items']:
|
||||||
asyncio.create_task(self.give_item(item.item, item.location))
|
asyncio.create_task(self.give_item(item.item, item.location))
|
||||||
@@ -370,12 +390,14 @@ class KH2Context(CommonContext):
|
|||||||
if not self.kh2:
|
if not self.kh2:
|
||||||
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||||
self.get_addresses()
|
self.get_addresses()
|
||||||
|
#
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.kh2connected:
|
if self.kh2connected:
|
||||||
self.kh2connected = False
|
self.kh2connected = False
|
||||||
logger.info("Game is not open.")
|
logger.info("Game is not open.")
|
||||||
self.serverconneced = True
|
|
||||||
|
self.serverconnected = True
|
||||||
|
self.slot_name = self.auth
|
||||||
|
|
||||||
def data_package_kh2_cache(self, loc_to_id, item_to_id):
|
def data_package_kh2_cache(self, loc_to_id, item_to_id):
|
||||||
self.kh2_loc_name_to_id = loc_to_id
|
self.kh2_loc_name_to_id = loc_to_id
|
||||||
@@ -493,23 +515,38 @@ class KH2Context(CommonContext):
|
|||||||
|
|
||||||
async def give_item(self, item, location):
|
async def give_item(self, item, location):
|
||||||
try:
|
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
|
||||||
#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:
|
while not self.lookup_id_to_item:
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
itemname = self.lookup_id_to_item[item]
|
itemname = self.lookup_id_to_item[item]
|
||||||
itemdata = self.item_name_to_data[itemname]
|
itemdata = self.item_name_to_data[itemname]
|
||||||
# itemcode = self.kh2_item_name_to_id[itemname]
|
|
||||||
if itemdata.ability:
|
if itemdata.ability:
|
||||||
if location in self.all_weapon_location_id:
|
if location in self.all_weapon_location_id:
|
||||||
return
|
return
|
||||||
|
# growth have reserved ability slots because of how the goa handles them
|
||||||
if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}:
|
if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}:
|
||||||
self.kh2_seed_save_cache["AmountInvo"]["Growth"][itemname] += 1
|
self.kh2_seed_save_cache["AmountInvo"]["Growth"][itemname] += 1
|
||||||
return
|
return
|
||||||
|
|
||||||
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]:
|
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]:
|
||||||
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname] = []
|
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname] = []
|
||||||
# appending the slot that the ability should be in
|
# appending the slot that the ability should be in
|
||||||
|
# abilities have a limit amount of slots.
|
||||||
|
# we start from the back going down to not mess with stuff.
|
||||||
|
# Front of Invo
|
||||||
|
# Sora: Save+24F0+0x54 : 0x2546
|
||||||
|
# Donald: Save+2604+0x54 : 0x2658
|
||||||
|
# Goofy: Save+2718+0x54 : 0x276C
|
||||||
|
# Back of Invo. Sora has 6 ability slots that are reserved
|
||||||
|
# Sora: Save+24F0+0x54+0x92 : 0x25D8
|
||||||
|
# Donald: Save+2604+0x54+0x9C : 0x26F4
|
||||||
|
# Goofy: Save+2718+0x54+0x9C : 0x2808
|
||||||
|
# seed has 2 scans in sora's abilities
|
||||||
|
# recieved second scan
|
||||||
|
# if len(seed_save(Scan:[ability slot 52]) < (2)amount of that ability they should have from slot data
|
||||||
|
# ability_slot = back of inventory that isnt taken
|
||||||
|
# add ability_slot to seed_save(Scan[]) so now its Scan:[ability slot 52,50]
|
||||||
|
# decrease back of inventory since its ability_slot is already taken
|
||||||
if len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \
|
if len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \
|
||||||
self.AbilityQuantityDict[itemname]:
|
self.AbilityQuantityDict[itemname]:
|
||||||
if itemname in self.sora_ability_set:
|
if itemname in self.sora_ability_set:
|
||||||
@@ -528,18 +565,21 @@ class KH2Context(CommonContext):
|
|||||||
if ability_slot in self.front_ability_slots:
|
if ability_slot in self.front_ability_slots:
|
||||||
self.front_ability_slots.remove(ability_slot)
|
self.front_ability_slots.remove(ability_slot)
|
||||||
|
|
||||||
|
# if itemdata in {bitmask} all the forms,summons and a few other things are bitmasks
|
||||||
elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}:
|
elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}:
|
||||||
# if memaddr is in a bitmask location in memory
|
# if memaddr is in a bitmask location in memory
|
||||||
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]:
|
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]:
|
||||||
self.kh2_seed_save_cache["AmountInvo"]["Bitmask"].append(itemname)
|
self.kh2_seed_save_cache["AmountInvo"]["Bitmask"].append(itemname)
|
||||||
|
|
||||||
|
# if itemdata in {magic}
|
||||||
elif itemdata.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}:
|
elif itemdata.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}:
|
||||||
# if memaddr is in magic addresses
|
|
||||||
self.kh2_seed_save_cache["AmountInvo"]["Magic"][itemname] += 1
|
self.kh2_seed_save_cache["AmountInvo"]["Magic"][itemname] += 1
|
||||||
|
|
||||||
|
# equipment is a list instead of dict because you can only have 1 currently
|
||||||
elif itemname in self.all_equipment:
|
elif itemname in self.all_equipment:
|
||||||
self.kh2_seed_save_cache["AmountInvo"]["Equipment"].append(itemname)
|
self.kh2_seed_save_cache["AmountInvo"]["Equipment"].append(itemname)
|
||||||
|
|
||||||
|
# weapons are done differently since you can only have one and has to check it differently
|
||||||
elif itemname in self.all_weapons:
|
elif itemname in self.all_weapons:
|
||||||
if itemname in self.keyblade_set:
|
if itemname in self.keyblade_set:
|
||||||
self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Sora"].append(itemname)
|
self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Sora"].append(itemname)
|
||||||
@@ -548,9 +588,11 @@ class KH2Context(CommonContext):
|
|||||||
else:
|
else:
|
||||||
self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Goofy"].append(itemname)
|
self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Goofy"].append(itemname)
|
||||||
|
|
||||||
|
# TODO: this can just be removed and put into the else below it
|
||||||
elif itemname in self.stat_increase_set:
|
elif itemname in self.stat_increase_set:
|
||||||
self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][itemname] += 1
|
self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][itemname] += 1
|
||||||
else:
|
else:
|
||||||
|
# "normal" items. They have a unique byte reserved for how many they have
|
||||||
if itemname in self.kh2_seed_save_cache["AmountInvo"]["Amount"]:
|
if itemname in self.kh2_seed_save_cache["AmountInvo"]["Amount"]:
|
||||||
self.kh2_seed_save_cache["AmountInvo"]["Amount"][itemname] += 1
|
self.kh2_seed_save_cache["AmountInvo"]["Amount"][itemname] += 1
|
||||||
else:
|
else:
|
||||||
@@ -930,7 +972,7 @@ def finishedGame(ctx: KH2Context):
|
|||||||
async def kh2_watcher(ctx: KH2Context):
|
async def kh2_watcher(ctx: KH2Context):
|
||||||
while not ctx.exit_event.is_set():
|
while not ctx.exit_event.is_set():
|
||||||
try:
|
try:
|
||||||
if ctx.kh2connected and ctx.serverconneced:
|
if ctx.kh2connected and ctx.serverconnected:
|
||||||
ctx.sending = []
|
ctx.sending = []
|
||||||
await asyncio.create_task(ctx.checkWorldLocations())
|
await asyncio.create_task(ctx.checkWorldLocations())
|
||||||
await asyncio.create_task(ctx.checkLevels())
|
await asyncio.create_task(ctx.checkLevels())
|
||||||
@@ -944,13 +986,19 @@ async def kh2_watcher(ctx: KH2Context):
|
|||||||
if ctx.sending:
|
if ctx.sending:
|
||||||
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
|
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
|
||||||
await ctx.send_msgs(message)
|
await ctx.send_msgs(message)
|
||||||
elif not ctx.kh2connected and ctx.serverconneced:
|
elif not ctx.kh2connected and ctx.serverconnected:
|
||||||
logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.")
|
logger.info("Game Connection lost. trying to reconnect.")
|
||||||
ctx.kh2 = None
|
ctx.kh2 = None
|
||||||
while not ctx.kh2connected and ctx.serverconneced:
|
while not ctx.kh2connected and ctx.serverconnected:
|
||||||
await asyncio.sleep(15)
|
try:
|
||||||
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||||
ctx.get_addresses()
|
ctx.get_addresses()
|
||||||
|
logger.info("Game Connection Established.")
|
||||||
|
except Exception as e:
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
if ctx.disconnect_from_server:
|
||||||
|
ctx.disconnect_from_server = False
|
||||||
|
await ctx.disconnect()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if ctx.kh2connected:
|
if ctx.kh2connected:
|
||||||
ctx.kh2connected = False
|
ctx.kh2connected = False
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from worlds.Files import APPlayerContainer
|
|||||||
|
|
||||||
class KH2Container(APPlayerContainer):
|
class KH2Container(APPlayerContainer):
|
||||||
game: str = 'Kingdom Hearts 2'
|
game: str = 'Kingdom Hearts 2'
|
||||||
|
patch_file_ending = ".zip"
|
||||||
|
|
||||||
def __init__(self, patch_data: dict, base_path: str, output_directory: str,
|
def __init__(self, patch_data: dict, base_path: str, output_directory: str,
|
||||||
player=None, player_name: str = "", server: str = ""):
|
player=None, player_name: str = "", server: str = ""):
|
||||||
|
|||||||
@@ -277,9 +277,7 @@ class KH2World(World):
|
|||||||
if self.options.FillerItemsLocal:
|
if self.options.FillerItemsLocal:
|
||||||
for item in filler_items:
|
for item in filler_items:
|
||||||
self.options.local_items.value.add(item)
|
self.options.local_items.value.add(item)
|
||||||
# By imitating remote this doesn't have to be plandoded filler anymore
|
|
||||||
# for location in {LocationName.JunkMedal, LocationName.JunkMedal}:
|
|
||||||
# self.plando_locations[location] = random_stt_item
|
|
||||||
if not self.options.SummonLevelLocationToggle:
|
if not self.options.SummonLevelLocationToggle:
|
||||||
self.total_locations -= 6
|
self.total_locations -= 6
|
||||||
|
|
||||||
@@ -400,6 +398,8 @@ class KH2World(World):
|
|||||||
# plando goofy get bonuses
|
# plando goofy get bonuses
|
||||||
goofy_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in
|
goofy_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in
|
||||||
Goofy_Checks.keys() if Goofy_Checks[location].yml != "Keyblade"]
|
Goofy_Checks.keys() if Goofy_Checks[location].yml != "Keyblade"]
|
||||||
|
if len(goofy_get_bonus_location_pool) > len(self.goofy_get_bonus_abilities):
|
||||||
|
raise Exception(f"Too little abilities to fill goofy get bonus locations for player {self.player_name}.")
|
||||||
for location in goofy_get_bonus_location_pool:
|
for location in goofy_get_bonus_location_pool:
|
||||||
self.random.choice(self.goofy_get_bonus_abilities)
|
self.random.choice(self.goofy_get_bonus_abilities)
|
||||||
random_ability = self.random.choice(self.goofy_get_bonus_abilities)
|
random_ability = self.random.choice(self.goofy_get_bonus_abilities)
|
||||||
@@ -416,11 +416,12 @@ class KH2World(World):
|
|||||||
random_ability = self.random.choice(self.donald_weapon_abilities)
|
random_ability = self.random.choice(self.donald_weapon_abilities)
|
||||||
location.place_locked_item(random_ability)
|
location.place_locked_item(random_ability)
|
||||||
self.donald_weapon_abilities.remove(random_ability)
|
self.donald_weapon_abilities.remove(random_ability)
|
||||||
|
# if option is turned off
|
||||||
if not self.options.DonaldGoofyStatsanity:
|
if not self.options.DonaldGoofyStatsanity:
|
||||||
# plando goofy get bonuses
|
|
||||||
donald_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in
|
donald_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in
|
||||||
Donald_Checks.keys() if Donald_Checks[location].yml != "Keyblade"]
|
Donald_Checks.keys() if Donald_Checks[location].yml != "Keyblade"]
|
||||||
|
if len(donald_get_bonus_location_pool) > len(self.donald_get_bonus_abilities):
|
||||||
|
raise Exception(f"Too little abilities to fill donald get bonus locations for player {self.player_name}.")
|
||||||
for location in donald_get_bonus_location_pool:
|
for location in donald_get_bonus_location_pool:
|
||||||
random_ability = self.random.choice(self.donald_get_bonus_abilities)
|
random_ability = self.random.choice(self.donald_get_bonus_abilities)
|
||||||
location.place_locked_item(random_ability)
|
location.place_locked_item(random_ability)
|
||||||
|
|||||||
@@ -4956,10 +4956,16 @@
|
|||||||
Outside The Initiated:
|
Outside The Initiated:
|
||||||
room: Art Gallery
|
room: Art Gallery
|
||||||
door: Exit
|
door: Exit
|
||||||
The Bearer (East): True
|
The Bearer (East):
|
||||||
The Bearer (North): True
|
static_painting: True
|
||||||
The Bearer (South): True
|
The Bearer (North):
|
||||||
The Bearer (West): True
|
static_painting: True
|
||||||
|
The Bearer (South):
|
||||||
|
static_painting: True
|
||||||
|
The Bearer (West):
|
||||||
|
- static_painting: True
|
||||||
|
- room: The Bearer (West)
|
||||||
|
door: Side Area Shortcut
|
||||||
Roof: True
|
Roof: True
|
||||||
panels:
|
panels:
|
||||||
Achievement:
|
Achievement:
|
||||||
@@ -5053,7 +5059,8 @@
|
|||||||
- MIDDLE
|
- MIDDLE
|
||||||
The Bearer (East):
|
The Bearer (East):
|
||||||
entrances:
|
entrances:
|
||||||
Cross Tower (East): True
|
Cross Tower (East):
|
||||||
|
static_painting: True
|
||||||
Bearer Side Area:
|
Bearer Side Area:
|
||||||
door: Side Area Access
|
door: Side Area Access
|
||||||
Roof: True
|
Roof: True
|
||||||
@@ -5084,7 +5091,8 @@
|
|||||||
panel: SPACE
|
panel: SPACE
|
||||||
The Bearer (North):
|
The Bearer (North):
|
||||||
entrances:
|
entrances:
|
||||||
Cross Tower (East): True
|
Cross Tower (North):
|
||||||
|
static_painting: True
|
||||||
Roof: True
|
Roof: True
|
||||||
panels:
|
panels:
|
||||||
SILENT (1):
|
SILENT (1):
|
||||||
@@ -5128,7 +5136,8 @@
|
|||||||
panel: POTS
|
panel: POTS
|
||||||
The Bearer (South):
|
The Bearer (South):
|
||||||
entrances:
|
entrances:
|
||||||
Cross Tower (North): True
|
Cross Tower (South):
|
||||||
|
static_painting: True
|
||||||
Bearer Side Area:
|
Bearer Side Area:
|
||||||
door: Side Area Shortcut
|
door: Side Area Shortcut
|
||||||
Roof: True
|
Roof: True
|
||||||
@@ -5162,7 +5171,10 @@
|
|||||||
panel: SILENT (1)
|
panel: SILENT (1)
|
||||||
The Bearer (West):
|
The Bearer (West):
|
||||||
entrances:
|
entrances:
|
||||||
Cross Tower (West): True
|
Cross Tower (West):
|
||||||
|
static_painting: True
|
||||||
|
The Bearer:
|
||||||
|
door: Side Area Shortcut
|
||||||
Bearer Side Area:
|
Bearer Side Area:
|
||||||
door: Side Area Shortcut
|
door: Side Area Shortcut
|
||||||
Roof: True
|
Roof: True
|
||||||
@@ -5235,6 +5247,7 @@
|
|||||||
The Bearer:
|
The Bearer:
|
||||||
room: The Bearer
|
room: The Bearer
|
||||||
door: East Entrance
|
door: East Entrance
|
||||||
|
static_painting: True
|
||||||
Roof: True
|
Roof: True
|
||||||
panels:
|
panels:
|
||||||
WINTER:
|
WINTER:
|
||||||
@@ -5250,6 +5263,7 @@
|
|||||||
The Bearer (East):
|
The Bearer (East):
|
||||||
room: The Bearer (East)
|
room: The Bearer (East)
|
||||||
door: North Entrance
|
door: North Entrance
|
||||||
|
static_painting: True
|
||||||
Roof: True
|
Roof: True
|
||||||
panels:
|
panels:
|
||||||
NORTH:
|
NORTH:
|
||||||
@@ -5270,6 +5284,7 @@
|
|||||||
The Bearer (North):
|
The Bearer (North):
|
||||||
room: The Bearer (North)
|
room: The Bearer (North)
|
||||||
door: South Entrance
|
door: South Entrance
|
||||||
|
static_painting: True
|
||||||
panels:
|
panels:
|
||||||
FIRE:
|
FIRE:
|
||||||
id: Cross Room/Panel_fire_fire
|
id: Cross Room/Panel_fire_fire
|
||||||
@@ -5284,6 +5299,7 @@
|
|||||||
Bearer Side Area:
|
Bearer Side Area:
|
||||||
room: Bearer Side Area
|
room: Bearer Side Area
|
||||||
door: West Entrance
|
door: West Entrance
|
||||||
|
static_painting: True
|
||||||
Roof: True
|
Roof: True
|
||||||
panels:
|
panels:
|
||||||
DIAMONDS:
|
DIAMONDS:
|
||||||
@@ -7108,6 +7124,8 @@
|
|||||||
entrances:
|
entrances:
|
||||||
Orange Tower Third Floor:
|
Orange Tower Third Floor:
|
||||||
warp: True
|
warp: True
|
||||||
|
Art Gallery (First Floor):
|
||||||
|
warp: True
|
||||||
Art Gallery (Second Floor):
|
Art Gallery (Second Floor):
|
||||||
warp: True
|
warp: True
|
||||||
Art Gallery (Third Floor):
|
Art Gallery (Third Floor):
|
||||||
@@ -7125,22 +7143,6 @@
|
|||||||
required_door:
|
required_door:
|
||||||
room: Number Hunt
|
room: Number Hunt
|
||||||
door: Eights
|
door: Eights
|
||||||
EON:
|
|
||||||
id: Painting Room/Panel_eon_one
|
|
||||||
colors: yellow
|
|
||||||
tag: midyellow
|
|
||||||
TRUSTWORTHY:
|
|
||||||
id: Painting Room/Panel_to_two
|
|
||||||
colors: red
|
|
||||||
tag: midred
|
|
||||||
FREE:
|
|
||||||
id: Painting Room/Panel_free_three
|
|
||||||
colors: purple
|
|
||||||
tag: midpurp
|
|
||||||
OUR:
|
|
||||||
id: Painting Room/Panel_our_four
|
|
||||||
colors: blue
|
|
||||||
tag: midblue
|
|
||||||
ORDER:
|
ORDER:
|
||||||
id: Painting Room/Panel_order_onepathmanyturns
|
id: Painting Room/Panel_order_onepathmanyturns
|
||||||
tag: forbid
|
tag: forbid
|
||||||
@@ -7159,15 +7161,8 @@
|
|||||||
- scenery_painting_2c
|
- scenery_painting_2c
|
||||||
skip_location: True
|
skip_location: True
|
||||||
panels:
|
panels:
|
||||||
- EON
|
- room: Art Gallery (First Floor)
|
||||||
First Floor Puzzles:
|
panel: EON
|
||||||
skip_item: True
|
|
||||||
location_name: Art Gallery - First Floor Puzzles
|
|
||||||
panels:
|
|
||||||
- EON
|
|
||||||
- TRUSTWORTHY
|
|
||||||
- FREE
|
|
||||||
- OUR
|
|
||||||
Third Floor:
|
Third Floor:
|
||||||
painting_id:
|
painting_id:
|
||||||
- scenery_painting_3b
|
- scenery_painting_3b
|
||||||
@@ -7227,11 +7222,42 @@
|
|||||||
- Third Floor
|
- Third Floor
|
||||||
- Fourth Floor
|
- Fourth Floor
|
||||||
- Fifth Floor
|
- Fifth Floor
|
||||||
|
Art Gallery (First Floor):
|
||||||
|
entrances:
|
||||||
|
Art Gallery:
|
||||||
|
static_painting: True
|
||||||
|
panels:
|
||||||
|
EON:
|
||||||
|
id: Painting Room/Panel_eon_one
|
||||||
|
colors: yellow
|
||||||
|
tag: midyellow
|
||||||
|
TRUSTWORTHY:
|
||||||
|
id: Painting Room/Panel_to_two
|
||||||
|
colors: red
|
||||||
|
tag: midred
|
||||||
|
FREE:
|
||||||
|
id: Painting Room/Panel_free_three
|
||||||
|
colors: purple
|
||||||
|
tag: midpurp
|
||||||
|
OUR:
|
||||||
|
id: Painting Room/Panel_our_four
|
||||||
|
colors: blue
|
||||||
|
tag: midblue
|
||||||
|
doors:
|
||||||
|
Puzzles:
|
||||||
|
skip_item: True
|
||||||
|
location_name: Art Gallery - First Floor Puzzles
|
||||||
|
panels:
|
||||||
|
- EON
|
||||||
|
- TRUSTWORTHY
|
||||||
|
- FREE
|
||||||
|
- OUR
|
||||||
Art Gallery (Second Floor):
|
Art Gallery (Second Floor):
|
||||||
entrances:
|
entrances:
|
||||||
Art Gallery:
|
Art Gallery:
|
||||||
room: Art Gallery
|
room: Art Gallery
|
||||||
door: Second Floor
|
door: Second Floor
|
||||||
|
static_painting: True
|
||||||
panels:
|
panels:
|
||||||
HOUSE:
|
HOUSE:
|
||||||
id: Painting Room/Panel_house_neighborhood
|
id: Painting Room/Panel_house_neighborhood
|
||||||
@@ -7263,6 +7289,7 @@
|
|||||||
Art Gallery:
|
Art Gallery:
|
||||||
room: Art Gallery
|
room: Art Gallery
|
||||||
door: Third Floor
|
door: Third Floor
|
||||||
|
static_painting: True
|
||||||
panels:
|
panels:
|
||||||
AN:
|
AN:
|
||||||
id: Painting Room/Panel_an_many
|
id: Painting Room/Panel_an_many
|
||||||
@@ -7294,6 +7321,7 @@
|
|||||||
Art Gallery:
|
Art Gallery:
|
||||||
room: Art Gallery
|
room: Art Gallery
|
||||||
door: Fourth Floor
|
door: Fourth Floor
|
||||||
|
static_painting: True
|
||||||
panels:
|
panels:
|
||||||
URNS:
|
URNS:
|
||||||
id: Painting Room/Panel_urns_turns
|
id: Painting Room/Panel_urns_turns
|
||||||
|
|||||||
Binary file not shown.
@@ -727,11 +727,12 @@ panels:
|
|||||||
WANDER: 444975
|
WANDER: 444975
|
||||||
Art Gallery:
|
Art Gallery:
|
||||||
EIGHT: 444976
|
EIGHT: 444976
|
||||||
|
ORDER: 444981
|
||||||
|
Art Gallery (First Floor):
|
||||||
EON: 444977
|
EON: 444977
|
||||||
TRUSTWORTHY: 444978
|
TRUSTWORTHY: 444978
|
||||||
FREE: 444979
|
FREE: 444979
|
||||||
OUR: 444980
|
OUR: 444980
|
||||||
ORDER: 444981
|
|
||||||
Art Gallery (Second Floor):
|
Art Gallery (Second Floor):
|
||||||
HOUSE: 444982
|
HOUSE: 444982
|
||||||
PATH: 444983
|
PATH: 444983
|
||||||
@@ -1382,8 +1383,6 @@ doors:
|
|||||||
Art Gallery:
|
Art Gallery:
|
||||||
Second Floor:
|
Second Floor:
|
||||||
item: 444558
|
item: 444558
|
||||||
First Floor Puzzles:
|
|
||||||
location: 445256
|
|
||||||
Third Floor:
|
Third Floor:
|
||||||
item: 444559
|
item: 444559
|
||||||
Fourth Floor:
|
Fourth Floor:
|
||||||
@@ -1393,6 +1392,9 @@ doors:
|
|||||||
Exit:
|
Exit:
|
||||||
item: 444562
|
item: 444562
|
||||||
location: 444981
|
location: 444981
|
||||||
|
Art Gallery (First Floor):
|
||||||
|
Puzzles:
|
||||||
|
location: 445256
|
||||||
Art Gallery (Second Floor):
|
Art Gallery (Second Floor):
|
||||||
Puzzles:
|
Puzzles:
|
||||||
location: 445257
|
location: 445257
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class EntranceType(Flag):
|
|||||||
SUNWARP = auto()
|
SUNWARP = auto()
|
||||||
WARP = auto()
|
WARP = auto()
|
||||||
CROSSROADS_ROOF_ACCESS = auto()
|
CROSSROADS_ROOF_ACCESS = auto()
|
||||||
|
STATIC_PAINTING = auto()
|
||||||
|
|
||||||
|
|
||||||
class RoomEntrance(NamedTuple):
|
class RoomEntrance(NamedTuple):
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ def is_acceptable_pilgrimage_entrance(entrance_type: EntranceType, world: "Lingo
|
|||||||
allowed_entrance_types = EntranceType.NORMAL
|
allowed_entrance_types = EntranceType.NORMAL
|
||||||
|
|
||||||
if world.options.pilgrimage_allows_paintings:
|
if world.options.pilgrimage_allows_paintings:
|
||||||
allowed_entrance_types |= EntranceType.PAINTING
|
allowed_entrance_types |= EntranceType.PAINTING | EntranceType.STATIC_PAINTING
|
||||||
|
|
||||||
if world.options.pilgrimage_allows_roof_access:
|
if world.options.pilgrimage_allows_roof_access:
|
||||||
allowed_entrance_types |= EntranceType.CROSSROADS_ROOF_ACCESS
|
allowed_entrance_types |= EntranceType.CROSSROADS_ROOF_ACCESS
|
||||||
@@ -105,7 +105,8 @@ def create_regions(world: "LingoWorld") -> None:
|
|||||||
regions[pilgrimage_region_name] = Region(pilgrimage_region_name, world.player, world.multiworld)
|
regions[pilgrimage_region_name] = Region(pilgrimage_region_name, world.player, world.multiworld)
|
||||||
|
|
||||||
# Connect all created regions now that they exist.
|
# Connect all created regions now that they exist.
|
||||||
allowed_entrance_types = EntranceType.NORMAL | EntranceType.WARP | EntranceType.CROSSROADS_ROOF_ACCESS
|
allowed_entrance_types = EntranceType.NORMAL | EntranceType.WARP | EntranceType.CROSSROADS_ROOF_ACCESS | \
|
||||||
|
EntranceType.STATIC_PAINTING
|
||||||
|
|
||||||
if not painting_shuffle:
|
if not painting_shuffle:
|
||||||
# Don't use the vanilla painting connections if we are shuffling paintings.
|
# Don't use the vanilla painting connections if we are shuffling paintings.
|
||||||
@@ -156,11 +157,11 @@ def create_regions(world: "LingoWorld") -> None:
|
|||||||
regions[from_room].connect(regions[to_room], f"Pilgrimage Part {i+1}")
|
regions[from_room].connect(regions[to_room], f"Pilgrimage Part {i+1}")
|
||||||
else:
|
else:
|
||||||
connect_entrance(regions, regions["Starting Room"], regions["Pilgrim Antechamber"], "Sun Painting",
|
connect_entrance(regions, regions["Starting Room"], regions["Pilgrim Antechamber"], "Sun Painting",
|
||||||
RoomAndDoor("Pilgrim Antechamber", "Sun Painting"), EntranceType.PAINTING, False, world)
|
RoomAndDoor("Pilgrim Antechamber", "Sun Painting"), EntranceType.STATIC_PAINTING, False, world)
|
||||||
|
|
||||||
if early_color_hallways:
|
if early_color_hallways:
|
||||||
connect_entrance(regions, regions["Starting Room"], regions["Color Hallways"], "Early Color Hallways",
|
connect_entrance(regions, regions["Starting Room"], regions["Color Hallways"], "Early Color Hallways",
|
||||||
None, EntranceType.PAINTING, False, world)
|
None, EntranceType.STATIC_PAINTING, False, world)
|
||||||
|
|
||||||
if painting_shuffle:
|
if painting_shuffle:
|
||||||
for warp_enter, warp_exit in world.player_logic.painting_mapping.items():
|
for warp_enter, warp_exit in world.player_logic.painting_mapping.items():
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ def process_single_entrance(source_room: str, room_name: str, door_obj) -> RoomE
|
|||||||
entrance_type = EntranceType.WARP
|
entrance_type = EntranceType.WARP
|
||||||
elif source_room == "Crossroads" and room_name == "Roof":
|
elif source_room == "Crossroads" and room_name == "Roof":
|
||||||
entrance_type = EntranceType.CROSSROADS_ROOF_ACCESS
|
entrance_type = EntranceType.CROSSROADS_ROOF_ACCESS
|
||||||
|
elif "static_painting" in door_obj and door_obj["static_painting"]:
|
||||||
|
entrance_type = EntranceType.STATIC_PAINTING
|
||||||
|
|
||||||
if "painting" in door_obj and door_obj["painting"]:
|
if "painting" in door_obj and door_obj["painting"]:
|
||||||
PAINTING_EXIT_ROOMS.add(room_name)
|
PAINTING_EXIT_ROOMS.add(room_name)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# the file are consistent. It also checks that the panel and door IDs mentioned
|
# the file are consistent. It also checks that the panel and door IDs mentioned
|
||||||
# all exist in the map file.
|
# all exist in the map file.
|
||||||
#
|
#
|
||||||
# Usage: validate_config.rb [config file] [map file]
|
# Usage: validate_config.rb [config file] [ids path] [map file]
|
||||||
|
|
||||||
require 'set'
|
require 'set'
|
||||||
require 'yaml'
|
require 'yaml'
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ AP_JUNK = 0xD5
|
|||||||
|
|
||||||
class OoTContainer(APPatch):
|
class OoTContainer(APPatch):
|
||||||
game: str = 'Ocarina of Time'
|
game: str = 'Ocarina of Time'
|
||||||
|
patch_file_ending = ".apz5"
|
||||||
|
|
||||||
def __init__(self, patch_data: bytes, base_path: str, output_directory: str,
|
def __init__(self, patch_data: bytes, base_path: str, output_directory: str,
|
||||||
player = None, player_name: str = "", server: str = ""):
|
player = None, player_name: str = "", server: str = ""):
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ Da wir BizHawk benutzen, gilt diese Anleitung nur für Windows und Linux.
|
|||||||
## Benötigte Software
|
## Benötigte Software
|
||||||
|
|
||||||
- BizHawk: [BizHawk Veröffentlichungen von TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
- BizHawk: [BizHawk Veröffentlichungen von TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||||
- Version 2.3.1 und später werden unterstützt. Version 2.9 ist empfohlen.
|
- Version 2.3.1 und später werden unterstützt. Version 2.10 ist empfohlen.
|
||||||
- Detailierte Installtionsanweisungen für BizHawk können über den obrigen Link gefunden werden.
|
- Detailierte Installtionsanweisungen für BizHawk können über den obrigen Link gefunden werden.
|
||||||
- Windows-Benutzer müssen die Prerequisiten installiert haben. Diese können ebenfalls über
|
- Windows-Benutzer müssen die Prerequisiten installiert haben. Diese können ebenfalls über
|
||||||
den obrigen Link gefunden werden.
|
den obrigen Link gefunden werden.
|
||||||
- Der integrierte Archipelago-Client, welcher [hier](https://github.com/ArchipelagoMW/Archipelago/releases) installiert
|
- Der integrierte Archipelago-Client, welcher [hier](https://github.com/ArchipelagoMW/Archipelago/releases) installiert
|
||||||
werden kann.
|
werden kann.
|
||||||
- Eine `Ocarina of Time v1.0 US(?) ROM`. (Nicht aus Europa und keine Master-Quest oder Debug-Rom!)
|
- Eine `Ocarina of Time v1.0 US ROM`. (Nicht aus Europa und keine Master-Quest oder Debug-Rom!)
|
||||||
|
|
||||||
## Konfigurieren von BizHawk
|
## Konfigurieren von BizHawk
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
|
|||||||
## Required Software
|
## Required Software
|
||||||
|
|
||||||
- BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
- BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||||
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
|
- Version 2.3.1 and later are supported. Version 2.10 is recommended for stability.
|
||||||
- Detailed installation instructions for BizHawk can be found at the above link.
|
- Detailed installation instructions for BizHawk can be found at the above link.
|
||||||
- Windows users must run the prereq installer first, which can also be found at the above link.
|
- Windows users must run the prereq installer first, which can also be found at the above link.
|
||||||
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
|
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||||
- An Ocarina of Time v1.0 ROM.
|
- A US Ocarina of Time v1.0 ROM.
|
||||||
|
|
||||||
## Configuring BizHawk
|
## Configuring BizHawk
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windo
|
|||||||
## Logiciel requis
|
## Logiciel requis
|
||||||
|
|
||||||
- BizHawk : [Sorties BizHawk de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
- BizHawk : [Sorties BizHawk de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||||
- Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour des raisons de stabilité.
|
- Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.10 est recommandée pour des raisons de stabilité.
|
||||||
- Des instructions d'installation détaillées pour BizHawk peuvent être trouvées sur le lien ci-dessus.
|
- Des instructions d'installation détaillées pour BizHawk peuvent être trouvées sur le lien ci-dessus.
|
||||||
- Les utilisateurs Windows doivent d'abord exécuter le programme d'installation des prérequis, qui peut également être trouvé sur le lien ci-dessus.
|
- Les utilisateurs Windows doivent d'abord exécuter le programme d'installation des prérequis, qui peut également être trouvé sur le lien ci-dessus.
|
||||||
- Le client Archipelago intégré, qui peut être installé [ici](https://github.com/ArchipelagoMW/Archipelago/releases)
|
- Le client Archipelago intégré, qui peut être installé [ici](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||||
(sélectionnez « Ocarina of Time Client » lors de l'installation).
|
(sélectionnez « Ocarina of Time Client » lors de l'installation).
|
||||||
- Une ROM Ocarina of Time v1.0.
|
- Un fichier ROM v1.0 US d'Ocarina of Time.
|
||||||
|
|
||||||
## Configuration de BizHawk
|
## Configuration de BizHawk
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ class RaftWorld(World):
|
|||||||
options_dataclass = RaftOptions
|
options_dataclass = RaftOptions
|
||||||
options: RaftOptions
|
options: RaftOptions
|
||||||
|
|
||||||
|
extraItemNamePool: list[str] | None = None
|
||||||
|
|
||||||
required_client_version = (0, 3, 4)
|
required_client_version = (0, 3, 4)
|
||||||
|
|
||||||
def create_items(self):
|
def create_items(self):
|
||||||
@@ -52,52 +54,52 @@ class RaftWorld(World):
|
|||||||
pool = []
|
pool = []
|
||||||
frequencyItems = []
|
frequencyItems = []
|
||||||
for item in item_table:
|
for item in item_table:
|
||||||
raft_item = self.create_item_replaceAsNecessary(item["name"])
|
raft_item = self.create_item(self.replace_item_name_as_necessary(item["name"]))
|
||||||
if isFillingFrequencies and "Frequency" in item["name"]:
|
if isFillingFrequencies and "Frequency" in item["name"]:
|
||||||
frequencyItems.append(raft_item)
|
frequencyItems.append(raft_item)
|
||||||
else:
|
else:
|
||||||
pool.append(raft_item)
|
pool.append(raft_item)
|
||||||
|
|
||||||
extraItemNamePool = []
|
self.extraItemNamePool = []
|
||||||
extras = len(location_table) - len(item_table) - 1 # Victory takes up 1 unaccounted-for slot
|
extras = len(location_table) - len(item_table) - 1 # Victory takes up 1 unaccounted-for slot
|
||||||
if extras > 0:
|
|
||||||
if (self.options.filler_item_types != self.options.filler_item_types.option_duplicates): # Use resource packs
|
|
||||||
for packItem in resourcePackItems:
|
|
||||||
for i in range(minimumResourcePackAmount, maximumResourcePackAmount + 1):
|
|
||||||
extraItemNamePool.append(createResourcePackName(i, packItem))
|
|
||||||
|
|
||||||
if self.options.filler_item_types != self.options.filler_item_types.option_resource_packs: # Use duplicate items
|
if (self.options.filler_item_types != self.options.filler_item_types.option_duplicates): # Use resource packs
|
||||||
dupeItemPool = item_table.copy()
|
for packItem in resourcePackItems:
|
||||||
# Remove frequencies if necessary
|
for i in range(minimumResourcePackAmount, maximumResourcePackAmount + 1):
|
||||||
if self.options.island_frequency_locations != self.options.island_frequency_locations.option_anywhere: # Not completely random locations
|
self.extraItemNamePool.append(createResourcePackName(i, packItem))
|
||||||
# If we let frequencies stay in with progressive-frequencies, the progressive-frequency item
|
|
||||||
# will be included 7 times. This is a massive flood of progressive-frequency items, so we
|
if self.options.filler_item_types != self.options.filler_item_types.option_resource_packs: # Use duplicate items
|
||||||
# instead add progressive-frequency as its own item a smaller amount of times to prevent
|
dupeItemPool = item_table.copy()
|
||||||
# flooding the duplicate item pool with them.
|
# Remove frequencies if necessary
|
||||||
if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive:
|
if self.options.island_frequency_locations != self.options.island_frequency_locations.option_anywhere: # Not completely random locations
|
||||||
for _ in range(2):
|
# If we let frequencies stay in with progressive-frequencies, the progressive-frequency item
|
||||||
# Progressives are not in item_pool, need to create faux item for duplicate item pool
|
# will be included 7 times. This is a massive flood of progressive-frequency items, so we
|
||||||
# This can still be filtered out later by duplicate_items setting
|
# instead add progressive-frequency as its own item a smaller amount of times to prevent
|
||||||
dupeItemPool.append({ "name": "progressive-frequency", "progression": True }) # Progressive frequencies need to be included
|
# flooding the duplicate item pool with them.
|
||||||
# Always remove non-progressive Frequency items
|
if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive:
|
||||||
dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"])
|
for _ in range(2):
|
||||||
|
# Progressives are not in item_pool, need to create faux item for duplicate item pool
|
||||||
# Remove progression or non-progression items if necessary
|
# This can still be filtered out later by duplicate_items setting
|
||||||
if (self.options.duplicate_items == self.options.duplicate_items.option_progression): # Progression only
|
dupeItemPool.append({ "name": "progressive-frequency", "progression": True }) # Progressive frequencies need to be included
|
||||||
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True)
|
# Always remove non-progressive Frequency items
|
||||||
elif (self.options.duplicate_items == self.options.duplicate_items.option_non_progression): # Non-progression only
|
dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"])
|
||||||
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False)
|
|
||||||
|
# Remove progression or non-progression items if necessary
|
||||||
dupeItemPool = list(dupeItemPool)
|
if (self.options.duplicate_items == self.options.duplicate_items.option_progression): # Progression only
|
||||||
# Finally, add items as necessary
|
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True)
|
||||||
if len(dupeItemPool) > 0:
|
elif (self.options.duplicate_items == self.options.duplicate_items.option_non_progression): # Non-progression only
|
||||||
for item in dupeItemPool:
|
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False)
|
||||||
extraItemNamePool.append(item["name"])
|
|
||||||
|
dupeItemPool = list(dupeItemPool)
|
||||||
|
# Finally, add items as necessary
|
||||||
|
for item in dupeItemPool:
|
||||||
|
self.extraItemNamePool.append(self.replace_item_name_as_necessary(item))
|
||||||
|
|
||||||
if (len(extraItemNamePool) > 0):
|
assert self.extraItemNamePool, f"Don't know what extra items to create for {self.player_name}."
|
||||||
for randomItem in self.random.choices(extraItemNamePool, k=extras):
|
|
||||||
raft_item = self.create_item_replaceAsNecessary(randomItem)
|
for randomItem in self.random.choices(self.extraItemNamePool, k=extras):
|
||||||
pool.append(raft_item)
|
raft_item = self.create_item(randomItem)
|
||||||
|
pool.append(raft_item)
|
||||||
|
|
||||||
self.multiworld.itempool += pool
|
self.multiworld.itempool += pool
|
||||||
|
|
||||||
@@ -108,19 +110,35 @@ class RaftWorld(World):
|
|||||||
if frequencyItems:
|
if frequencyItems:
|
||||||
self.place_frequencyItems(frequencyItems)
|
self.place_frequencyItems(frequencyItems)
|
||||||
|
|
||||||
|
def get_filler_item_name(self) -> str:
|
||||||
|
# A normal Raft world will have an extraItemNamePool defined after create_items.
|
||||||
|
if self.extraItemNamePool:
|
||||||
|
return self.random.choice(self.extraItemNamePool)
|
||||||
|
|
||||||
|
# If this is a "fake" world, e.g. item links with link replacement: Resource packs are always be safe to create
|
||||||
|
minRPSpecified = self.options.minimum_resource_pack_amount.value
|
||||||
|
maxRPSpecified = self.options.maximum_resource_pack_amount.value
|
||||||
|
minimumResourcePackAmount = min(minRPSpecified, maxRPSpecified)
|
||||||
|
maximumResourcePackAmount = max(minRPSpecified, maxRPSpecified)
|
||||||
|
resource_amount = self.random.randint(minimumResourcePackAmount, maximumResourcePackAmount)
|
||||||
|
resource_type = self.random.choice(resourcePackItems)
|
||||||
|
return createResourcePackName(resource_amount, resource_type)
|
||||||
|
|
||||||
def set_rules(self):
|
def set_rules(self):
|
||||||
set_rules(self.multiworld, self.player)
|
set_rules(self.multiworld, self.player)
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
create_regions(self.multiworld, self.player)
|
create_regions(self.multiworld, self.player)
|
||||||
|
|
||||||
def create_item_replaceAsNecessary(self, name: str) -> Item:
|
def replace_item_name_as_necessary(self, name: str) -> str:
|
||||||
isFrequency = "Frequency" in name
|
if name not in progressive_table:
|
||||||
shouldUseProgressive = bool((isFrequency and self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive)
|
return name
|
||||||
or (not isFrequency and self.options.progressive_items))
|
if "Frequency" in name:
|
||||||
if shouldUseProgressive and name in progressive_table:
|
if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive:
|
||||||
name = progressive_table[name]
|
return progressive_table[name]
|
||||||
return self.create_item(name)
|
elif self.options.progressive_items:
|
||||||
|
return progressive_table[name]
|
||||||
|
return name
|
||||||
|
|
||||||
def create_item(self, name: str) -> Item:
|
def create_item(self, name: str) -> Item:
|
||||||
item = lookup_name_to_item[name]
|
item = lookup_name_to_item[name]
|
||||||
|
|||||||
@@ -92,17 +92,7 @@ class TestGlobalOptionsImport(TestCase):
|
|||||||
f"{max_levels_and_upgrades} instead.")
|
f"{max_levels_and_upgrades} instead.")
|
||||||
|
|
||||||
|
|
||||||
class TestMinimum(ShapezTestBase):
|
# The following unittests are intended to test all code paths of the generator
|
||||||
options = options_presets["Minimum checks"]
|
|
||||||
|
|
||||||
|
|
||||||
class TestMaximum(ShapezTestBase):
|
|
||||||
options = options_presets["Maximum checks"]
|
|
||||||
|
|
||||||
|
|
||||||
class TestRestrictive(ShapezTestBase):
|
|
||||||
options = options_presets["Restrictive start"]
|
|
||||||
|
|
||||||
|
|
||||||
class TestAllRelevantOptions1(ShapezTestBase):
|
class TestAllRelevantOptions1(ShapezTestBase):
|
||||||
options = {
|
options = {
|
||||||
|
|||||||
@@ -130,9 +130,7 @@ page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platfor
|
|||||||
|
|
||||||
### Open the client
|
### Open the client
|
||||||
|
|
||||||
Open ap-soeclient ([Evermizer Archipelago Client Page](http://evermizer.com/apclient)) in a modern browser. Do not
|
Open ap-soeclient ([Evermizer Archipelago Client Page](http://evermizer.com/apclient)) in a modern browser.
|
||||||
switch tabs, open it in a new window if you want to use the browser while playing. Do not minimize the window with the
|
|
||||||
client.
|
|
||||||
|
|
||||||
The client should automatically connect to SNI, the "SNES" status should change to green.
|
The client should automatically connect to SNI, the "SNES" status should change to green.
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
from random import Random
|
from random import Random
|
||||||
from typing import Dict, Any, Iterable, Optional, List, TextIO
|
from typing import Dict, Any, Optional, List, TextIO
|
||||||
|
|
||||||
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
|
import entrance_rando
|
||||||
|
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
|
||||||
from Options import PerGameCommonOptions
|
from Options import PerGameCommonOptions
|
||||||
from worlds.AutoWorld import World, WebWorld
|
from worlds.AutoWorld import World, WebWorld
|
||||||
from .bundles.bundle_room import BundleRoom
|
from .bundles.bundle_room import BundleRoom
|
||||||
@@ -21,7 +22,7 @@ from .options.forced_options import force_change_options_if_incompatible
|
|||||||
from .options.option_groups import sv_option_groups
|
from .options.option_groups import sv_option_groups
|
||||||
from .options.presets import sv_options_presets
|
from .options.presets import sv_options_presets
|
||||||
from .options.worlds_group import apply_most_restrictive_options
|
from .options.worlds_group import apply_most_restrictive_options
|
||||||
from .regions import create_regions
|
from .regions import create_regions, prepare_mod_data
|
||||||
from .rules import set_rules
|
from .rules import set_rules
|
||||||
from .stardew_rule import True_, StardewRule, HasProgressionPercent
|
from .stardew_rule import True_, StardewRule, HasProgressionPercent
|
||||||
from .strings.ap_names.event_names import Event
|
from .strings.ap_names.event_names import Event
|
||||||
@@ -124,18 +125,13 @@ class StardewValleyWorld(World):
|
|||||||
self.content = create_content(self.options)
|
self.content = create_content(self.options)
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
def create_region(name: str, exits: Iterable[str]) -> Region:
|
def create_region(name: str) -> Region:
|
||||||
region = Region(name, self.player, self.multiworld)
|
return Region(name, self.player, self.multiworld)
|
||||||
region.exits = [Entrance(self.player, exit_name, region) for exit_name in exits]
|
|
||||||
return region
|
|
||||||
|
|
||||||
world_regions, world_entrances, self.randomized_entrances = create_regions(create_region, self.random, self.options, self.content)
|
world_regions = create_regions(create_region, self.options, self.content)
|
||||||
|
|
||||||
self.logic = StardewLogic(self.player, self.options, self.content, world_regions.keys())
|
self.logic = StardewLogic(self.player, self.options, self.content, world_regions.keys())
|
||||||
self.modified_bundles = get_all_bundles(self.random,
|
self.modified_bundles = get_all_bundles(self.random, self.logic, self.content, self.options)
|
||||||
self.logic,
|
|
||||||
self.content,
|
|
||||||
self.options)
|
|
||||||
|
|
||||||
def add_location(name: str, code: Optional[int], region: str):
|
def add_location(name: str, code: Optional[int], region: str):
|
||||||
region: Region = world_regions[region]
|
region: Region = world_regions[region]
|
||||||
@@ -308,6 +304,11 @@ class StardewValleyWorld(World):
|
|||||||
def set_rules(self):
|
def set_rules(self):
|
||||||
set_rules(self)
|
set_rules(self)
|
||||||
|
|
||||||
|
def connect_entrances(self) -> None:
|
||||||
|
no_target_groups = {0: [0]}
|
||||||
|
placement = entrance_rando.randomize_entrances(self, coupled=True, target_group_lookup=no_target_groups)
|
||||||
|
self.randomized_entrances = prepare_mod_data(placement)
|
||||||
|
|
||||||
def generate_basic(self):
|
def generate_basic(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ from ...strings.skill_names import Skill
|
|||||||
from ...strings.tool_names import Tool, ToolMaterial
|
from ...strings.tool_names import Tool, ToolMaterial
|
||||||
from ...strings.villager_names import ModNPC
|
from ...strings.villager_names import ModNPC
|
||||||
|
|
||||||
|
# Used to adapt content not yet moved to content packs to easily detect when SVE and Ginger Island are both enabled.
|
||||||
|
SVE_GINGER_ISLAND_PACK = ModNames.sve + "+" + ginger_island_content_pack.name
|
||||||
|
|
||||||
|
|
||||||
class SVEContentPack(ContentPack):
|
class SVEContentPack(ContentPack):
|
||||||
|
|
||||||
@@ -67,6 +70,10 @@ class SVEContentPack(ContentPack):
|
|||||||
content.game_items.pop(SVESeed.slime)
|
content.game_items.pop(SVESeed.slime)
|
||||||
content.game_items.pop(SVEFruit.slime_berry)
|
content.game_items.pop(SVEFruit.slime_berry)
|
||||||
|
|
||||||
|
def finalize_hook(self, content: StardewContent):
|
||||||
|
if ginger_island_content_pack.name in content.registered_packs:
|
||||||
|
content.registered_packs.add(SVE_GINGER_ISLAND_PACK)
|
||||||
|
|
||||||
|
|
||||||
register_mod_content_pack(SVEContentPack(
|
register_mod_content_pack(SVEContentPack(
|
||||||
ModNames.sve,
|
ModNames.sve,
|
||||||
@@ -80,8 +87,9 @@ register_mod_content_pack(SVEContentPack(
|
|||||||
ModEdible.lightning_elixir: (ShopSource(money_price=12000, shop_region=SVERegion.galmoran_outpost),),
|
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.barbarian_elixir: (ShopSource(money_price=22000, shop_region=SVERegion.galmoran_outpost),),
|
||||||
ModEdible.gravity_elixir: (ShopSource(money_price=4000, shop_region=SVERegion.galmoran_outpost),),
|
ModEdible.gravity_elixir: (ShopSource(money_price=4000, shop_region=SVERegion.galmoran_outpost),),
|
||||||
SVEMeal.grampleton_orange_chicken: (
|
SVEMeal.grampleton_orange_chicken: (ShopSource(money_price=650,
|
||||||
ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),),
|
shop_region=Region.saloon,
|
||||||
|
other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),),
|
||||||
ModEdible.hero_elixir: (ShopSource(money_price=8000, shop_region=SVERegion.isaac_shop),),
|
ModEdible.hero_elixir: (ShopSource(money_price=8000, shop_region=SVERegion.isaac_shop),),
|
||||||
ModEdible.aegis_elixir: (ShopSource(money_price=28000, shop_region=SVERegion.galmoran_outpost),),
|
ModEdible.aegis_elixir: (ShopSource(money_price=28000, shop_region=SVERegion.galmoran_outpost),),
|
||||||
SVEBeverage.sports_drink: (ShopSource(money_price=750, shop_region=Region.hospital),),
|
SVEBeverage.sports_drink: (ShopSource(money_price=750, shop_region=Region.hospital),),
|
||||||
@@ -118,8 +126,8 @@ register_mod_content_pack(SVEContentPack(
|
|||||||
|
|
||||||
ModLoot.green_mushroom: (ForagingSource(regions=(SVERegion.highlands_pond,), seasons=Season.not_winter),),
|
ModLoot.green_mushroom: (ForagingSource(regions=(SVERegion.highlands_pond,), seasons=Season.not_winter),),
|
||||||
ModLoot.ornate_treasure_chest: (ForagingSource(regions=(SVERegion.highlands_outside,),
|
ModLoot.ornate_treasure_chest: (ForagingSource(regions=(SVERegion.highlands_outside,),
|
||||||
other_requirements=(
|
other_requirements=(CombatRequirement(Performance.galaxy),
|
||||||
CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),),
|
ToolRequirement(Tool.axe, ToolMaterial.iron))),),
|
||||||
ModLoot.swirl_stone: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.galaxy),)),),
|
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),)),),
|
ModLoot.void_soul: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.good),)),),
|
||||||
SVEForage.winter_star_rose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.winter,)),),
|
SVEForage.winter_star_rose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.winter,)),),
|
||||||
@@ -139,8 +147,9 @@ register_mod_content_pack(SVEContentPack(
|
|||||||
SVEForage.thistle: (ForagingSource(regions=(SVERegion.summit,)),),
|
SVEForage.thistle: (ForagingSource(regions=(SVERegion.summit,)),),
|
||||||
ModLoot.void_pebble: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),),
|
ModLoot.void_pebble: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),),
|
||||||
ModLoot.void_shard: (ForagingSource(regions=(SVERegion.crimson_badlands,),
|
ModLoot.void_shard: (ForagingSource(regions=(SVERegion.crimson_badlands,),
|
||||||
other_requirements=(
|
other_requirements=(CombatRequirement(Performance.galaxy),
|
||||||
CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),),
|
SkillRequirement(Skill.combat, 10),
|
||||||
|
YearRequirement(3),)),),
|
||||||
SVEWaterItem.dulse_seaweed: (ForagingSource(regions=(Region.beach,), other_requirements=(FishingRequirement(Region.beach),)),),
|
SVEWaterItem.dulse_seaweed: (ForagingSource(regions=(Region.beach,), other_requirements=(FishingRequirement(Region.beach),)),),
|
||||||
|
|
||||||
# Fable Reef
|
# Fable Reef
|
||||||
@@ -207,7 +216,6 @@ register_mod_content_pack(SVEContentPack(
|
|||||||
villagers_data.scarlett,
|
villagers_data.scarlett,
|
||||||
villagers_data.susan,
|
villagers_data.susan,
|
||||||
villagers_data.morris,
|
villagers_data.morris,
|
||||||
# The wizard leaves his tower on sunday, for like 1 hour... Good enough for entrance rando!
|
override(villagers_data.wizard, bachelor=True, mod_name=ModNames.sve),
|
||||||
override(villagers_data.wizard, locations=(Region.wizard_tower, Region.forest), bachelor=True, mod_name=ModNames.sve),
|
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ hopper = ap_recipe(Craftable.hopper, {Material.hardwood: 10, MetalBar.iridium: 1
|
|||||||
cookout_kit = skill_recipe(Craftable.cookout_kit, Skill.foraging, 3, {Material.wood: 15, Material.fiber: 10, Material.coal: 3})
|
cookout_kit = skill_recipe(Craftable.cookout_kit, Skill.foraging, 3, {Material.wood: 15, Material.fiber: 10, Material.coal: 3})
|
||||||
tent_kit = skill_recipe(Craftable.tent_kit, Skill.foraging, 8, {Material.hardwood: 10, Material.fiber: 25, ArtisanGood.cloth: 1})
|
tent_kit = skill_recipe(Craftable.tent_kit, Skill.foraging, 8, {Material.hardwood: 10, Material.fiber: 25, ArtisanGood.cloth: 1})
|
||||||
|
|
||||||
statue_of_blessings = mastery_recipe(Statue.blessings, Skill.farming, {Material.sap: 999, Material.fiber: 999, Material.stone: 999})
|
statue_of_blessings = mastery_recipe(Statue.blessings, Skill.farming, {Material.sap: 999, Material.fiber: 999, Material.stone: 999, Material.moss: 333})
|
||||||
statue_of_dwarf_king = mastery_recipe(Statue.dwarf_king, Skill.mining, {MetalBar.iridium: 20})
|
statue_of_dwarf_king = mastery_recipe(Statue.dwarf_king, Skill.mining, {MetalBar.iridium: 20})
|
||||||
heavy_furnace = mastery_recipe(Machine.heavy_furnace, Skill.mining, {Machine.furnace: 2, MetalBar.iron: 3, Material.stone: 50})
|
heavy_furnace = mastery_recipe(Machine.heavy_furnace, Skill.mining, {Machine.furnace: 2, MetalBar.iron: 3, Material.stone: 50})
|
||||||
mystic_tree_seed = mastery_recipe(TreeSeed.mystic, Skill.foraging, {TreeSeed.acorn: 5, TreeSeed.maple: 5, TreeSeed.pine: 5, TreeSeed.mahogany: 5})
|
mystic_tree_seed = mastery_recipe(TreeSeed.mystic, Skill.foraging, {TreeSeed.acorn: 5, TreeSeed.maple: 5, TreeSeed.pine: 5, TreeSeed.mahogany: 5})
|
||||||
|
|||||||
@@ -1129,8 +1129,8 @@ id,region,name,tags,mod_name
|
|||||||
2204,Leo's Hut,Leo's Parrot,"GINGER_ISLAND,WALNUT_PURCHASE",
|
2204,Leo's Hut,Leo's Parrot,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||||
2205,Island South,Island West Turtle,"GINGER_ISLAND,WALNUT_PURCHASE",
|
2205,Island South,Island West Turtle,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||||
2206,Island West,Island Farmhouse,"GINGER_ISLAND,WALNUT_PURCHASE",
|
2206,Island West,Island Farmhouse,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||||
2207,Island Farmhouse,Island Mailbox,"GINGER_ISLAND,WALNUT_PURCHASE",
|
2207,Island West,Island Mailbox,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||||
2208,Island Farmhouse,Farm Obelisk,"GINGER_ISLAND,WALNUT_PURCHASE",
|
2208,Island West,Farm Obelisk,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||||
2209,Island North,Dig Site Bridge,"GINGER_ISLAND,WALNUT_PURCHASE",
|
2209,Island North,Dig Site Bridge,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||||
2210,Island North,Island Trader,"GINGER_ISLAND,WALNUT_PURCHASE",
|
2210,Island North,Island Trader,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||||
2211,Volcano Entrance,Volcano Bridge,"GINGER_ISLAND,WALNUT_PURCHASE",
|
2211,Volcano Entrance,Volcano Bridge,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||||
|
|||||||
|
@@ -89,7 +89,7 @@ class QuestLogic(BaseLogic):
|
|||||||
Quest.goblin_problem: self.logic.region.can_reach(Region.witch_swamp)
|
Quest.goblin_problem: self.logic.region.can_reach(Region.witch_swamp)
|
||||||
# Void mayo can be fished at 5% chance in the witch swamp while the quest is active. It drops a lot after the quest.
|
# Void mayo can be fished at 5% chance in the witch swamp while the quest is active. It drops a lot after the quest.
|
||||||
& (self.logic.has(ArtisanGood.void_mayonnaise) | self.logic.fishing.can_fish()),
|
& (self.logic.has(ArtisanGood.void_mayonnaise) | self.logic.fishing.can_fish()),
|
||||||
Quest.magic_ink: self.logic.relationship.can_meet(NPC.wizard),
|
Quest.magic_ink: self.logic.region.can_reach(Region.witch_hut) & self.logic.relationship.can_meet(NPC.wizard),
|
||||||
Quest.the_pirates_wife: self.logic.relationship.can_meet(NPC.kent) & self.logic.relationship.can_meet(NPC.gus) &
|
Quest.the_pirates_wife: self.logic.relationship.can_meet(NPC.kent) & self.logic.relationship.can_meet(NPC.gus) &
|
||||||
self.logic.relationship.can_meet(NPC.sandy) & self.logic.relationship.can_meet(NPC.george) &
|
self.logic.relationship.can_meet(NPC.sandy) & self.logic.relationship.can_meet(NPC.george) &
|
||||||
self.logic.relationship.can_meet(NPC.wizard) & self.logic.relationship.can_meet(NPC.willy),
|
self.logic.relationship.can_meet(NPC.wizard) & self.logic.relationship.can_meet(NPC.willy),
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
from typing import Tuple, Union
|
from typing import Tuple
|
||||||
|
|
||||||
from Utils import cache_self1
|
from Utils import cache_self1
|
||||||
from .base_logic import BaseLogic, BaseLogicMixin
|
from .base_logic import BaseLogic, BaseLogicMixin
|
||||||
from .has_logic import HasLogicMixin
|
|
||||||
from ..options import EntranceRandomization
|
from ..options import EntranceRandomization
|
||||||
from ..stardew_rule import StardewRule, Reach, false_, true_
|
from ..stardew_rule import StardewRule, Reach, false_, true_
|
||||||
from ..strings.region_names import Region
|
from ..strings.region_names import Region
|
||||||
|
|
||||||
main_outside_area = {Region.menu, Region.stardew_valley, Region.farm_house, Region.farm, Region.town, Region.beach, Region.mountain, Region.forest,
|
main_outside_area = {Region.menu, Region.stardew_valley, Region.farm_house, Region.farm, Region.town, Region.beach, Region.mountain, Region.forest,
|
||||||
Region.bus_stop, Region.backwoods, Region.bus_tunnel, Region.tunnel_entrance}
|
Region.bus_stop, Region.backwoods, Region.bus_tunnel, Region.tunnel_entrance}
|
||||||
always_accessible_regions_without_er = {*main_outside_area, Region.community_center, Region.pantry, Region.crafts_room, Region.fish_tank, Region.boiler_room,
|
always_accessible_regions_with_non_progression_er = {*main_outside_area, Region.mines, Region.hospital, Region.carpenter, Region.alex_house,
|
||||||
Region.vault, Region.bulletin_board, Region.mines, Region.hospital, Region.carpenter, Region.alex_house,
|
Region.ranch, Region.farm_cave, Region.wizard_tower, Region.tent,
|
||||||
Region.elliott_house, Region.ranch, Region.farm_cave, Region.wizard_tower, Region.tent, Region.pierre_store,
|
Region.pierre_store, Region.saloon, Region.blacksmith, Region.trailer, Region.museum, Region.mayor_house,
|
||||||
Region.saloon, Region.blacksmith, Region.trailer, Region.museum, Region.mayor_house, Region.haley_house,
|
Region.haley_house, Region.sam_house, Region.jojamart, Region.fish_shop}
|
||||||
Region.sam_house, Region.jojamart, Region.fish_shop}
|
always_accessible_regions_without_er = {*always_accessible_regions_with_non_progression_er, Region.community_center, Region.pantry, Region.crafts_room,
|
||||||
|
Region.fish_tank, Region.boiler_room, Region.vault, Region.bulletin_board}
|
||||||
|
|
||||||
always_regions_by_setting = {EntranceRandomization.option_disabled: always_accessible_regions_without_er,
|
always_regions_by_setting = {EntranceRandomization.option_disabled: always_accessible_regions_without_er,
|
||||||
EntranceRandomization.option_pelican_town: always_accessible_regions_without_er,
|
EntranceRandomization.option_pelican_town: always_accessible_regions_without_er,
|
||||||
EntranceRandomization.option_non_progression: always_accessible_regions_without_er,
|
EntranceRandomization.option_non_progression: always_accessible_regions_with_non_progression_er,
|
||||||
EntranceRandomization.option_buildings_without_house: main_outside_area,
|
EntranceRandomization.option_buildings_without_house: main_outside_area,
|
||||||
EntranceRandomization.option_buildings: main_outside_area,
|
EntranceRandomization.option_buildings: main_outside_area,
|
||||||
EntranceRandomization.option_chaos: always_accessible_regions_without_er}
|
EntranceRandomization.option_chaos: always_accessible_regions_without_er}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
from ..mod_regions import SVERegion
|
|
||||||
from ...logic.base_logic import BaseLogicMixin, BaseLogic
|
from ...logic.base_logic import BaseLogicMixin, BaseLogic
|
||||||
from ...strings.ap_names.mods.mod_items import SVELocation, SVERunes, SVEQuestItem
|
from ...strings.ap_names.mods.mod_items import SVELocation, SVERunes, SVEQuestItem
|
||||||
from ...strings.quest_names import Quest, ModQuest
|
from ...strings.quest_names import Quest, ModQuest
|
||||||
from ...strings.region_names import Region
|
from ...strings.region_names import Region, SVERegion
|
||||||
from ...strings.tool_names import Tool, ToolMaterial
|
from ...strings.tool_names import Tool, ToolMaterial
|
||||||
from ...strings.wallet_item_names import Wallet
|
from ...strings.wallet_item_names import Wallet
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
from .mod_data import ModNames
|
from .mod_data import ModNames
|
||||||
from ..region_classes import RegionData, ConnectionData, ModificationFlag, RandomizationFlag, ModRegionData
|
from ..content.mods.sve import SVE_GINGER_ISLAND_PACK
|
||||||
|
from ..regions.model import RegionData, ConnectionData, MergeFlag, RandomizationFlag, ModRegionsData
|
||||||
from ..strings.entrance_names import Entrance, DeepWoodsEntrance, EugeneEntrance, LaceyEntrance, BoardingHouseEntrance, \
|
from ..strings.entrance_names import Entrance, DeepWoodsEntrance, EugeneEntrance, LaceyEntrance, BoardingHouseEntrance, \
|
||||||
JasperEntrance, AlecEntrance, YobaEntrance, JunaEntrance, MagicEntrance, AyeishaEntrance, RileyEntrance, SVEEntrance, AlectoEntrance
|
JasperEntrance, AlecEntrance, YobaEntrance, JunaEntrance, MagicEntrance, AyeishaEntrance, RileyEntrance, SVEEntrance, AlectoEntrance
|
||||||
from ..strings.region_names import Region, DeepWoodsRegion, EugeneRegion, JasperRegion, BoardingHouseRegion, \
|
from ..strings.region_names import Region, DeepWoodsRegion, EugeneRegion, JasperRegion, BoardingHouseRegion, \
|
||||||
AlecRegion, YobaRegion, JunaRegion, MagicRegion, AyeishaRegion, RileyRegion, SVERegion, AlectoRegion, LaceyRegion
|
AlecRegion, YobaRegion, JunaRegion, MagicRegion, AyeishaRegion, RileyRegion, SVERegion, AlectoRegion, LaceyRegion
|
||||||
|
|
||||||
deep_woods_regions = [
|
deep_woods_regions = [
|
||||||
RegionData(Region.farm, [DeepWoodsEntrance.use_woods_obelisk]),
|
RegionData(Region.farm, (DeepWoodsEntrance.use_woods_obelisk,)),
|
||||||
RegionData(DeepWoodsRegion.woods_obelisk_menu, [DeepWoodsEntrance.deep_woods_depth_1,
|
RegionData(DeepWoodsRegion.woods_obelisk_menu, (DeepWoodsEntrance.deep_woods_depth_1,
|
||||||
DeepWoodsEntrance.deep_woods_depth_10,
|
DeepWoodsEntrance.deep_woods_depth_10,
|
||||||
DeepWoodsEntrance.deep_woods_depth_20,
|
DeepWoodsEntrance.deep_woods_depth_20,
|
||||||
DeepWoodsEntrance.deep_woods_depth_30,
|
DeepWoodsEntrance.deep_woods_depth_30,
|
||||||
@@ -19,9 +18,9 @@ deep_woods_regions = [
|
|||||||
DeepWoodsEntrance.deep_woods_depth_70,
|
DeepWoodsEntrance.deep_woods_depth_70,
|
||||||
DeepWoodsEntrance.deep_woods_depth_80,
|
DeepWoodsEntrance.deep_woods_depth_80,
|
||||||
DeepWoodsEntrance.deep_woods_depth_90,
|
DeepWoodsEntrance.deep_woods_depth_90,
|
||||||
DeepWoodsEntrance.deep_woods_depth_100]),
|
DeepWoodsEntrance.deep_woods_depth_100)),
|
||||||
RegionData(Region.secret_woods, [DeepWoodsEntrance.secret_woods_to_deep_woods]),
|
RegionData(Region.secret_woods, (DeepWoodsEntrance.secret_woods_to_deep_woods,)),
|
||||||
RegionData(DeepWoodsRegion.main_lichtung, [DeepWoodsEntrance.deep_woods_house]),
|
RegionData(DeepWoodsRegion.main_lichtung, (DeepWoodsEntrance.deep_woods_house,)),
|
||||||
RegionData(DeepWoodsRegion.abandoned_home),
|
RegionData(DeepWoodsRegion.abandoned_home),
|
||||||
RegionData(DeepWoodsRegion.floor_10),
|
RegionData(DeepWoodsRegion.floor_10),
|
||||||
RegionData(DeepWoodsRegion.floor_20),
|
RegionData(DeepWoodsRegion.floor_20),
|
||||||
@@ -32,14 +31,13 @@ deep_woods_regions = [
|
|||||||
RegionData(DeepWoodsRegion.floor_70),
|
RegionData(DeepWoodsRegion.floor_70),
|
||||||
RegionData(DeepWoodsRegion.floor_80),
|
RegionData(DeepWoodsRegion.floor_80),
|
||||||
RegionData(DeepWoodsRegion.floor_90),
|
RegionData(DeepWoodsRegion.floor_90),
|
||||||
RegionData(DeepWoodsRegion.floor_100)
|
RegionData(DeepWoodsRegion.floor_100),
|
||||||
]
|
]
|
||||||
|
|
||||||
deep_woods_entrances = [
|
deep_woods_entrances = [
|
||||||
ConnectionData(DeepWoodsEntrance.use_woods_obelisk, DeepWoodsRegion.woods_obelisk_menu),
|
ConnectionData(DeepWoodsEntrance.use_woods_obelisk, DeepWoodsRegion.woods_obelisk_menu),
|
||||||
ConnectionData(DeepWoodsEntrance.secret_woods_to_deep_woods, DeepWoodsRegion.main_lichtung),
|
ConnectionData(DeepWoodsEntrance.secret_woods_to_deep_woods, DeepWoodsRegion.main_lichtung),
|
||||||
ConnectionData(DeepWoodsEntrance.deep_woods_house, DeepWoodsRegion.abandoned_home,
|
ConnectionData(DeepWoodsEntrance.deep_woods_house, DeepWoodsRegion.abandoned_home, flag=RandomizationFlag.BUILDINGS),
|
||||||
flag=RandomizationFlag.NON_PROGRESSION),
|
|
||||||
ConnectionData(DeepWoodsEntrance.deep_woods_depth_1, DeepWoodsRegion.main_lichtung),
|
ConnectionData(DeepWoodsEntrance.deep_woods_depth_1, DeepWoodsRegion.main_lichtung),
|
||||||
ConnectionData(DeepWoodsEntrance.deep_woods_depth_10, DeepWoodsRegion.floor_10),
|
ConnectionData(DeepWoodsEntrance.deep_woods_depth_10, DeepWoodsRegion.floor_10),
|
||||||
ConnectionData(DeepWoodsEntrance.deep_woods_depth_20, DeepWoodsRegion.floor_20),
|
ConnectionData(DeepWoodsEntrance.deep_woods_depth_20, DeepWoodsRegion.floor_20),
|
||||||
@@ -50,165 +48,166 @@ deep_woods_entrances = [
|
|||||||
ConnectionData(DeepWoodsEntrance.deep_woods_depth_70, DeepWoodsRegion.floor_70),
|
ConnectionData(DeepWoodsEntrance.deep_woods_depth_70, DeepWoodsRegion.floor_70),
|
||||||
ConnectionData(DeepWoodsEntrance.deep_woods_depth_80, DeepWoodsRegion.floor_80),
|
ConnectionData(DeepWoodsEntrance.deep_woods_depth_80, DeepWoodsRegion.floor_80),
|
||||||
ConnectionData(DeepWoodsEntrance.deep_woods_depth_90, DeepWoodsRegion.floor_90),
|
ConnectionData(DeepWoodsEntrance.deep_woods_depth_90, DeepWoodsRegion.floor_90),
|
||||||
ConnectionData(DeepWoodsEntrance.deep_woods_depth_100, DeepWoodsRegion.floor_100)
|
ConnectionData(DeepWoodsEntrance.deep_woods_depth_100, DeepWoodsRegion.floor_100),
|
||||||
]
|
]
|
||||||
|
|
||||||
eugene_regions = [
|
eugene_regions = [
|
||||||
RegionData(Region.forest, [EugeneEntrance.forest_to_garden]),
|
RegionData(Region.forest, (EugeneEntrance.forest_to_garden,)),
|
||||||
RegionData(EugeneRegion.eugene_garden, [EugeneEntrance.garden_to_bedroom]),
|
RegionData(EugeneRegion.eugene_garden, (EugeneEntrance.garden_to_bedroom,)),
|
||||||
RegionData(EugeneRegion.eugene_bedroom)
|
RegionData(EugeneRegion.eugene_bedroom),
|
||||||
]
|
]
|
||||||
|
|
||||||
eugene_entrances = [
|
eugene_entrances = [
|
||||||
ConnectionData(EugeneEntrance.forest_to_garden, EugeneRegion.eugene_garden,
|
ConnectionData(EugeneEntrance.forest_to_garden, EugeneRegion.eugene_garden,
|
||||||
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
ConnectionData(EugeneEntrance.garden_to_bedroom, EugeneRegion.eugene_bedroom, flag=RandomizationFlag.BUILDINGS)
|
ConnectionData(EugeneEntrance.garden_to_bedroom, EugeneRegion.eugene_bedroom, flag=RandomizationFlag.BUILDINGS),
|
||||||
]
|
]
|
||||||
|
|
||||||
magic_regions = [
|
magic_regions = [
|
||||||
RegionData(Region.pierre_store, [MagicEntrance.store_to_altar]),
|
RegionData(Region.pierre_store, (MagicEntrance.store_to_altar,)),
|
||||||
RegionData(MagicRegion.altar)
|
RegionData(MagicRegion.altar),
|
||||||
]
|
]
|
||||||
|
|
||||||
magic_entrances = [
|
magic_entrances = [
|
||||||
ConnectionData(MagicEntrance.store_to_altar, MagicRegion.altar, flag=RandomizationFlag.NOT_RANDOMIZED)
|
ConnectionData(MagicEntrance.store_to_altar, MagicRegion.altar, flag=RandomizationFlag.NOT_RANDOMIZED),
|
||||||
]
|
]
|
||||||
|
|
||||||
jasper_regions = [
|
jasper_regions = [
|
||||||
RegionData(Region.museum, [JasperEntrance.museum_to_bedroom]),
|
RegionData(Region.museum, (JasperEntrance.museum_to_bedroom,)),
|
||||||
RegionData(JasperRegion.jasper_bedroom)
|
RegionData(JasperRegion.jasper_bedroom),
|
||||||
]
|
]
|
||||||
|
|
||||||
jasper_entrances = [
|
jasper_entrances = [
|
||||||
ConnectionData(JasperEntrance.museum_to_bedroom, JasperRegion.jasper_bedroom, flag=RandomizationFlag.BUILDINGS)
|
ConnectionData(JasperEntrance.museum_to_bedroom, JasperRegion.jasper_bedroom, flag=RandomizationFlag.BUILDINGS),
|
||||||
]
|
]
|
||||||
alec_regions = [
|
alec_regions = [
|
||||||
RegionData(Region.forest, [AlecEntrance.forest_to_petshop]),
|
RegionData(Region.forest, (AlecEntrance.forest_to_petshop,)),
|
||||||
RegionData(AlecRegion.pet_store, [AlecEntrance.petshop_to_bedroom]),
|
RegionData(AlecRegion.pet_store, (AlecEntrance.petshop_to_bedroom,)),
|
||||||
RegionData(AlecRegion.alec_bedroom)
|
RegionData(AlecRegion.alec_bedroom),
|
||||||
]
|
]
|
||||||
|
|
||||||
alec_entrances = [
|
alec_entrances = [
|
||||||
ConnectionData(AlecEntrance.forest_to_petshop, AlecRegion.pet_store,
|
ConnectionData(AlecEntrance.forest_to_petshop, AlecRegion.pet_store,
|
||||||
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
ConnectionData(AlecEntrance.petshop_to_bedroom, AlecRegion.alec_bedroom, flag=RandomizationFlag.BUILDINGS)
|
ConnectionData(AlecEntrance.petshop_to_bedroom, AlecRegion.alec_bedroom, flag=RandomizationFlag.BUILDINGS),
|
||||||
]
|
]
|
||||||
|
|
||||||
yoba_regions = [
|
yoba_regions = [
|
||||||
RegionData(Region.secret_woods, [YobaEntrance.secret_woods_to_clearing]),
|
RegionData(Region.secret_woods, (YobaEntrance.secret_woods_to_clearing,)),
|
||||||
RegionData(YobaRegion.yoba_clearing)
|
RegionData(YobaRegion.yoba_clearing),
|
||||||
]
|
]
|
||||||
|
|
||||||
yoba_entrances = [
|
yoba_entrances = [
|
||||||
ConnectionData(YobaEntrance.secret_woods_to_clearing, YobaRegion.yoba_clearing, flag=RandomizationFlag.BUILDINGS)
|
ConnectionData(YobaEntrance.secret_woods_to_clearing, YobaRegion.yoba_clearing, flag=RandomizationFlag.BUILDINGS),
|
||||||
]
|
]
|
||||||
|
|
||||||
juna_regions = [
|
juna_regions = [
|
||||||
RegionData(Region.forest, [JunaEntrance.forest_to_juna_cave]),
|
RegionData(Region.forest, (JunaEntrance.forest_to_juna_cave,)),
|
||||||
RegionData(JunaRegion.juna_cave)
|
RegionData(JunaRegion.juna_cave),
|
||||||
]
|
]
|
||||||
|
|
||||||
juna_entrances = [
|
juna_entrances = [
|
||||||
ConnectionData(JunaEntrance.forest_to_juna_cave, JunaRegion.juna_cave,
|
ConnectionData(JunaEntrance.forest_to_juna_cave, JunaRegion.juna_cave,
|
||||||
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA)
|
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
]
|
]
|
||||||
|
|
||||||
ayeisha_regions = [
|
ayeisha_regions = [
|
||||||
RegionData(Region.bus_stop, [AyeishaEntrance.bus_stop_to_mail_van]),
|
RegionData(Region.bus_stop, (AyeishaEntrance.bus_stop_to_mail_van,)),
|
||||||
RegionData(AyeishaRegion.mail_van)
|
RegionData(AyeishaRegion.mail_van),
|
||||||
]
|
]
|
||||||
|
|
||||||
ayeisha_entrances = [
|
ayeisha_entrances = [
|
||||||
ConnectionData(AyeishaEntrance.bus_stop_to_mail_van, AyeishaRegion.mail_van,
|
ConnectionData(AyeishaEntrance.bus_stop_to_mail_van, AyeishaRegion.mail_van,
|
||||||
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA)
|
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
]
|
]
|
||||||
|
|
||||||
riley_regions = [
|
riley_regions = [
|
||||||
RegionData(Region.town, [RileyEntrance.town_to_riley]),
|
RegionData(Region.town, (RileyEntrance.town_to_riley,)),
|
||||||
RegionData(RileyRegion.riley_house)
|
RegionData(RileyRegion.riley_house),
|
||||||
]
|
]
|
||||||
|
|
||||||
riley_entrances = [
|
riley_entrances = [
|
||||||
ConnectionData(RileyEntrance.town_to_riley, RileyRegion.riley_house,
|
ConnectionData(RileyEntrance.town_to_riley, RileyRegion.riley_house,
|
||||||
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA)
|
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
]
|
]
|
||||||
|
|
||||||
stardew_valley_expanded_regions = [
|
sve_main_land_regions = [
|
||||||
RegionData(Region.backwoods, [SVEEntrance.backwoods_to_grove]),
|
RegionData(Region.backwoods, (SVEEntrance.backwoods_to_grove,)),
|
||||||
RegionData(SVERegion.enchanted_grove, [SVEEntrance.grove_to_outpost_warp, SVEEntrance.grove_to_wizard_warp,
|
RegionData(SVERegion.enchanted_grove, (SVEEntrance.grove_to_outpost_warp, SVEEntrance.grove_to_wizard_warp,
|
||||||
SVEEntrance.grove_to_farm_warp, SVEEntrance.grove_to_guild_warp, SVEEntrance.grove_to_junimo_warp,
|
SVEEntrance.grove_to_farm_warp, SVEEntrance.grove_to_guild_warp, SVEEntrance.grove_to_junimo_warp,
|
||||||
SVEEntrance.grove_to_spring_warp, SVEEntrance.grove_to_aurora_warp]),
|
SVEEntrance.grove_to_spring_warp, SVEEntrance.grove_to_aurora_warp)),
|
||||||
RegionData(SVERegion.grove_farm_warp, [SVEEntrance.farm_warp_to_farm]),
|
RegionData(SVERegion.grove_farm_warp, (SVEEntrance.farm_warp_to_farm,)),
|
||||||
RegionData(SVERegion.grove_aurora_warp, [SVEEntrance.aurora_warp_to_aurora]),
|
RegionData(SVERegion.grove_aurora_warp, (SVEEntrance.aurora_warp_to_aurora,)),
|
||||||
RegionData(SVERegion.grove_guild_warp, [SVEEntrance.guild_warp_to_guild]),
|
RegionData(SVERegion.grove_guild_warp, (SVEEntrance.guild_warp_to_guild,)),
|
||||||
RegionData(SVERegion.grove_junimo_warp, [SVEEntrance.junimo_warp_to_junimo]),
|
RegionData(SVERegion.grove_junimo_warp, (SVEEntrance.junimo_warp_to_junimo,)),
|
||||||
RegionData(SVERegion.grove_spring_warp, [SVEEntrance.spring_warp_to_spring]),
|
RegionData(SVERegion.grove_spring_warp, (SVEEntrance.spring_warp_to_spring,)),
|
||||||
RegionData(SVERegion.grove_outpost_warp, [SVEEntrance.outpost_warp_to_outpost]),
|
RegionData(SVERegion.grove_outpost_warp, (SVEEntrance.outpost_warp_to_outpost,)),
|
||||||
RegionData(SVERegion.grove_wizard_warp, [SVEEntrance.wizard_warp_to_wizard]),
|
RegionData(SVERegion.grove_wizard_warp, (SVEEntrance.wizard_warp_to_wizard,)),
|
||||||
RegionData(SVERegion.galmoran_outpost, [SVEEntrance.outpost_to_badlands_entrance, SVEEntrance.use_alesia_shop,
|
RegionData(SVERegion.galmoran_outpost, (SVEEntrance.outpost_to_badlands_entrance, SVEEntrance.use_alesia_shop, SVEEntrance.use_isaac_shop)),
|
||||||
SVEEntrance.use_isaac_shop]),
|
RegionData(SVERegion.badlands_entrance, (SVEEntrance.badlands_entrance_to_badlands,)),
|
||||||
RegionData(SVERegion.badlands_entrance, [SVEEntrance.badlands_entrance_to_badlands]),
|
RegionData(SVERegion.crimson_badlands, (SVEEntrance.badlands_to_cave,)),
|
||||||
RegionData(SVERegion.crimson_badlands, [SVEEntrance.badlands_to_cave]),
|
|
||||||
RegionData(SVERegion.badlands_cave),
|
RegionData(SVERegion.badlands_cave),
|
||||||
RegionData(Region.bus_stop, [SVEEntrance.bus_stop_to_shed]),
|
RegionData(Region.bus_stop, (SVEEntrance.bus_stop_to_shed,)),
|
||||||
RegionData(SVERegion.grandpas_shed, [SVEEntrance.grandpa_shed_to_interior, SVEEntrance.grandpa_shed_to_town]),
|
RegionData(SVERegion.grandpas_shed, (SVEEntrance.grandpa_shed_to_interior, SVEEntrance.grandpa_shed_to_town)),
|
||||||
RegionData(SVERegion.grandpas_shed_interior, [SVEEntrance.grandpa_interior_to_upstairs]),
|
RegionData(SVERegion.grandpas_shed_interior, (SVEEntrance.grandpa_interior_to_upstairs,)),
|
||||||
RegionData(SVERegion.grandpas_shed_upstairs),
|
RegionData(SVERegion.grandpas_shed_upstairs),
|
||||||
RegionData(Region.forest,
|
RegionData(Region.forest,
|
||||||
[SVEEntrance.forest_to_fairhaven, SVEEntrance.forest_to_west, SVEEntrance.forest_to_lost_woods,
|
(SVEEntrance.forest_to_fairhaven, SVEEntrance.forest_to_west, SVEEntrance.forest_to_lost_woods,
|
||||||
SVEEntrance.forest_to_bmv, SVEEntrance.forest_to_marnie_shed]),
|
SVEEntrance.forest_to_bmv, SVEEntrance.forest_to_marnie_shed)),
|
||||||
RegionData(SVERegion.marnies_shed),
|
RegionData(SVERegion.marnies_shed),
|
||||||
RegionData(SVERegion.fairhaven_farm),
|
RegionData(SVERegion.fairhaven_farm),
|
||||||
RegionData(Region.town, [SVEEntrance.town_to_bmv, SVEEntrance.town_to_jenkins,
|
RegionData(Region.town, (SVEEntrance.town_to_bmv, SVEEntrance.town_to_jenkins, SVEEntrance.town_to_bridge, SVEEntrance.town_to_plot)),
|
||||||
SVEEntrance.town_to_bridge, SVEEntrance.town_to_plot]),
|
RegionData(SVERegion.blue_moon_vineyard, (SVEEntrance.bmv_to_sophia, SVEEntrance.bmv_to_beach)),
|
||||||
RegionData(SVERegion.blue_moon_vineyard, [SVEEntrance.bmv_to_sophia, SVEEntrance.bmv_to_beach]),
|
|
||||||
RegionData(SVERegion.sophias_house),
|
RegionData(SVERegion.sophias_house),
|
||||||
RegionData(SVERegion.jenkins_residence, [SVEEntrance.jenkins_to_cellar]),
|
RegionData(SVERegion.jenkins_residence, (SVEEntrance.jenkins_to_cellar,)),
|
||||||
RegionData(SVERegion.jenkins_cellar),
|
RegionData(SVERegion.jenkins_cellar),
|
||||||
RegionData(SVERegion.unclaimed_plot, [SVEEntrance.plot_to_bridge]),
|
RegionData(SVERegion.unclaimed_plot, (SVEEntrance.plot_to_bridge,)),
|
||||||
RegionData(SVERegion.shearwater),
|
RegionData(SVERegion.shearwater),
|
||||||
RegionData(Region.museum, [SVEEntrance.museum_to_gunther_bedroom]),
|
RegionData(Region.museum, (SVEEntrance.museum_to_gunther_bedroom,)),
|
||||||
RegionData(SVERegion.gunther_bedroom),
|
RegionData(SVERegion.gunther_bedroom),
|
||||||
RegionData(Region.fish_shop, [SVEEntrance.fish_shop_to_willy_bedroom]),
|
RegionData(Region.fish_shop, (SVEEntrance.fish_shop_to_willy_bedroom,)),
|
||||||
RegionData(SVERegion.willy_bedroom),
|
RegionData(SVERegion.willy_bedroom),
|
||||||
RegionData(Region.mountain, [SVEEntrance.mountain_to_guild_summit]),
|
RegionData(Region.mountain, (SVEEntrance.mountain_to_guild_summit,)),
|
||||||
RegionData(SVERegion.guild_summit, [SVEEntrance.guild_to_interior, SVEEntrance.guild_to_mines,
|
# These entrances are removed from the mountain region when SVE is enabled
|
||||||
SVEEntrance.summit_to_highlands]),
|
RegionData(Region.mountain, (Entrance.mountain_to_adventurer_guild, Entrance.mountain_to_the_mines), flag=MergeFlag.REMOVE_EXITS),
|
||||||
RegionData(Region.railroad, [SVEEntrance.to_susan_house, SVEEntrance.enter_summit, SVEEntrance.railroad_to_grampleton_station]),
|
RegionData(SVERegion.guild_summit, (SVEEntrance.guild_to_interior, SVEEntrance.guild_to_mines)),
|
||||||
RegionData(SVERegion.grampleton_station, [SVEEntrance.grampleton_station_to_grampleton_suburbs]),
|
RegionData(Region.railroad, (SVEEntrance.to_susan_house, SVEEntrance.enter_summit, SVEEntrance.railroad_to_grampleton_station)),
|
||||||
RegionData(SVERegion.grampleton_suburbs, [SVEEntrance.grampleton_suburbs_to_scarlett_house]),
|
RegionData(SVERegion.grampleton_station, (SVEEntrance.grampleton_station_to_grampleton_suburbs,)),
|
||||||
|
RegionData(SVERegion.grampleton_suburbs, (SVEEntrance.grampleton_suburbs_to_scarlett_house,)),
|
||||||
RegionData(SVERegion.scarlett_house),
|
RegionData(SVERegion.scarlett_house),
|
||||||
RegionData(Region.wizard_basement, [SVEEntrance.wizard_to_fable_reef]),
|
RegionData(SVERegion.forest_west, (SVEEntrance.forest_west_to_spring, SVEEntrance.west_to_aurora, SVEEntrance.use_bear_shop,)),
|
||||||
RegionData(SVERegion.fable_reef, [SVEEntrance.fable_reef_to_guild], is_ginger_island=True),
|
RegionData(SVERegion.aurora_vineyard, (SVEEntrance.to_aurora_basement,)),
|
||||||
RegionData(SVERegion.first_slash_guild, [SVEEntrance.first_slash_guild_to_hallway], is_ginger_island=True),
|
|
||||||
RegionData(SVERegion.first_slash_hallway, [SVEEntrance.first_slash_hallway_to_room], is_ginger_island=True),
|
|
||||||
RegionData(SVERegion.first_slash_spare_room, is_ginger_island=True),
|
|
||||||
RegionData(SVERegion.highlands_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave, SVEEntrance.highlands_to_pond], is_ginger_island=True),
|
|
||||||
RegionData(SVERegion.highlands_pond, is_ginger_island=True),
|
|
||||||
RegionData(SVERegion.highlands_cavern, [SVEEntrance.to_dwarf_prison], is_ginger_island=True),
|
|
||||||
RegionData(SVERegion.dwarf_prison, is_ginger_island=True),
|
|
||||||
RegionData(SVERegion.lances_house, [SVEEntrance.lance_to_ladder], is_ginger_island=True),
|
|
||||||
RegionData(SVERegion.lances_ladder, [SVEEntrance.lance_ladder_to_highlands], is_ginger_island=True),
|
|
||||||
RegionData(SVERegion.forest_west, [SVEEntrance.forest_west_to_spring, SVEEntrance.west_to_aurora,
|
|
||||||
SVEEntrance.use_bear_shop]),
|
|
||||||
RegionData(SVERegion.aurora_vineyard, [SVEEntrance.to_aurora_basement]),
|
|
||||||
RegionData(SVERegion.aurora_vineyard_basement),
|
RegionData(SVERegion.aurora_vineyard_basement),
|
||||||
RegionData(Region.secret_woods, [SVEEntrance.secret_woods_to_west]),
|
RegionData(Region.secret_woods, (SVEEntrance.secret_woods_to_west,)),
|
||||||
RegionData(SVERegion.bear_shop),
|
RegionData(SVERegion.bear_shop),
|
||||||
RegionData(SVERegion.sprite_spring, [SVEEntrance.sprite_spring_to_cave]),
|
RegionData(SVERegion.sprite_spring, (SVEEntrance.sprite_spring_to_cave,)),
|
||||||
RegionData(SVERegion.sprite_spring_cave),
|
RegionData(SVERegion.sprite_spring_cave),
|
||||||
RegionData(SVERegion.lost_woods, [SVEEntrance.lost_woods_to_junimo_woods]),
|
RegionData(SVERegion.lost_woods, (SVEEntrance.lost_woods_to_junimo_woods,)),
|
||||||
RegionData(SVERegion.junimo_woods, [SVEEntrance.use_purple_junimo]),
|
RegionData(SVERegion.junimo_woods, (SVEEntrance.use_purple_junimo,)),
|
||||||
RegionData(SVERegion.purple_junimo_shop),
|
RegionData(SVERegion.purple_junimo_shop),
|
||||||
RegionData(SVERegion.alesia_shop),
|
RegionData(SVERegion.alesia_shop),
|
||||||
RegionData(SVERegion.isaac_shop),
|
RegionData(SVERegion.isaac_shop),
|
||||||
RegionData(SVERegion.summit),
|
RegionData(SVERegion.summit),
|
||||||
RegionData(SVERegion.susans_house),
|
RegionData(SVERegion.susans_house),
|
||||||
RegionData(Region.mountain, [Entrance.mountain_to_adventurer_guild, Entrance.mountain_to_the_mines], ModificationFlag.MODIFIED)
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
mandatory_sve_connections = [
|
sve_ginger_island_regions = [
|
||||||
|
RegionData(Region.wizard_basement, (SVEEntrance.wizard_to_fable_reef,)),
|
||||||
|
|
||||||
|
RegionData(SVERegion.fable_reef, (SVEEntrance.fable_reef_to_guild,)),
|
||||||
|
RegionData(SVERegion.first_slash_guild, (SVEEntrance.first_slash_guild_to_hallway,)),
|
||||||
|
RegionData(SVERegion.first_slash_hallway, (SVEEntrance.first_slash_hallway_to_room,)),
|
||||||
|
RegionData(SVERegion.first_slash_spare_room),
|
||||||
|
RegionData(SVERegion.guild_summit, (SVEEntrance.summit_to_highlands,)),
|
||||||
|
RegionData(SVERegion.highlands_outside, (SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave, SVEEntrance.highlands_to_pond), ),
|
||||||
|
RegionData(SVERegion.highlands_pond),
|
||||||
|
RegionData(SVERegion.highlands_cavern, (SVEEntrance.to_dwarf_prison,)),
|
||||||
|
RegionData(SVERegion.dwarf_prison),
|
||||||
|
RegionData(SVERegion.lances_house, (SVEEntrance.lance_to_ladder,)),
|
||||||
|
RegionData(SVERegion.lances_ladder, (SVEEntrance.lance_ladder_to_highlands,)),
|
||||||
|
]
|
||||||
|
|
||||||
|
sve_main_land_connections = [
|
||||||
ConnectionData(SVEEntrance.town_to_jenkins, SVERegion.jenkins_residence, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
ConnectionData(SVEEntrance.town_to_jenkins, SVERegion.jenkins_residence, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
ConnectionData(SVEEntrance.jenkins_to_cellar, SVERegion.jenkins_cellar, flag=RandomizationFlag.BUILDINGS),
|
ConnectionData(SVEEntrance.jenkins_to_cellar, SVERegion.jenkins_cellar, flag=RandomizationFlag.BUILDINGS),
|
||||||
ConnectionData(SVEEntrance.forest_to_bmv, SVERegion.blue_moon_vineyard),
|
ConnectionData(SVEEntrance.forest_to_bmv, SVERegion.blue_moon_vineyard),
|
||||||
@@ -223,7 +222,7 @@ mandatory_sve_connections = [
|
|||||||
ConnectionData(SVEEntrance.grandpa_interior_to_upstairs, SVERegion.grandpas_shed_upstairs, flag=RandomizationFlag.BUILDINGS),
|
ConnectionData(SVEEntrance.grandpa_interior_to_upstairs, SVERegion.grandpas_shed_upstairs, flag=RandomizationFlag.BUILDINGS),
|
||||||
ConnectionData(SVEEntrance.grandpa_shed_to_town, Region.town),
|
ConnectionData(SVEEntrance.grandpa_shed_to_town, Region.town),
|
||||||
ConnectionData(SVEEntrance.bmv_to_sophia, SVERegion.sophias_house, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
ConnectionData(SVEEntrance.bmv_to_sophia, SVERegion.sophias_house, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
ConnectionData(SVEEntrance.summit_to_highlands, SVERegion.highlands_outside, flag=RandomizationFlag.GINGER_ISLAND),
|
ConnectionData(SVEEntrance.summit_to_highlands, SVERegion.highlands_outside),
|
||||||
ConnectionData(SVEEntrance.guild_to_interior, Region.adventurer_guild, flag=RandomizationFlag.BUILDINGS),
|
ConnectionData(SVEEntrance.guild_to_interior, Region.adventurer_guild, flag=RandomizationFlag.BUILDINGS),
|
||||||
ConnectionData(SVEEntrance.backwoods_to_grove, SVERegion.enchanted_grove, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
ConnectionData(SVEEntrance.backwoods_to_grove, SVERegion.enchanted_grove, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
ConnectionData(SVEEntrance.grove_to_outpost_warp, SVERegion.grove_outpost_warp),
|
ConnectionData(SVEEntrance.grove_to_outpost_warp, SVERegion.grove_outpost_warp),
|
||||||
@@ -242,8 +241,6 @@ mandatory_sve_connections = [
|
|||||||
ConnectionData(SVEEntrance.use_purple_junimo, SVERegion.purple_junimo_shop),
|
ConnectionData(SVEEntrance.use_purple_junimo, SVERegion.purple_junimo_shop),
|
||||||
ConnectionData(SVEEntrance.grove_to_spring_warp, SVERegion.grove_spring_warp),
|
ConnectionData(SVEEntrance.grove_to_spring_warp, SVERegion.grove_spring_warp),
|
||||||
ConnectionData(SVEEntrance.spring_warp_to_spring, SVERegion.sprite_spring, flag=RandomizationFlag.BUILDINGS),
|
ConnectionData(SVEEntrance.spring_warp_to_spring, SVERegion.sprite_spring, flag=RandomizationFlag.BUILDINGS),
|
||||||
ConnectionData(SVEEntrance.wizard_to_fable_reef, SVERegion.fable_reef, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(SVEEntrance.fable_reef_to_guild, SVERegion.first_slash_guild, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(SVEEntrance.outpost_to_badlands_entrance, SVERegion.badlands_entrance, flag=RandomizationFlag.BUILDINGS),
|
ConnectionData(SVEEntrance.outpost_to_badlands_entrance, SVERegion.badlands_entrance, flag=RandomizationFlag.BUILDINGS),
|
||||||
ConnectionData(SVEEntrance.badlands_entrance_to_badlands, SVERegion.crimson_badlands, flag=RandomizationFlag.BUILDINGS),
|
ConnectionData(SVEEntrance.badlands_entrance_to_badlands, SVERegion.crimson_badlands, flag=RandomizationFlag.BUILDINGS),
|
||||||
ConnectionData(SVEEntrance.badlands_to_cave, SVERegion.badlands_cave, flag=RandomizationFlag.BUILDINGS),
|
ConnectionData(SVEEntrance.badlands_to_cave, SVERegion.badlands_cave, flag=RandomizationFlag.BUILDINGS),
|
||||||
@@ -259,71 +256,75 @@ mandatory_sve_connections = [
|
|||||||
ConnectionData(SVEEntrance.to_susan_house, SVERegion.susans_house, flag=RandomizationFlag.BUILDINGS),
|
ConnectionData(SVEEntrance.to_susan_house, SVERegion.susans_house, flag=RandomizationFlag.BUILDINGS),
|
||||||
ConnectionData(SVEEntrance.enter_summit, SVERegion.summit, flag=RandomizationFlag.BUILDINGS),
|
ConnectionData(SVEEntrance.enter_summit, SVERegion.summit, flag=RandomizationFlag.BUILDINGS),
|
||||||
ConnectionData(SVEEntrance.forest_to_fairhaven, SVERegion.fairhaven_farm, flag=RandomizationFlag.NON_PROGRESSION),
|
ConnectionData(SVEEntrance.forest_to_fairhaven, SVERegion.fairhaven_farm, flag=RandomizationFlag.NON_PROGRESSION),
|
||||||
ConnectionData(SVEEntrance.highlands_to_lance, SVERegion.lances_house, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(SVEEntrance.lance_to_ladder, SVERegion.lances_ladder, flag=RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(SVEEntrance.lance_ladder_to_highlands, SVERegion.highlands_outside, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(SVEEntrance.highlands_to_cave, SVERegion.highlands_cavern, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(SVEEntrance.use_bear_shop, SVERegion.bear_shop),
|
ConnectionData(SVEEntrance.use_bear_shop, SVERegion.bear_shop),
|
||||||
ConnectionData(SVEEntrance.use_purple_junimo, SVERegion.purple_junimo_shop),
|
ConnectionData(SVEEntrance.use_purple_junimo, SVERegion.purple_junimo_shop),
|
||||||
ConnectionData(SVEEntrance.use_alesia_shop, SVERegion.alesia_shop),
|
ConnectionData(SVEEntrance.use_alesia_shop, SVERegion.alesia_shop),
|
||||||
ConnectionData(SVEEntrance.use_isaac_shop, SVERegion.isaac_shop),
|
ConnectionData(SVEEntrance.use_isaac_shop, SVERegion.isaac_shop),
|
||||||
ConnectionData(SVEEntrance.to_dwarf_prison, SVERegion.dwarf_prison, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(SVEEntrance.railroad_to_grampleton_station, SVERegion.grampleton_station),
|
ConnectionData(SVEEntrance.railroad_to_grampleton_station, SVERegion.grampleton_station),
|
||||||
ConnectionData(SVEEntrance.grampleton_station_to_grampleton_suburbs, SVERegion.grampleton_suburbs),
|
ConnectionData(SVEEntrance.grampleton_station_to_grampleton_suburbs, SVERegion.grampleton_suburbs),
|
||||||
ConnectionData(SVEEntrance.grampleton_suburbs_to_scarlett_house, SVERegion.scarlett_house, flag=RandomizationFlag.BUILDINGS),
|
ConnectionData(SVEEntrance.grampleton_suburbs_to_scarlett_house, SVERegion.scarlett_house, flag=RandomizationFlag.BUILDINGS),
|
||||||
ConnectionData(SVEEntrance.first_slash_guild_to_hallway, SVERegion.first_slash_hallway, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(SVEEntrance.first_slash_hallway_to_room, SVERegion.first_slash_spare_room,
|
|
||||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(SVEEntrance.sprite_spring_to_cave, SVERegion.sprite_spring_cave, flag=RandomizationFlag.BUILDINGS),
|
ConnectionData(SVEEntrance.sprite_spring_to_cave, SVERegion.sprite_spring_cave, flag=RandomizationFlag.BUILDINGS),
|
||||||
ConnectionData(SVEEntrance.fish_shop_to_willy_bedroom, SVERegion.willy_bedroom, flag=RandomizationFlag.BUILDINGS),
|
ConnectionData(SVEEntrance.fish_shop_to_willy_bedroom, SVERegion.willy_bedroom, flag=RandomizationFlag.BUILDINGS),
|
||||||
ConnectionData(SVEEntrance.museum_to_gunther_bedroom, SVERegion.gunther_bedroom, flag=RandomizationFlag.BUILDINGS),
|
ConnectionData(SVEEntrance.museum_to_gunther_bedroom, SVERegion.gunther_bedroom, flag=RandomizationFlag.BUILDINGS),
|
||||||
ConnectionData(SVEEntrance.highlands_to_pond, SVERegion.highlands_pond),
|
ConnectionData(SVEEntrance.highlands_to_pond, SVERegion.highlands_pond),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
sve_ginger_island_connections = [
|
||||||
|
ConnectionData(SVEEntrance.wizard_to_fable_reef, SVERegion.fable_reef, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(SVEEntrance.fable_reef_to_guild, SVERegion.first_slash_guild, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(SVEEntrance.highlands_to_lance, SVERegion.lances_house, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(SVEEntrance.lance_to_ladder, SVERegion.lances_ladder),
|
||||||
|
ConnectionData(SVEEntrance.lance_ladder_to_highlands, SVERegion.highlands_outside, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(SVEEntrance.highlands_to_cave, SVERegion.highlands_cavern, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(SVEEntrance.to_dwarf_prison, SVERegion.dwarf_prison, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(SVEEntrance.first_slash_guild_to_hallway, SVERegion.first_slash_hallway, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(SVEEntrance.first_slash_hallway_to_room, SVERegion.first_slash_spare_room, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
]
|
||||||
|
|
||||||
alecto_regions = [
|
alecto_regions = [
|
||||||
RegionData(Region.witch_hut, [AlectoEntrance.witch_hut_to_witch_attic]),
|
RegionData(Region.witch_hut, (AlectoEntrance.witch_hut_to_witch_attic,)),
|
||||||
RegionData(AlectoRegion.witch_attic)
|
RegionData(AlectoRegion.witch_attic),
|
||||||
]
|
]
|
||||||
|
|
||||||
alecto_entrances = [
|
alecto_entrances = [
|
||||||
ConnectionData(AlectoEntrance.witch_hut_to_witch_attic, AlectoRegion.witch_attic, flag=RandomizationFlag.BUILDINGS)
|
ConnectionData(AlectoEntrance.witch_hut_to_witch_attic, AlectoRegion.witch_attic, flag=RandomizationFlag.BUILDINGS),
|
||||||
]
|
]
|
||||||
|
|
||||||
lacey_regions = [
|
lacey_regions = [
|
||||||
RegionData(Region.forest, [LaceyEntrance.forest_to_hat_house]),
|
RegionData(Region.forest, (LaceyEntrance.forest_to_hat_house,)),
|
||||||
RegionData(LaceyRegion.hat_house)
|
RegionData(LaceyRegion.hat_house),
|
||||||
]
|
]
|
||||||
|
|
||||||
lacey_entrances = [
|
lacey_entrances = [
|
||||||
ConnectionData(LaceyEntrance.forest_to_hat_house, LaceyRegion.hat_house, flag=RandomizationFlag.BUILDINGS)
|
ConnectionData(LaceyEntrance.forest_to_hat_house, LaceyRegion.hat_house, flag=RandomizationFlag.BUILDINGS),
|
||||||
]
|
]
|
||||||
|
|
||||||
boarding_house_regions = [
|
boarding_house_regions = [
|
||||||
RegionData(Region.bus_stop, [BoardingHouseEntrance.bus_stop_to_boarding_house_plateau]),
|
RegionData(Region.bus_stop, (BoardingHouseEntrance.bus_stop_to_boarding_house_plateau,)),
|
||||||
RegionData(BoardingHouseRegion.boarding_house_plateau, [BoardingHouseEntrance.boarding_house_plateau_to_boarding_house_first,
|
RegionData(BoardingHouseRegion.boarding_house_plateau, (BoardingHouseEntrance.boarding_house_plateau_to_boarding_house_first,
|
||||||
BoardingHouseEntrance.boarding_house_plateau_to_buffalo_ranch,
|
BoardingHouseEntrance.boarding_house_plateau_to_buffalo_ranch,
|
||||||
BoardingHouseEntrance.boarding_house_plateau_to_abandoned_mines_entrance]),
|
BoardingHouseEntrance.boarding_house_plateau_to_abandoned_mines_entrance)),
|
||||||
RegionData(BoardingHouseRegion.boarding_house_first, [BoardingHouseEntrance.boarding_house_first_to_boarding_house_second]),
|
RegionData(BoardingHouseRegion.boarding_house_first, (BoardingHouseEntrance.boarding_house_first_to_boarding_house_second,)),
|
||||||
RegionData(BoardingHouseRegion.boarding_house_second),
|
RegionData(BoardingHouseRegion.boarding_house_second),
|
||||||
RegionData(BoardingHouseRegion.buffalo_ranch),
|
RegionData(BoardingHouseRegion.buffalo_ranch),
|
||||||
RegionData(BoardingHouseRegion.abandoned_mines_entrance, [BoardingHouseEntrance.abandoned_mines_entrance_to_abandoned_mines_1a,
|
RegionData(BoardingHouseRegion.abandoned_mines_entrance, (BoardingHouseEntrance.abandoned_mines_entrance_to_abandoned_mines_1a,
|
||||||
BoardingHouseEntrance.abandoned_mines_entrance_to_the_lost_valley]),
|
BoardingHouseEntrance.abandoned_mines_entrance_to_the_lost_valley)),
|
||||||
RegionData(BoardingHouseRegion.abandoned_mines_1a, [BoardingHouseEntrance.abandoned_mines_1a_to_abandoned_mines_1b]),
|
RegionData(BoardingHouseRegion.abandoned_mines_1a, (BoardingHouseEntrance.abandoned_mines_1a_to_abandoned_mines_1b,)),
|
||||||
RegionData(BoardingHouseRegion.abandoned_mines_1b, [BoardingHouseEntrance.abandoned_mines_1b_to_abandoned_mines_2a]),
|
RegionData(BoardingHouseRegion.abandoned_mines_1b, (BoardingHouseEntrance.abandoned_mines_1b_to_abandoned_mines_2a,)),
|
||||||
RegionData(BoardingHouseRegion.abandoned_mines_2a, [BoardingHouseEntrance.abandoned_mines_2a_to_abandoned_mines_2b]),
|
RegionData(BoardingHouseRegion.abandoned_mines_2a, (BoardingHouseEntrance.abandoned_mines_2a_to_abandoned_mines_2b,)),
|
||||||
RegionData(BoardingHouseRegion.abandoned_mines_2b, [BoardingHouseEntrance.abandoned_mines_2b_to_abandoned_mines_3]),
|
RegionData(BoardingHouseRegion.abandoned_mines_2b, (BoardingHouseEntrance.abandoned_mines_2b_to_abandoned_mines_3,)),
|
||||||
RegionData(BoardingHouseRegion.abandoned_mines_3, [BoardingHouseEntrance.abandoned_mines_3_to_abandoned_mines_4]),
|
RegionData(BoardingHouseRegion.abandoned_mines_3, (BoardingHouseEntrance.abandoned_mines_3_to_abandoned_mines_4,)),
|
||||||
RegionData(BoardingHouseRegion.abandoned_mines_4, [BoardingHouseEntrance.abandoned_mines_4_to_abandoned_mines_5]),
|
RegionData(BoardingHouseRegion.abandoned_mines_4, (BoardingHouseEntrance.abandoned_mines_4_to_abandoned_mines_5,)),
|
||||||
RegionData(BoardingHouseRegion.abandoned_mines_5, [BoardingHouseEntrance.abandoned_mines_5_to_the_lost_valley]),
|
RegionData(BoardingHouseRegion.abandoned_mines_5, (BoardingHouseEntrance.abandoned_mines_5_to_the_lost_valley,)),
|
||||||
RegionData(BoardingHouseRegion.the_lost_valley, [BoardingHouseEntrance.the_lost_valley_to_gregory_tent,
|
RegionData(BoardingHouseRegion.the_lost_valley, (BoardingHouseEntrance.the_lost_valley_to_gregory_tent,
|
||||||
BoardingHouseEntrance.lost_valley_to_lost_valley_minecart,
|
BoardingHouseEntrance.lost_valley_to_lost_valley_minecart,
|
||||||
BoardingHouseEntrance.the_lost_valley_to_lost_valley_ruins]),
|
BoardingHouseEntrance.the_lost_valley_to_lost_valley_ruins)),
|
||||||
RegionData(BoardingHouseRegion.gregory_tent),
|
RegionData(BoardingHouseRegion.gregory_tent),
|
||||||
RegionData(BoardingHouseRegion.lost_valley_ruins, [BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1,
|
RegionData(BoardingHouseRegion.lost_valley_ruins, (BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1,
|
||||||
BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2]),
|
BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2)),
|
||||||
RegionData(BoardingHouseRegion.lost_valley_minecart),
|
RegionData(BoardingHouseRegion.lost_valley_minecart),
|
||||||
RegionData(BoardingHouseRegion.lost_valley_house_1),
|
RegionData(BoardingHouseRegion.lost_valley_house_1),
|
||||||
RegionData(BoardingHouseRegion.lost_valley_house_2)
|
RegionData(BoardingHouseRegion.lost_valley_house_2),
|
||||||
]
|
]
|
||||||
|
|
||||||
boarding_house_entrances = [
|
boarding_house_entrances = [
|
||||||
@@ -351,30 +352,29 @@ boarding_house_entrances = [
|
|||||||
ConnectionData(BoardingHouseEntrance.lost_valley_to_lost_valley_minecart, BoardingHouseRegion.lost_valley_minecart),
|
ConnectionData(BoardingHouseEntrance.lost_valley_to_lost_valley_minecart, BoardingHouseRegion.lost_valley_minecart),
|
||||||
ConnectionData(BoardingHouseEntrance.the_lost_valley_to_lost_valley_ruins, BoardingHouseRegion.lost_valley_ruins, flag=RandomizationFlag.BUILDINGS),
|
ConnectionData(BoardingHouseEntrance.the_lost_valley_to_lost_valley_ruins, BoardingHouseRegion.lost_valley_ruins, flag=RandomizationFlag.BUILDINGS),
|
||||||
ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1, BoardingHouseRegion.lost_valley_house_1, flag=RandomizationFlag.BUILDINGS),
|
ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1, BoardingHouseRegion.lost_valley_house_1, flag=RandomizationFlag.BUILDINGS),
|
||||||
ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2, BoardingHouseRegion.lost_valley_house_2, flag=RandomizationFlag.BUILDINGS)
|
ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2, BoardingHouseRegion.lost_valley_house_2, flag=RandomizationFlag.BUILDINGS),
|
||||||
]
|
]
|
||||||
|
|
||||||
vanilla_connections_to_remove_by_mod: Dict[str, List[ConnectionData]] = {
|
vanilla_connections_to_remove_by_content_pack: dict[str, tuple[str, ...]] = {
|
||||||
ModNames.sve: [
|
ModNames.sve: (
|
||||||
ConnectionData(Entrance.mountain_to_the_mines, Region.mines,
|
Entrance.mountain_to_the_mines,
|
||||||
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
Entrance.mountain_to_adventurer_guild,
|
||||||
ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild,
|
)
|
||||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ModDataList = {
|
region_data_by_content_pack = {
|
||||||
ModNames.deepwoods: ModRegionData(ModNames.deepwoods, deep_woods_regions, deep_woods_entrances),
|
ModNames.deepwoods: ModRegionsData(ModNames.deepwoods, deep_woods_regions, deep_woods_entrances),
|
||||||
ModNames.eugene: ModRegionData(ModNames.eugene, eugene_regions, eugene_entrances),
|
ModNames.eugene: ModRegionsData(ModNames.eugene, eugene_regions, eugene_entrances),
|
||||||
ModNames.jasper: ModRegionData(ModNames.jasper, jasper_regions, jasper_entrances),
|
ModNames.jasper: ModRegionsData(ModNames.jasper, jasper_regions, jasper_entrances),
|
||||||
ModNames.alec: ModRegionData(ModNames.alec, alec_regions, alec_entrances),
|
ModNames.alec: ModRegionsData(ModNames.alec, alec_regions, alec_entrances),
|
||||||
ModNames.yoba: ModRegionData(ModNames.yoba, yoba_regions, yoba_entrances),
|
ModNames.yoba: ModRegionsData(ModNames.yoba, yoba_regions, yoba_entrances),
|
||||||
ModNames.juna: ModRegionData(ModNames.juna, juna_regions, juna_entrances),
|
ModNames.juna: ModRegionsData(ModNames.juna, juna_regions, juna_entrances),
|
||||||
ModNames.magic: ModRegionData(ModNames.magic, magic_regions, magic_entrances),
|
ModNames.magic: ModRegionsData(ModNames.magic, magic_regions, magic_entrances),
|
||||||
ModNames.ayeisha: ModRegionData(ModNames.ayeisha, ayeisha_regions, ayeisha_entrances),
|
ModNames.ayeisha: ModRegionsData(ModNames.ayeisha, ayeisha_regions, ayeisha_entrances),
|
||||||
ModNames.riley: ModRegionData(ModNames.riley, riley_regions, riley_entrances),
|
ModNames.riley: ModRegionsData(ModNames.riley, riley_regions, riley_entrances),
|
||||||
ModNames.sve: ModRegionData(ModNames.sve, stardew_valley_expanded_regions, mandatory_sve_connections),
|
ModNames.sve: ModRegionsData(ModNames.sve, sve_main_land_regions, sve_main_land_connections),
|
||||||
ModNames.alecto: ModRegionData(ModNames.alecto, alecto_regions, alecto_entrances),
|
SVE_GINGER_ISLAND_PACK: ModRegionsData(SVE_GINGER_ISLAND_PACK, sve_ginger_island_regions, sve_ginger_island_connections),
|
||||||
ModNames.lacey: ModRegionData(ModNames.lacey, lacey_regions, lacey_entrances),
|
ModNames.alecto: ModRegionsData(ModNames.alecto, alecto_regions, alecto_entrances),
|
||||||
ModNames.boarding_house: ModRegionData(ModNames.boarding_house, boarding_house_regions, boarding_house_entrances),
|
ModNames.lacey: ModRegionsData(ModNames.lacey, lacey_regions, lacey_entrances),
|
||||||
|
ModNames.boarding_house: ModRegionsData(ModNames.boarding_house, boarding_house_regions, boarding_house_entrances),
|
||||||
}
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
from copy import deepcopy
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from enum import IntFlag
|
|
||||||
from typing import Optional, List, Set
|
|
||||||
|
|
||||||
connector_keyword = " to "
|
|
||||||
|
|
||||||
|
|
||||||
class ModificationFlag(IntFlag):
|
|
||||||
NOT_MODIFIED = 0
|
|
||||||
MODIFIED = 1
|
|
||||||
|
|
||||||
|
|
||||||
class RandomizationFlag(IntFlag):
|
|
||||||
NOT_RANDOMIZED = 0b0
|
|
||||||
PELICAN_TOWN = 0b00011111
|
|
||||||
NON_PROGRESSION = 0b00011110
|
|
||||||
BUILDINGS = 0b00011100
|
|
||||||
EVERYTHING = 0b00011000
|
|
||||||
GINGER_ISLAND = 0b00100000
|
|
||||||
LEAD_TO_OPEN_AREA = 0b01000000
|
|
||||||
MASTERIES = 0b10000000
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class RegionData:
|
|
||||||
name: str
|
|
||||||
exits: List[str] = field(default_factory=list)
|
|
||||||
flag: ModificationFlag = ModificationFlag.NOT_MODIFIED
|
|
||||||
is_ginger_island: bool = False
|
|
||||||
|
|
||||||
def get_merged_with(self, exits: List[str]):
|
|
||||||
merged_exits = []
|
|
||||||
merged_exits.extend(self.exits)
|
|
||||||
if exits is not None:
|
|
||||||
merged_exits.extend(exits)
|
|
||||||
merged_exits = sorted(set(merged_exits))
|
|
||||||
return RegionData(self.name, merged_exits, is_ginger_island=self.is_ginger_island)
|
|
||||||
|
|
||||||
def get_without_exits(self, exits_to_remove: Set[str]):
|
|
||||||
exits = [exit_ for exit_ in self.exits if exit_ not in exits_to_remove]
|
|
||||||
return RegionData(self.name, exits, is_ginger_island=self.is_ginger_island)
|
|
||||||
|
|
||||||
def get_clone(self):
|
|
||||||
return deepcopy(self)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ConnectionData:
|
|
||||||
name: str
|
|
||||||
destination: str
|
|
||||||
origin: Optional[str] = None
|
|
||||||
reverse: Optional[str] = None
|
|
||||||
flag: RandomizationFlag = RandomizationFlag.NOT_RANDOMIZED
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
if connector_keyword in self.name:
|
|
||||||
origin, destination = self.name.split(connector_keyword)
|
|
||||||
if self.reverse is None:
|
|
||||||
super().__setattr__("reverse", f"{destination}{connector_keyword}{origin}")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ModRegionData:
|
|
||||||
mod_name: str
|
|
||||||
regions: List[RegionData]
|
|
||||||
connections: List[ConnectionData]
|
|
||||||
@@ -1,775 +0,0 @@
|
|||||||
from random import Random
|
|
||||||
from typing import Iterable, Dict, Protocol, List, Tuple, Set
|
|
||||||
|
|
||||||
from BaseClasses import Region, Entrance
|
|
||||||
from .content import content_packs, StardewContent
|
|
||||||
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 as RegionName, LogicRegion
|
|
||||||
|
|
||||||
|
|
||||||
class RegionFactory(Protocol):
|
|
||||||
def __call__(self, name: str, regions: Iterable[str]) -> Region:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
vanilla_regions = [
|
|
||||||
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(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,
|
|
||||||
LogicEntrance.fishing, ]),
|
|
||||||
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(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(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(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(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(RegionName.beach,
|
|
||||||
[Entrance.beach_to_willy_fish_shop, Entrance.enter_elliott_house, Entrance.enter_tide_pools, LogicEntrance.attend_luau,
|
|
||||||
LogicEntrance.attend_moonlight_jellies, LogicEntrance.attend_night_market, LogicEntrance.attend_squidfest]),
|
|
||||||
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(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(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(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(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(RegionName.professor_snail_cave, is_ginger_island=True),
|
|
||||||
RegionData(RegionName.coop),
|
|
||||||
RegionData(RegionName.barn),
|
|
||||||
RegionData(RegionName.shed),
|
|
||||||
RegionData(RegionName.slime_hutch),
|
|
||||||
|
|
||||||
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]),
|
|
||||||
RegionData(LogicRegion.blacksmith_iron, [LogicEntrance.blacksmith_gold]),
|
|
||||||
RegionData(LogicRegion.blacksmith_gold, [LogicEntrance.blacksmith_iridium]),
|
|
||||||
RegionData(LogicRegion.blacksmith_iridium),
|
|
||||||
RegionData(LogicRegion.kitchen),
|
|
||||||
RegionData(LogicRegion.queen_of_sauce),
|
|
||||||
RegionData(LogicRegion.fishing),
|
|
||||||
|
|
||||||
RegionData(LogicRegion.spring_farming),
|
|
||||||
RegionData(LogicRegion.summer_farming, [LogicEntrance.grow_summer_fall_crops_in_summer]),
|
|
||||||
RegionData(LogicRegion.fall_farming, [LogicEntrance.grow_summer_fall_crops_in_fall]),
|
|
||||||
RegionData(LogicRegion.winter_farming),
|
|
||||||
RegionData(LogicRegion.summer_or_fall_farming),
|
|
||||||
RegionData(LogicRegion.indoor_farming),
|
|
||||||
|
|
||||||
RegionData(LogicRegion.shipping),
|
|
||||||
RegionData(LogicRegion.traveling_cart, [LogicEntrance.buy_from_traveling_merchant_sunday,
|
|
||||||
LogicEntrance.buy_from_traveling_merchant_monday,
|
|
||||||
LogicEntrance.buy_from_traveling_merchant_tuesday,
|
|
||||||
LogicEntrance.buy_from_traveling_merchant_wednesday,
|
|
||||||
LogicEntrance.buy_from_traveling_merchant_thursday,
|
|
||||||
LogicEntrance.buy_from_traveling_merchant_friday,
|
|
||||||
LogicEntrance.buy_from_traveling_merchant_saturday]),
|
|
||||||
RegionData(LogicRegion.traveling_cart_sunday),
|
|
||||||
RegionData(LogicRegion.traveling_cart_monday),
|
|
||||||
RegionData(LogicRegion.traveling_cart_tuesday),
|
|
||||||
RegionData(LogicRegion.traveling_cart_wednesday),
|
|
||||||
RegionData(LogicRegion.traveling_cart_thursday),
|
|
||||||
RegionData(LogicRegion.traveling_cart_friday),
|
|
||||||
RegionData(LogicRegion.traveling_cart_saturday),
|
|
||||||
RegionData(LogicRegion.raccoon_daddy, [LogicEntrance.buy_from_raccoon]),
|
|
||||||
RegionData(LogicRegion.raccoon_shop),
|
|
||||||
|
|
||||||
RegionData(LogicRegion.egg_festival),
|
|
||||||
RegionData(LogicRegion.desert_festival),
|
|
||||||
RegionData(LogicRegion.flower_dance),
|
|
||||||
RegionData(LogicRegion.luau),
|
|
||||||
RegionData(LogicRegion.trout_derby),
|
|
||||||
RegionData(LogicRegion.moonlight_jellies),
|
|
||||||
RegionData(LogicRegion.fair),
|
|
||||||
RegionData(LogicRegion.spirit_eve),
|
|
||||||
RegionData(LogicRegion.festival_of_ice),
|
|
||||||
RegionData(LogicRegion.night_market),
|
|
||||||
RegionData(LogicRegion.winter_star),
|
|
||||||
RegionData(LogicRegion.squidfest),
|
|
||||||
RegionData(LogicRegion.bookseller_1, [LogicEntrance.buy_year1_books]),
|
|
||||||
RegionData(LogicRegion.bookseller_2, [LogicEntrance.buy_year3_books]),
|
|
||||||
RegionData(LogicRegion.bookseller_3),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Exists and where they lead
|
|
||||||
vanilla_connections = [
|
|
||||||
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, 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, RegionName.leah_house,
|
|
||||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
|
||||||
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, RegionName.leo_treehouse,
|
|
||||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(Entrance.mountain_to_carpenter_shop, RegionName.carpenter,
|
|
||||||
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
|
||||||
ConnectionData(Entrance.mountain_to_maru_room, RegionName.maru_room,
|
|
||||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
|
||||||
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, 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, 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, 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, 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, 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, RegionName.saloon,
|
|
||||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
|
||||||
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, RegionName.haley_house,
|
|
||||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
|
||||||
ConnectionData(Entrance.town_to_mayor_manor, RegionName.mayor_house,
|
|
||||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
|
||||||
ConnectionData(Entrance.town_to_alex_house, RegionName.alex_house,
|
|
||||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
|
||||||
ConnectionData(Entrance.town_to_trailer, RegionName.trailer,
|
|
||||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
|
||||||
ConnectionData(Entrance.town_to_museum, RegionName.museum,
|
|
||||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
|
||||||
ConnectionData(Entrance.town_to_jojamart, RegionName.jojamart,
|
|
||||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
|
||||||
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, RegionName.fish_shop,
|
|
||||||
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
|
||||||
ConnectionData(Entrance.fish_shop_to_boat_tunnel, RegionName.boat_tunnel,
|
|
||||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
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, 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, RegionName.oasis,
|
|
||||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
|
||||||
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, 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, 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, RegionName.gourmand_frog_cave,
|
|
||||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(Entrance.island_west_to_crystals_cave, RegionName.colored_crystals_cave,
|
|
||||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(Entrance.island_west_to_shipwreck, RegionName.shipwreck,
|
|
||||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
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, RegionName.island_shrine,
|
|
||||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(Entrance.island_southeast_to_pirate_cove, RegionName.pirate_cove,
|
|
||||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(Entrance.island_north_to_field_office, RegionName.field_office,
|
|
||||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
|
|
||||||
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.island_north_to_volcano, RegionName.volcano,
|
|
||||||
flag=RandomizationFlag.BUILDINGS | 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),
|
|
||||||
|
|
||||||
ConnectionData(LogicEntrance.buy_from_traveling_merchant, LogicRegion.traveling_cart),
|
|
||||||
ConnectionData(LogicEntrance.buy_from_traveling_merchant_sunday, LogicRegion.traveling_cart_sunday),
|
|
||||||
ConnectionData(LogicEntrance.buy_from_traveling_merchant_monday, LogicRegion.traveling_cart_monday),
|
|
||||||
ConnectionData(LogicEntrance.buy_from_traveling_merchant_tuesday, LogicRegion.traveling_cart_tuesday),
|
|
||||||
ConnectionData(LogicEntrance.buy_from_traveling_merchant_wednesday, LogicRegion.traveling_cart_wednesday),
|
|
||||||
ConnectionData(LogicEntrance.buy_from_traveling_merchant_thursday, LogicRegion.traveling_cart_thursday),
|
|
||||||
ConnectionData(LogicEntrance.buy_from_traveling_merchant_friday, LogicRegion.traveling_cart_friday),
|
|
||||||
ConnectionData(LogicEntrance.buy_from_traveling_merchant_saturday, LogicRegion.traveling_cart_saturday),
|
|
||||||
ConnectionData(LogicEntrance.complete_raccoon_requests, LogicRegion.raccoon_daddy),
|
|
||||||
ConnectionData(LogicEntrance.fish_in_waterfall, LogicRegion.forest_waterfall),
|
|
||||||
ConnectionData(LogicEntrance.buy_from_raccoon, LogicRegion.raccoon_shop),
|
|
||||||
ConnectionData(LogicEntrance.farmhouse_cooking, LogicRegion.kitchen),
|
|
||||||
ConnectionData(LogicEntrance.watch_queen_of_sauce, LogicRegion.queen_of_sauce),
|
|
||||||
|
|
||||||
ConnectionData(LogicEntrance.grow_spring_crops, LogicRegion.spring_farming),
|
|
||||||
ConnectionData(LogicEntrance.grow_summer_crops, LogicRegion.summer_farming),
|
|
||||||
ConnectionData(LogicEntrance.grow_fall_crops, LogicRegion.fall_farming),
|
|
||||||
ConnectionData(LogicEntrance.grow_winter_crops, LogicRegion.winter_farming),
|
|
||||||
ConnectionData(LogicEntrance.grow_spring_crops_in_greenhouse, LogicRegion.spring_farming),
|
|
||||||
ConnectionData(LogicEntrance.grow_summer_crops_in_greenhouse, LogicRegion.summer_farming),
|
|
||||||
ConnectionData(LogicEntrance.grow_fall_crops_in_greenhouse, LogicRegion.fall_farming),
|
|
||||||
ConnectionData(LogicEntrance.grow_winter_crops_in_greenhouse, LogicRegion.winter_farming),
|
|
||||||
ConnectionData(LogicEntrance.grow_indoor_crops_in_greenhouse, LogicRegion.indoor_farming),
|
|
||||||
ConnectionData(LogicEntrance.grow_spring_crops_on_island, LogicRegion.spring_farming, flag=RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(LogicEntrance.grow_summer_crops_on_island, LogicRegion.summer_farming, flag=RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(LogicEntrance.grow_fall_crops_on_island, LogicRegion.fall_farming, flag=RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(LogicEntrance.grow_winter_crops_on_island, LogicRegion.winter_farming, flag=RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(LogicEntrance.grow_indoor_crops_on_island, LogicRegion.indoor_farming, flag=RandomizationFlag.GINGER_ISLAND),
|
|
||||||
ConnectionData(LogicEntrance.grow_summer_fall_crops_in_summer, LogicRegion.summer_or_fall_farming),
|
|
||||||
ConnectionData(LogicEntrance.grow_summer_fall_crops_in_fall, LogicRegion.summer_or_fall_farming),
|
|
||||||
|
|
||||||
ConnectionData(LogicEntrance.shipping, LogicRegion.shipping),
|
|
||||||
ConnectionData(LogicEntrance.blacksmith_copper, LogicRegion.blacksmith_copper),
|
|
||||||
ConnectionData(LogicEntrance.blacksmith_iron, LogicRegion.blacksmith_iron),
|
|
||||||
ConnectionData(LogicEntrance.blacksmith_gold, LogicRegion.blacksmith_gold),
|
|
||||||
ConnectionData(LogicEntrance.blacksmith_iridium, LogicRegion.blacksmith_iridium),
|
|
||||||
ConnectionData(LogicEntrance.fishing, LogicRegion.fishing),
|
|
||||||
ConnectionData(LogicEntrance.island_cooking, LogicRegion.kitchen),
|
|
||||||
ConnectionData(LogicEntrance.attend_egg_festival, LogicRegion.egg_festival),
|
|
||||||
ConnectionData(LogicEntrance.attend_desert_festival, LogicRegion.desert_festival),
|
|
||||||
ConnectionData(LogicEntrance.attend_flower_dance, LogicRegion.flower_dance),
|
|
||||||
ConnectionData(LogicEntrance.attend_luau, LogicRegion.luau),
|
|
||||||
ConnectionData(LogicEntrance.attend_trout_derby, LogicRegion.trout_derby),
|
|
||||||
ConnectionData(LogicEntrance.attend_moonlight_jellies, LogicRegion.moonlight_jellies),
|
|
||||||
ConnectionData(LogicEntrance.attend_fair, LogicRegion.fair),
|
|
||||||
ConnectionData(LogicEntrance.attend_spirit_eve, LogicRegion.spirit_eve),
|
|
||||||
ConnectionData(LogicEntrance.attend_festival_of_ice, LogicRegion.festival_of_ice),
|
|
||||||
ConnectionData(LogicEntrance.attend_night_market, LogicRegion.night_market),
|
|
||||||
ConnectionData(LogicEntrance.attend_winter_star, LogicRegion.winter_star),
|
|
||||||
ConnectionData(LogicEntrance.attend_squidfest, LogicRegion.squidfest),
|
|
||||||
ConnectionData(LogicEntrance.buy_experience_books, LogicRegion.bookseller_1),
|
|
||||||
ConnectionData(LogicEntrance.buy_year1_books, LogicRegion.bookseller_2),
|
|
||||||
ConnectionData(LogicEntrance.buy_year3_books, LogicRegion.bookseller_3),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def create_final_regions(world_options) -> List[RegionData]:
|
|
||||||
final_regions = []
|
|
||||||
final_regions.extend(vanilla_regions)
|
|
||||||
if world_options.mods is None:
|
|
||||||
return final_regions
|
|
||||||
for mod in sorted(world_options.mods.value):
|
|
||||||
if mod not in ModDataList:
|
|
||||||
continue
|
|
||||||
for mod_region in ModDataList[mod].regions:
|
|
||||||
existing_region = next(
|
|
||||||
(region for region in final_regions if region.name == mod_region.name), None)
|
|
||||||
if existing_region:
|
|
||||||
final_regions.remove(existing_region)
|
|
||||||
if ModificationFlag.MODIFIED in mod_region.flag:
|
|
||||||
mod_region = modify_vanilla_regions(existing_region, mod_region)
|
|
||||||
final_regions.append(existing_region.get_merged_with(mod_region.exits))
|
|
||||||
continue
|
|
||||||
final_regions.append(mod_region.get_clone())
|
|
||||||
|
|
||||||
return final_regions
|
|
||||||
|
|
||||||
|
|
||||||
def create_final_connections_and_regions(world_options) -> Tuple[Dict[str, ConnectionData], Dict[str, RegionData]]:
|
|
||||||
regions_data: Dict[str, RegionData] = {region.name: region for region in create_final_regions(world_options)}
|
|
||||||
connections = {connection.name: connection for connection in vanilla_connections}
|
|
||||||
connections = modify_connections_for_mods(connections, sorted(world_options.mods.value))
|
|
||||||
include_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_false
|
|
||||||
return remove_ginger_island_regions_and_connections(regions_data, connections, include_island)
|
|
||||||
|
|
||||||
|
|
||||||
def remove_ginger_island_regions_and_connections(regions_by_name: Dict[str, RegionData], connections: Dict[str, ConnectionData], include_island: bool):
|
|
||||||
if include_island:
|
|
||||||
return connections, regions_by_name
|
|
||||||
|
|
||||||
removed_connections = set()
|
|
||||||
|
|
||||||
for connection_name in tuple(connections):
|
|
||||||
connection = connections[connection_name]
|
|
||||||
if connection.flag & RandomizationFlag.GINGER_ISLAND:
|
|
||||||
connections.pop(connection_name)
|
|
||||||
removed_connections.add(connection_name)
|
|
||||||
|
|
||||||
for region_name in tuple(regions_by_name):
|
|
||||||
region = regions_by_name[region_name]
|
|
||||||
if region.is_ginger_island:
|
|
||||||
regions_by_name.pop(region_name)
|
|
||||||
else:
|
|
||||||
regions_by_name[region_name] = region.get_without_exits(removed_connections)
|
|
||||||
|
|
||||||
return connections, regions_by_name
|
|
||||||
|
|
||||||
|
|
||||||
def modify_connections_for_mods(connections: Dict[str, ConnectionData], mods: Iterable) -> Dict[str, ConnectionData]:
|
|
||||||
for mod in mods:
|
|
||||||
if mod not in ModDataList:
|
|
||||||
continue
|
|
||||||
if mod in vanilla_connections_to_remove_by_mod:
|
|
||||||
for connection_data in vanilla_connections_to_remove_by_mod[mod]:
|
|
||||||
connections.pop(connection_data.name)
|
|
||||||
connections.update({connection.name: connection for connection in ModDataList[mod].connections})
|
|
||||||
return connections
|
|
||||||
|
|
||||||
|
|
||||||
def modify_vanilla_regions(existing_region: RegionData, modified_region: RegionData) -> RegionData:
|
|
||||||
updated_region = existing_region
|
|
||||||
region_exits = updated_region.exits
|
|
||||||
modified_exits = modified_region.exits
|
|
||||||
for exits in modified_exits:
|
|
||||||
region_exits.remove(exits)
|
|
||||||
|
|
||||||
return updated_region
|
|
||||||
|
|
||||||
|
|
||||||
def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewValleyOptions, content: StardewContent) \
|
|
||||||
-> Tuple[Dict[str, Region], Dict[str, Entrance], Dict[str, str]]:
|
|
||||||
entrances_data, regions_data = create_final_connections_and_regions(world_options)
|
|
||||||
regions_by_name: Dict[str: Region] = {region_name: region_factory(region_name, regions_data[region_name].exits) for region_name in regions_data}
|
|
||||||
entrances_by_name: Dict[str: Entrance] = {
|
|
||||||
entrance.name: entrance
|
|
||||||
for region in regions_by_name.values()
|
|
||||||
for entrance in region.exits
|
|
||||||
if entrance.name in entrances_data
|
|
||||||
}
|
|
||||||
|
|
||||||
connections, randomized_data = randomize_connections(random, world_options, content, regions_data, entrances_data)
|
|
||||||
|
|
||||||
for connection in connections:
|
|
||||||
if connection.name in entrances_by_name:
|
|
||||||
entrances_by_name[connection.name].connect(regions_by_name[connection.destination])
|
|
||||||
return regions_by_name, entrances_by_name, randomized_data
|
|
||||||
|
|
||||||
|
|
||||||
def randomize_connections(random: Random, world_options: StardewValleyOptions, content: StardewContent, regions_by_name: Dict[str, RegionData],
|
|
||||||
connections_by_name: Dict[str, ConnectionData]) -> Tuple[List[ConnectionData], Dict[str, str]]:
|
|
||||||
connections_to_randomize: List[ConnectionData] = []
|
|
||||||
if world_options.entrance_randomization == EntranceRandomization.option_pelican_town:
|
|
||||||
connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if
|
|
||||||
RandomizationFlag.PELICAN_TOWN in connections_by_name[connection].flag]
|
|
||||||
elif world_options.entrance_randomization == EntranceRandomization.option_non_progression:
|
|
||||||
connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if
|
|
||||||
RandomizationFlag.NON_PROGRESSION in connections_by_name[connection].flag]
|
|
||||||
elif world_options.entrance_randomization == EntranceRandomization.option_buildings or world_options.entrance_randomization == EntranceRandomization.option_buildings_without_house:
|
|
||||||
connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if
|
|
||||||
RandomizationFlag.BUILDINGS in connections_by_name[connection].flag]
|
|
||||||
elif world_options.entrance_randomization == EntranceRandomization.option_chaos:
|
|
||||||
connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if
|
|
||||||
RandomizationFlag.BUILDINGS in connections_by_name[connection].flag]
|
|
||||||
connections_to_randomize = remove_excluded_entrances(connections_to_randomize, content)
|
|
||||||
|
|
||||||
# On Chaos, we just add the connections to randomize, unshuffled, and the client does it every day
|
|
||||||
randomized_data_for_mod = {}
|
|
||||||
for connection in connections_to_randomize:
|
|
||||||
randomized_data_for_mod[connection.name] = connection.name
|
|
||||||
randomized_data_for_mod[connection.reverse] = connection.reverse
|
|
||||||
return list(connections_by_name.values()), randomized_data_for_mod
|
|
||||||
|
|
||||||
connections_to_randomize = remove_excluded_entrances(connections_to_randomize, content)
|
|
||||||
random.shuffle(connections_to_randomize)
|
|
||||||
destination_pool = list(connections_to_randomize)
|
|
||||||
random.shuffle(destination_pool)
|
|
||||||
|
|
||||||
randomized_connections = randomize_chosen_connections(connections_to_randomize, destination_pool)
|
|
||||||
add_non_randomized_connections(list(connections_by_name.values()), connections_to_randomize, randomized_connections)
|
|
||||||
|
|
||||||
swap_connections_until_valid(regions_by_name, connections_by_name, randomized_connections, connections_to_randomize, random)
|
|
||||||
randomized_connections_for_generation = create_connections_for_generation(randomized_connections)
|
|
||||||
randomized_data_for_mod = create_data_for_mod(randomized_connections, connections_to_randomize)
|
|
||||||
|
|
||||||
return randomized_connections_for_generation, randomized_data_for_mod
|
|
||||||
|
|
||||||
|
|
||||||
def remove_excluded_entrances(connections_to_randomize: List[ConnectionData], content: StardewContent) -> List[ConnectionData]:
|
|
||||||
# FIXME remove when regions are handled in content packs
|
|
||||||
if content_packs.ginger_island_content_pack.name not in content.registered_packs:
|
|
||||||
connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.GINGER_ISLAND not in connection.flag]
|
|
||||||
if not content.features.skill_progression.are_masteries_shuffled:
|
|
||||||
connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.MASTERIES not in connection.flag]
|
|
||||||
|
|
||||||
return connections_to_randomize
|
|
||||||
|
|
||||||
|
|
||||||
def randomize_chosen_connections(connections_to_randomize: List[ConnectionData],
|
|
||||||
destination_pool: List[ConnectionData]) -> Dict[ConnectionData, ConnectionData]:
|
|
||||||
randomized_connections = {}
|
|
||||||
for connection in connections_to_randomize:
|
|
||||||
destination = destination_pool.pop()
|
|
||||||
randomized_connections[connection] = destination
|
|
||||||
return randomized_connections
|
|
||||||
|
|
||||||
|
|
||||||
def create_connections_for_generation(randomized_connections: Dict[ConnectionData, ConnectionData]) -> List[ConnectionData]:
|
|
||||||
connections = []
|
|
||||||
for connection in randomized_connections:
|
|
||||||
destination = randomized_connections[connection]
|
|
||||||
connections.append(ConnectionData(connection.name, destination.destination, destination.reverse))
|
|
||||||
return connections
|
|
||||||
|
|
||||||
|
|
||||||
def create_data_for_mod(randomized_connections: Dict[ConnectionData, ConnectionData],
|
|
||||||
connections_to_randomize: List[ConnectionData]) -> Dict[str, str]:
|
|
||||||
randomized_data_for_mod = {}
|
|
||||||
for connection in randomized_connections:
|
|
||||||
if connection not in connections_to_randomize:
|
|
||||||
continue
|
|
||||||
destination = randomized_connections[connection]
|
|
||||||
add_to_mod_data(connection, destination, randomized_data_for_mod)
|
|
||||||
return randomized_data_for_mod
|
|
||||||
|
|
||||||
|
|
||||||
def add_to_mod_data(connection: ConnectionData, destination: ConnectionData, randomized_data_for_mod: Dict[str, str]):
|
|
||||||
randomized_data_for_mod[connection.name] = destination.name
|
|
||||||
randomized_data_for_mod[destination.reverse] = connection.reverse
|
|
||||||
|
|
||||||
|
|
||||||
def add_non_randomized_connections(all_connections: List[ConnectionData], connections_to_randomize: List[ConnectionData],
|
|
||||||
randomized_connections: Dict[ConnectionData, ConnectionData]):
|
|
||||||
for connection in all_connections:
|
|
||||||
if connection in connections_to_randomize:
|
|
||||||
continue
|
|
||||||
randomized_connections[connection] = connection
|
|
||||||
|
|
||||||
|
|
||||||
def swap_connections_until_valid(regions_by_name, connections_by_name: Dict[str, ConnectionData], randomized_connections: Dict[ConnectionData, ConnectionData],
|
|
||||||
connections_to_randomize: List[ConnectionData], random: Random):
|
|
||||||
while True:
|
|
||||||
reachable_regions, unreachable_regions = find_reachable_regions(regions_by_name, connections_by_name, randomized_connections)
|
|
||||||
if not unreachable_regions:
|
|
||||||
return randomized_connections
|
|
||||||
swap_one_random_connection(regions_by_name, connections_by_name, randomized_connections, reachable_regions,
|
|
||||||
unreachable_regions, connections_to_randomize, random)
|
|
||||||
|
|
||||||
|
|
||||||
def region_should_be_reachable(region_name: str, connections_in_slot: Iterable[ConnectionData]) -> bool:
|
|
||||||
if region_name == RegionName.menu:
|
|
||||||
return True
|
|
||||||
for connection in connections_in_slot:
|
|
||||||
if region_name == connection.destination:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def find_reachable_regions(regions_by_name, connections_by_name,
|
|
||||||
randomized_connections: Dict[ConnectionData, ConnectionData]):
|
|
||||||
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(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:
|
|
||||||
# continue
|
|
||||||
exit_connection = connections_by_name[exit_name]
|
|
||||||
replaced_connection = randomized_connections[exit_connection]
|
|
||||||
target_region_name = replaced_connection.destination
|
|
||||||
if target_region_name in reachable_regions:
|
|
||||||
continue
|
|
||||||
|
|
||||||
target_region = regions_by_name[target_region_name]
|
|
||||||
reachable_regions.add(target_region_name)
|
|
||||||
unreachable_regions.remove(target_region_name)
|
|
||||||
exits_to_explore.extend(target_region.exits)
|
|
||||||
return reachable_regions, unreachable_regions
|
|
||||||
|
|
||||||
|
|
||||||
def swap_one_random_connection(regions_by_name, connections_by_name, randomized_connections: Dict[ConnectionData, ConnectionData],
|
|
||||||
reachable_regions: Set[str], unreachable_regions: Set[str],
|
|
||||||
connections_to_randomize: List[ConnectionData], random: Random):
|
|
||||||
randomized_connections_already_shuffled = {connection: randomized_connections[connection]
|
|
||||||
for connection in randomized_connections
|
|
||||||
if connection != randomized_connections[connection]}
|
|
||||||
unreachable_regions_names_leading_somewhere = [region for region in sorted(unreachable_regions) if len(regions_by_name[region].exits) > 0]
|
|
||||||
unreachable_regions_leading_somewhere = [regions_by_name[region_name] for region_name in unreachable_regions_names_leading_somewhere]
|
|
||||||
unreachable_regions_exits_names = [exit_name for region in unreachable_regions_leading_somewhere for exit_name in region.exits]
|
|
||||||
unreachable_connections = [connections_by_name[exit_name] for exit_name in unreachable_regions_exits_names]
|
|
||||||
unreachable_connections_that_can_be_randomized = [connection for connection in unreachable_connections if connection in connections_to_randomize]
|
|
||||||
|
|
||||||
chosen_unreachable_entrance = random.choice(unreachable_connections_that_can_be_randomized)
|
|
||||||
|
|
||||||
chosen_reachable_entrance = None
|
|
||||||
while chosen_reachable_entrance is None or chosen_reachable_entrance not in randomized_connections_already_shuffled:
|
|
||||||
chosen_reachable_region_name = random.choice(sorted(reachable_regions))
|
|
||||||
chosen_reachable_region = regions_by_name[chosen_reachable_region_name]
|
|
||||||
if not any(chosen_reachable_region.exits):
|
|
||||||
continue
|
|
||||||
chosen_reachable_entrance_name = random.choice(chosen_reachable_region.exits)
|
|
||||||
chosen_reachable_entrance = connections_by_name[chosen_reachable_entrance_name]
|
|
||||||
|
|
||||||
swap_two_connections(chosen_reachable_entrance, chosen_unreachable_entrance, randomized_connections)
|
|
||||||
|
|
||||||
|
|
||||||
def swap_two_connections(entrance_1, entrance_2, randomized_connections):
|
|
||||||
reachable_destination = randomized_connections[entrance_1]
|
|
||||||
unreachable_destination = randomized_connections[entrance_2]
|
|
||||||
randomized_connections[entrance_1] = unreachable_destination
|
|
||||||
randomized_connections[entrance_2] = reachable_destination
|
|
||||||
2
worlds/stardew_valley/regions/__init__.py
Normal file
2
worlds/stardew_valley/regions/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .entrance_rando import prepare_mod_data
|
||||||
|
from .regions import create_regions, RegionFactory
|
||||||
73
worlds/stardew_valley/regions/entrance_rando.py
Normal file
73
worlds/stardew_valley/regions/entrance_rando.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
from BaseClasses import Region
|
||||||
|
from entrance_rando import ERPlacementState
|
||||||
|
from .model import ConnectionData, RandomizationFlag, reverse_connection_name, RegionData
|
||||||
|
from ..content import StardewContent
|
||||||
|
from ..options import EntranceRandomization
|
||||||
|
|
||||||
|
|
||||||
|
def create_player_randomization_flag(entrance_randomization_choice: EntranceRandomization, content: StardewContent):
|
||||||
|
"""Return the flag that a connection is expected to have to be randomized. Only the bit corresponding to the player randomization choice will be enabled.
|
||||||
|
|
||||||
|
Other bits for content exclusion might also be enabled, tho the preferred solution to exclude content should be to not create those regions at alls, when possible.
|
||||||
|
"""
|
||||||
|
flag = RandomizationFlag.NOT_RANDOMIZED
|
||||||
|
|
||||||
|
if entrance_randomization_choice.value == EntranceRandomization.option_disabled:
|
||||||
|
return flag
|
||||||
|
|
||||||
|
if entrance_randomization_choice == EntranceRandomization.option_pelican_town:
|
||||||
|
flag |= RandomizationFlag.BIT_PELICAN_TOWN
|
||||||
|
elif entrance_randomization_choice == EntranceRandomization.option_non_progression:
|
||||||
|
flag |= RandomizationFlag.BIT_NON_PROGRESSION
|
||||||
|
elif entrance_randomization_choice in (
|
||||||
|
EntranceRandomization.option_buildings,
|
||||||
|
EntranceRandomization.option_buildings_without_house,
|
||||||
|
EntranceRandomization.option_chaos
|
||||||
|
):
|
||||||
|
flag |= RandomizationFlag.BIT_BUILDINGS
|
||||||
|
|
||||||
|
if not content.features.skill_progression.are_masteries_shuffled:
|
||||||
|
flag |= RandomizationFlag.EXCLUDE_MASTERIES
|
||||||
|
|
||||||
|
return flag
|
||||||
|
|
||||||
|
|
||||||
|
def connect_regions(region_data_by_name: dict[str, RegionData], connection_data_by_name: dict[str, ConnectionData], regions_by_name: dict[str, Region],
|
||||||
|
player_randomization_flag: RandomizationFlag) -> None:
|
||||||
|
for region_name, region_data in region_data_by_name.items():
|
||||||
|
origin_region = regions_by_name[region_name]
|
||||||
|
|
||||||
|
for exit_name in region_data.exits:
|
||||||
|
connection_data = connection_data_by_name[exit_name]
|
||||||
|
destination_region = regions_by_name[connection_data.destination]
|
||||||
|
|
||||||
|
if connection_data.is_eligible_for_randomization(player_randomization_flag):
|
||||||
|
create_entrance_rando_target(origin_region, destination_region, connection_data)
|
||||||
|
else:
|
||||||
|
origin_region.connect(destination_region, connection_data.name)
|
||||||
|
|
||||||
|
|
||||||
|
def create_entrance_rando_target(origin: Region, destination: Region, connection_data: ConnectionData) -> None:
|
||||||
|
"""We need our own function to create the GER targets, because the Stardew Mod have very specific expectations for the name of the entrances.
|
||||||
|
We need to know exactly which entrances to swap in both directions."""
|
||||||
|
origin.create_exit(connection_data.name)
|
||||||
|
destination.create_er_target(connection_data.reverse)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_mod_data(placements: ERPlacementState) -> dict[str, str]:
|
||||||
|
"""Take the placements from GER and prepare the data for the mod.
|
||||||
|
The mod require a dictionary detailing which connections need to be swapped. It acts as if the connections are decoupled, so both directions are required.
|
||||||
|
|
||||||
|
For instance, GER will provide placements like (Town to Community Center, Hospital to Town), meaning that the door of the Community Center will instead lead
|
||||||
|
to the Hospital, and that the exit of the Hospital will lead to the Town by the Community Center door. The StardewAP mod need to know both swaps, being the
|
||||||
|
original destination of the "Town to Community Center" connection is to be replaced by the original destination of "Town to Hospital", and the original
|
||||||
|
destination of "Hospital to Town" is to be replaced by the original destination of "Community Center to Town".
|
||||||
|
"""
|
||||||
|
|
||||||
|
swapped_connections = {}
|
||||||
|
|
||||||
|
for entrance, exit_ in placements.pairings:
|
||||||
|
swapped_connections[entrance] = reverse_connection_name(exit_)
|
||||||
|
swapped_connections[exit_] = reverse_connection_name(entrance)
|
||||||
|
|
||||||
|
return swapped_connections
|
||||||
94
worlds/stardew_valley/regions/model.py
Normal file
94
worlds/stardew_valley/regions/model.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Container
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import IntFlag
|
||||||
|
|
||||||
|
connector_keyword = " to "
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_connection_name(name: str) -> str | None:
|
||||||
|
try:
|
||||||
|
origin, destination = name.split(connector_keyword)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
return f"{destination}{connector_keyword}{origin}"
|
||||||
|
|
||||||
|
|
||||||
|
class MergeFlag(IntFlag):
|
||||||
|
ADD_EXITS = 0
|
||||||
|
REMOVE_EXITS = 1
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizationFlag(IntFlag):
|
||||||
|
NOT_RANDOMIZED = 0
|
||||||
|
|
||||||
|
# Randomization options
|
||||||
|
# The first 4 bits are used to mark if an entrance is eligible for randomization according to the entrance randomization options.
|
||||||
|
BIT_PELICAN_TOWN = 1 # 0b0001
|
||||||
|
BIT_NON_PROGRESSION = 1 << 1 # 0b0010
|
||||||
|
BIT_BUILDINGS = 1 << 2 # 0b0100
|
||||||
|
BIT_EVERYTHING = 1 << 3 # 0b1000
|
||||||
|
|
||||||
|
# Content flag for entrances exclusions
|
||||||
|
# The next 2 bits are used to mark if an entrance is to be excluded from randomization according to the content options.
|
||||||
|
# Those bits must be removed from an entrance flags when then entrance must be excluded.
|
||||||
|
__UNUSED = 1 << 4 # 0b010000
|
||||||
|
EXCLUDE_MASTERIES = 1 << 5 # 0b100000
|
||||||
|
|
||||||
|
# Entrance groups
|
||||||
|
# The last bit is used to add additional qualifiers on entrances to group them
|
||||||
|
# Those bits should be added when an entrance need additional qualifiers.
|
||||||
|
LEAD_TO_OPEN_AREA = 1 << 6
|
||||||
|
|
||||||
|
# Tags to apply on connections
|
||||||
|
EVERYTHING = EXCLUDE_MASTERIES | BIT_EVERYTHING
|
||||||
|
BUILDINGS = EVERYTHING | BIT_BUILDINGS
|
||||||
|
NON_PROGRESSION = BUILDINGS | BIT_NON_PROGRESSION
|
||||||
|
PELICAN_TOWN = NON_PROGRESSION | BIT_PELICAN_TOWN
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RegionData:
|
||||||
|
name: str
|
||||||
|
exits: tuple[str, ...] = field(default_factory=tuple)
|
||||||
|
flag: MergeFlag = MergeFlag.ADD_EXITS
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert not isinstance(self.exits, str), "Exits must be a tuple of strings, you probably forgot a trailing comma."
|
||||||
|
|
||||||
|
def merge_with(self, other: RegionData) -> RegionData:
|
||||||
|
assert self.name == other.name, "Regions must have the same name to be merged"
|
||||||
|
|
||||||
|
if other.flag == MergeFlag.REMOVE_EXITS:
|
||||||
|
return self.get_without_exits(other.exits)
|
||||||
|
|
||||||
|
merged_exits = self.exits + other.exits
|
||||||
|
assert len(merged_exits) == len(set(merged_exits)), "Two regions getting merged have duplicated exists..."
|
||||||
|
|
||||||
|
return RegionData(self.name, merged_exits)
|
||||||
|
|
||||||
|
def get_without_exits(self, exits_to_remove: Container[str]) -> RegionData:
|
||||||
|
exits = tuple(exit_ for exit_ in self.exits if exit_ not in exits_to_remove)
|
||||||
|
return RegionData(self.name, exits)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ConnectionData:
|
||||||
|
name: str
|
||||||
|
destination: str
|
||||||
|
flag: RandomizationFlag = RandomizationFlag.NOT_RANDOMIZED
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reverse(self) -> str | None:
|
||||||
|
return reverse_connection_name(self.name)
|
||||||
|
|
||||||
|
def is_eligible_for_randomization(self, chosen_randomization_flag: RandomizationFlag) -> bool:
|
||||||
|
return chosen_randomization_flag and chosen_randomization_flag in self.flag
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ModRegionsData:
|
||||||
|
mod_name: str
|
||||||
|
regions: list[RegionData]
|
||||||
|
connections: list[ConnectionData]
|
||||||
46
worlds/stardew_valley/regions/mods.py
Normal file
46
worlds/stardew_valley/regions/mods.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
from .model import ConnectionData, RegionData, ModRegionsData
|
||||||
|
from ..mods.region_data import region_data_by_content_pack, vanilla_connections_to_remove_by_content_pack
|
||||||
|
|
||||||
|
|
||||||
|
def modify_regions_for_mods(current_regions_by_name: dict[str, RegionData], active_content_packs: Iterable[str]) -> None:
|
||||||
|
for content_pack in active_content_packs:
|
||||||
|
try:
|
||||||
|
region_data = region_data_by_content_pack[content_pack]
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
merge_mod_regions(current_regions_by_name, region_data)
|
||||||
|
|
||||||
|
|
||||||
|
def merge_mod_regions(current_regions_by_name: dict[str, RegionData], mod_region_data: ModRegionsData) -> None:
|
||||||
|
for new_region in mod_region_data.regions:
|
||||||
|
region_name = new_region.name
|
||||||
|
try:
|
||||||
|
current_region = current_regions_by_name[region_name]
|
||||||
|
except KeyError:
|
||||||
|
current_regions_by_name[region_name] = new_region
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_regions_by_name[region_name] = current_region.merge_with(new_region)
|
||||||
|
|
||||||
|
|
||||||
|
def modify_connections_for_mods(connections: dict[str, ConnectionData], active_mods: Iterable[str]) -> None:
|
||||||
|
for active_mod in active_mods:
|
||||||
|
try:
|
||||||
|
region_data = region_data_by_content_pack[active_mod]
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
vanilla_connections_to_remove = vanilla_connections_to_remove_by_content_pack[active_mod]
|
||||||
|
for connection_name in vanilla_connections_to_remove:
|
||||||
|
connections.pop(connection_name)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
connections.update({
|
||||||
|
connection.name: connection
|
||||||
|
for connection in region_data.connections
|
||||||
|
})
|
||||||
61
worlds/stardew_valley/regions/regions.py
Normal file
61
worlds/stardew_valley/regions/regions.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
from BaseClasses import Region
|
||||||
|
from . import vanilla_data, mods
|
||||||
|
from .entrance_rando import create_player_randomization_flag, connect_regions
|
||||||
|
from .model import ConnectionData, RegionData
|
||||||
|
from ..content import StardewContent
|
||||||
|
from ..content.vanilla.ginger_island import ginger_island_content_pack
|
||||||
|
from ..options import StardewValleyOptions
|
||||||
|
|
||||||
|
|
||||||
|
class RegionFactory(Protocol):
|
||||||
|
def __call__(self, name: str) -> Region:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def create_regions(region_factory: RegionFactory, world_options: StardewValleyOptions, content: StardewContent) -> dict[str, Region]:
|
||||||
|
connection_data_by_name, region_data_by_name = create_connections_and_regions(content.registered_packs)
|
||||||
|
|
||||||
|
regions_by_name: dict[str: Region] = {
|
||||||
|
region_name: region_factory(region_name)
|
||||||
|
for region_name in region_data_by_name
|
||||||
|
}
|
||||||
|
|
||||||
|
randomization_flag = create_player_randomization_flag(world_options.entrance_randomization, content)
|
||||||
|
connect_regions(region_data_by_name, connection_data_by_name, regions_by_name, randomization_flag)
|
||||||
|
|
||||||
|
return regions_by_name
|
||||||
|
|
||||||
|
|
||||||
|
def create_connections_and_regions(active_content_packs: set[str]) -> tuple[dict[str, ConnectionData], dict[str, RegionData]]:
|
||||||
|
regions_by_name = create_all_regions(active_content_packs)
|
||||||
|
connections_by_name = create_all_connections(active_content_packs)
|
||||||
|
|
||||||
|
return connections_by_name, regions_by_name
|
||||||
|
|
||||||
|
|
||||||
|
def create_all_regions(active_content_packs: set[str]) -> dict[str, RegionData]:
|
||||||
|
current_regions_by_name = create_vanilla_regions(active_content_packs)
|
||||||
|
mods.modify_regions_for_mods(current_regions_by_name, sorted(active_content_packs))
|
||||||
|
return current_regions_by_name
|
||||||
|
|
||||||
|
|
||||||
|
def create_vanilla_regions(active_content_packs: set[str]) -> dict[str, RegionData]:
|
||||||
|
if ginger_island_content_pack.name in active_content_packs:
|
||||||
|
return {**vanilla_data.regions_with_ginger_island_by_name}
|
||||||
|
else:
|
||||||
|
return {**vanilla_data.regions_without_ginger_island_by_name}
|
||||||
|
|
||||||
|
|
||||||
|
def create_all_connections(active_content_packs: set[str]) -> dict[str, ConnectionData]:
|
||||||
|
connections = create_vanilla_connections(active_content_packs)
|
||||||
|
mods.modify_connections_for_mods(connections, sorted(active_content_packs))
|
||||||
|
return connections
|
||||||
|
|
||||||
|
|
||||||
|
def create_vanilla_connections(active_content_packs: set[str]) -> dict[str, ConnectionData]:
|
||||||
|
if ginger_island_content_pack.name in active_content_packs:
|
||||||
|
return {**vanilla_data.connections_with_ginger_island_by_name}
|
||||||
|
else:
|
||||||
|
return {**vanilla_data.connections_without_ginger_island_by_name}
|
||||||
522
worlds/stardew_valley/regions/vanilla_data.py
Normal file
522
worlds/stardew_valley/regions/vanilla_data.py
Normal file
@@ -0,0 +1,522 @@
|
|||||||
|
from collections.abc import Mapping
|
||||||
|
from types import MappingProxyType
|
||||||
|
|
||||||
|
from .model import ConnectionData, RandomizationFlag, RegionData
|
||||||
|
from ..strings.entrance_names import LogicEntrance, Entrance
|
||||||
|
from ..strings.region_names import LogicRegion, Region as RegionName
|
||||||
|
|
||||||
|
vanilla_regions: tuple[RegionData, ...] = (
|
||||||
|
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(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,
|
||||||
|
LogicEntrance.fishing,)),
|
||||||
|
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(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(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(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)),
|
||||||
|
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(RegionName.beach,
|
||||||
|
(Entrance.beach_to_willy_fish_shop, Entrance.enter_elliott_house, Entrance.enter_tide_pools, LogicEntrance.attend_luau,
|
||||||
|
LogicEntrance.attend_moonlight_jellies, LogicEntrance.attend_night_market, LogicEntrance.attend_squidfest)),
|
||||||
|
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)),
|
||||||
|
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(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),
|
||||||
|
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),
|
||||||
|
|
||||||
|
RegionData(RegionName.coop),
|
||||||
|
RegionData(RegionName.barn),
|
||||||
|
RegionData(RegionName.shed),
|
||||||
|
RegionData(RegionName.slime_hutch),
|
||||||
|
|
||||||
|
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),
|
||||||
|
|
||||||
|
RegionData(LogicRegion.mines_dwarf_shop),
|
||||||
|
RegionData(LogicRegion.blacksmith_copper, (LogicEntrance.blacksmith_iron,)),
|
||||||
|
RegionData(LogicRegion.blacksmith_iron, (LogicEntrance.blacksmith_gold,)),
|
||||||
|
RegionData(LogicRegion.blacksmith_gold, (LogicEntrance.blacksmith_iridium,)),
|
||||||
|
RegionData(LogicRegion.blacksmith_iridium),
|
||||||
|
RegionData(LogicRegion.kitchen),
|
||||||
|
RegionData(LogicRegion.queen_of_sauce),
|
||||||
|
RegionData(LogicRegion.fishing),
|
||||||
|
|
||||||
|
RegionData(LogicRegion.spring_farming),
|
||||||
|
RegionData(LogicRegion.summer_farming, (LogicEntrance.grow_summer_fall_crops_in_summer,)),
|
||||||
|
RegionData(LogicRegion.fall_farming, (LogicEntrance.grow_summer_fall_crops_in_fall,)),
|
||||||
|
RegionData(LogicRegion.winter_farming),
|
||||||
|
RegionData(LogicRegion.summer_or_fall_farming),
|
||||||
|
RegionData(LogicRegion.indoor_farming),
|
||||||
|
|
||||||
|
RegionData(LogicRegion.shipping),
|
||||||
|
RegionData(LogicRegion.traveling_cart, (LogicEntrance.buy_from_traveling_merchant_sunday,
|
||||||
|
LogicEntrance.buy_from_traveling_merchant_monday,
|
||||||
|
LogicEntrance.buy_from_traveling_merchant_tuesday,
|
||||||
|
LogicEntrance.buy_from_traveling_merchant_wednesday,
|
||||||
|
LogicEntrance.buy_from_traveling_merchant_thursday,
|
||||||
|
LogicEntrance.buy_from_traveling_merchant_friday,
|
||||||
|
LogicEntrance.buy_from_traveling_merchant_saturday)),
|
||||||
|
RegionData(LogicRegion.traveling_cart_sunday),
|
||||||
|
RegionData(LogicRegion.traveling_cart_monday),
|
||||||
|
RegionData(LogicRegion.traveling_cart_tuesday),
|
||||||
|
RegionData(LogicRegion.traveling_cart_wednesday),
|
||||||
|
RegionData(LogicRegion.traveling_cart_thursday),
|
||||||
|
RegionData(LogicRegion.traveling_cart_friday),
|
||||||
|
RegionData(LogicRegion.traveling_cart_saturday),
|
||||||
|
RegionData(LogicRegion.raccoon_daddy, (LogicEntrance.buy_from_raccoon,)),
|
||||||
|
RegionData(LogicRegion.raccoon_shop),
|
||||||
|
|
||||||
|
RegionData(LogicRegion.egg_festival),
|
||||||
|
RegionData(LogicRegion.desert_festival),
|
||||||
|
RegionData(LogicRegion.flower_dance),
|
||||||
|
RegionData(LogicRegion.luau),
|
||||||
|
RegionData(LogicRegion.trout_derby),
|
||||||
|
RegionData(LogicRegion.moonlight_jellies),
|
||||||
|
RegionData(LogicRegion.fair),
|
||||||
|
RegionData(LogicRegion.spirit_eve),
|
||||||
|
RegionData(LogicRegion.festival_of_ice),
|
||||||
|
RegionData(LogicRegion.night_market),
|
||||||
|
RegionData(LogicRegion.winter_star),
|
||||||
|
RegionData(LogicRegion.squidfest),
|
||||||
|
RegionData(LogicRegion.bookseller_1, (LogicEntrance.buy_year1_books,)),
|
||||||
|
RegionData(LogicRegion.bookseller_2, (LogicEntrance.buy_year3_books,)),
|
||||||
|
RegionData(LogicRegion.bookseller_3),
|
||||||
|
)
|
||||||
|
ginger_island_regions = (
|
||||||
|
# This overrides the regions from vanilla... When regions are moved to content packs, overriding existing entrances should no longer be necessary.
|
||||||
|
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(RegionName.wizard_tower, (Entrance.enter_wizard_basement, Entrance.use_desert_obelisk, Entrance.use_island_obelisk,)),
|
||||||
|
RegionData(RegionName.fish_shop, (Entrance.fish_shop_to_boat_tunnel,)),
|
||||||
|
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.skull_cavern_200, (Entrance.enter_dangerous_skull_cavern,)),
|
||||||
|
|
||||||
|
RegionData(RegionName.leo_treehouse),
|
||||||
|
RegionData(RegionName.boat_tunnel, (Entrance.boat_to_ginger_island,)),
|
||||||
|
RegionData(RegionName.dangerous_skull_cavern),
|
||||||
|
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), ),
|
||||||
|
RegionData(RegionName.island_resort),
|
||||||
|
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), ),
|
||||||
|
RegionData(RegionName.island_east, (Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine)),
|
||||||
|
RegionData(RegionName.island_shrine),
|
||||||
|
RegionData(RegionName.island_south_east, (Entrance.island_southeast_to_pirate_cove,)),
|
||||||
|
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), ),
|
||||||
|
RegionData(RegionName.volcano, (Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach)),
|
||||||
|
RegionData(RegionName.volcano_secret_beach),
|
||||||
|
RegionData(RegionName.volcano_floor_5, (Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10)),
|
||||||
|
RegionData(RegionName.volcano_dwarf_shop),
|
||||||
|
RegionData(RegionName.volcano_floor_10),
|
||||||
|
RegionData(RegionName.island_trader),
|
||||||
|
RegionData(RegionName.island_farmhouse, (LogicEntrance.island_cooking,)),
|
||||||
|
RegionData(RegionName.gourmand_frog_cave),
|
||||||
|
RegionData(RegionName.colored_crystals_cave),
|
||||||
|
RegionData(RegionName.shipwreck),
|
||||||
|
RegionData(RegionName.qi_walnut_room),
|
||||||
|
RegionData(RegionName.leo_hut),
|
||||||
|
RegionData(RegionName.pirate_cove),
|
||||||
|
RegionData(RegionName.field_office),
|
||||||
|
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), ),
|
||||||
|
|
||||||
|
RegionData(RegionName.professor_snail_cave),
|
||||||
|
RegionData(RegionName.dangerous_mines_20),
|
||||||
|
RegionData(RegionName.dangerous_mines_60),
|
||||||
|
RegionData(RegionName.dangerous_mines_100),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Exists and where they lead
|
||||||
|
vanilla_connections: tuple[ConnectionData, ...] = (
|
||||||
|
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.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, 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, RegionName.leah_house,
|
||||||
|
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
|
ConnectionData(Entrance.enter_secret_woods, RegionName.secret_woods),
|
||||||
|
ConnectionData(Entrance.forest_to_sewer, RegionName.sewer, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
# We remove the bit for masteries, because the mastery cave is to be excluded from the randomization if masteries are not shuffled.
|
||||||
|
ConnectionData(Entrance.forest_to_mastery_cave, RegionName.mastery_cave, flag=RandomizationFlag.BUILDINGS ^ RandomizationFlag.EXCLUDE_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_carpenter_shop, RegionName.carpenter,
|
||||||
|
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
|
ConnectionData(Entrance.mountain_to_maru_room, RegionName.maru_room,
|
||||||
|
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
|
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, 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, 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, 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, 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, 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, RegionName.saloon,
|
||||||
|
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
|
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, RegionName.haley_house,
|
||||||
|
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
|
ConnectionData(Entrance.town_to_mayor_manor, RegionName.mayor_house,
|
||||||
|
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
|
ConnectionData(Entrance.town_to_alex_house, RegionName.alex_house,
|
||||||
|
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
|
ConnectionData(Entrance.town_to_trailer, RegionName.trailer,
|
||||||
|
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
|
ConnectionData(Entrance.town_to_museum, RegionName.museum,
|
||||||
|
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
|
ConnectionData(Entrance.town_to_jojamart, RegionName.jojamart,
|
||||||
|
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
|
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, RegionName.fish_shop,
|
||||||
|
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
|
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, 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.enter_skull_cavern_entrance, RegionName.skull_cavern_entrance,
|
||||||
|
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
|
ConnectionData(Entrance.enter_oasis, RegionName.oasis,
|
||||||
|
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
|
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_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, RegionName.locker_room, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(Entrance.enter_public_bath, RegionName.public_bath, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(LogicEntrance.talk_to_mines_dwarf, LogicRegion.mines_dwarf_shop),
|
||||||
|
|
||||||
|
ConnectionData(LogicEntrance.buy_from_traveling_merchant, LogicRegion.traveling_cart),
|
||||||
|
ConnectionData(LogicEntrance.buy_from_traveling_merchant_sunday, LogicRegion.traveling_cart_sunday),
|
||||||
|
ConnectionData(LogicEntrance.buy_from_traveling_merchant_monday, LogicRegion.traveling_cart_monday),
|
||||||
|
ConnectionData(LogicEntrance.buy_from_traveling_merchant_tuesday, LogicRegion.traveling_cart_tuesday),
|
||||||
|
ConnectionData(LogicEntrance.buy_from_traveling_merchant_wednesday, LogicRegion.traveling_cart_wednesday),
|
||||||
|
ConnectionData(LogicEntrance.buy_from_traveling_merchant_thursday, LogicRegion.traveling_cart_thursday),
|
||||||
|
ConnectionData(LogicEntrance.buy_from_traveling_merchant_friday, LogicRegion.traveling_cart_friday),
|
||||||
|
ConnectionData(LogicEntrance.buy_from_traveling_merchant_saturday, LogicRegion.traveling_cart_saturday),
|
||||||
|
ConnectionData(LogicEntrance.complete_raccoon_requests, LogicRegion.raccoon_daddy),
|
||||||
|
ConnectionData(LogicEntrance.fish_in_waterfall, LogicRegion.forest_waterfall),
|
||||||
|
ConnectionData(LogicEntrance.buy_from_raccoon, LogicRegion.raccoon_shop),
|
||||||
|
ConnectionData(LogicEntrance.farmhouse_cooking, LogicRegion.kitchen),
|
||||||
|
ConnectionData(LogicEntrance.watch_queen_of_sauce, LogicRegion.queen_of_sauce),
|
||||||
|
|
||||||
|
ConnectionData(LogicEntrance.grow_spring_crops, LogicRegion.spring_farming),
|
||||||
|
ConnectionData(LogicEntrance.grow_summer_crops, LogicRegion.summer_farming),
|
||||||
|
ConnectionData(LogicEntrance.grow_fall_crops, LogicRegion.fall_farming),
|
||||||
|
ConnectionData(LogicEntrance.grow_winter_crops, LogicRegion.winter_farming),
|
||||||
|
ConnectionData(LogicEntrance.grow_spring_crops_in_greenhouse, LogicRegion.spring_farming),
|
||||||
|
ConnectionData(LogicEntrance.grow_summer_crops_in_greenhouse, LogicRegion.summer_farming),
|
||||||
|
ConnectionData(LogicEntrance.grow_fall_crops_in_greenhouse, LogicRegion.fall_farming),
|
||||||
|
ConnectionData(LogicEntrance.grow_winter_crops_in_greenhouse, LogicRegion.winter_farming),
|
||||||
|
ConnectionData(LogicEntrance.grow_indoor_crops_in_greenhouse, LogicRegion.indoor_farming),
|
||||||
|
ConnectionData(LogicEntrance.grow_summer_fall_crops_in_summer, LogicRegion.summer_or_fall_farming),
|
||||||
|
ConnectionData(LogicEntrance.grow_summer_fall_crops_in_fall, LogicRegion.summer_or_fall_farming),
|
||||||
|
|
||||||
|
ConnectionData(LogicEntrance.shipping, LogicRegion.shipping),
|
||||||
|
ConnectionData(LogicEntrance.blacksmith_copper, LogicRegion.blacksmith_copper),
|
||||||
|
ConnectionData(LogicEntrance.blacksmith_iron, LogicRegion.blacksmith_iron),
|
||||||
|
ConnectionData(LogicEntrance.blacksmith_gold, LogicRegion.blacksmith_gold),
|
||||||
|
ConnectionData(LogicEntrance.blacksmith_iridium, LogicRegion.blacksmith_iridium),
|
||||||
|
ConnectionData(LogicEntrance.fishing, LogicRegion.fishing),
|
||||||
|
ConnectionData(LogicEntrance.attend_egg_festival, LogicRegion.egg_festival),
|
||||||
|
ConnectionData(LogicEntrance.attend_desert_festival, LogicRegion.desert_festival),
|
||||||
|
ConnectionData(LogicEntrance.attend_flower_dance, LogicRegion.flower_dance),
|
||||||
|
ConnectionData(LogicEntrance.attend_luau, LogicRegion.luau),
|
||||||
|
ConnectionData(LogicEntrance.attend_trout_derby, LogicRegion.trout_derby),
|
||||||
|
ConnectionData(LogicEntrance.attend_moonlight_jellies, LogicRegion.moonlight_jellies),
|
||||||
|
ConnectionData(LogicEntrance.attend_fair, LogicRegion.fair),
|
||||||
|
ConnectionData(LogicEntrance.attend_spirit_eve, LogicRegion.spirit_eve),
|
||||||
|
ConnectionData(LogicEntrance.attend_festival_of_ice, LogicRegion.festival_of_ice),
|
||||||
|
ConnectionData(LogicEntrance.attend_night_market, LogicRegion.night_market),
|
||||||
|
ConnectionData(LogicEntrance.attend_winter_star, LogicRegion.winter_star),
|
||||||
|
ConnectionData(LogicEntrance.attend_squidfest, LogicRegion.squidfest),
|
||||||
|
ConnectionData(LogicEntrance.buy_experience_books, LogicRegion.bookseller_1),
|
||||||
|
ConnectionData(LogicEntrance.buy_year1_books, LogicRegion.bookseller_2),
|
||||||
|
ConnectionData(LogicEntrance.buy_year3_books, LogicRegion.bookseller_3),
|
||||||
|
)
|
||||||
|
|
||||||
|
ginger_island_connections = (
|
||||||
|
ConnectionData(Entrance.use_island_obelisk, RegionName.island_south),
|
||||||
|
ConnectionData(Entrance.use_farm_obelisk, RegionName.farm),
|
||||||
|
ConnectionData(Entrance.mountain_to_leo_treehouse, RegionName.leo_treehouse, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||||
|
ConnectionData(Entrance.fish_shop_to_boat_tunnel, RegionName.boat_tunnel, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(Entrance.boat_to_ginger_island, RegionName.island_south),
|
||||||
|
ConnectionData(Entrance.enter_dangerous_skull_cavern, RegionName.dangerous_skull_cavern),
|
||||||
|
ConnectionData(Entrance.dig_to_dangerous_mines_20, RegionName.dangerous_mines_20),
|
||||||
|
ConnectionData(Entrance.dig_to_dangerous_mines_60, RegionName.dangerous_mines_60),
|
||||||
|
ConnectionData(Entrance.dig_to_dangerous_mines_100, RegionName.dangerous_mines_100),
|
||||||
|
ConnectionData(Entrance.island_south_to_west, RegionName.island_west),
|
||||||
|
ConnectionData(Entrance.island_south_to_north, RegionName.island_north),
|
||||||
|
ConnectionData(Entrance.island_south_to_east, RegionName.island_east),
|
||||||
|
ConnectionData(Entrance.island_south_to_southeast, RegionName.island_south_east),
|
||||||
|
ConnectionData(Entrance.use_island_resort, RegionName.island_resort),
|
||||||
|
ConnectionData(Entrance.island_west_to_islandfarmhouse, RegionName.island_farmhouse, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(Entrance.island_west_to_gourmand_cave, RegionName.gourmand_frog_cave, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(Entrance.island_west_to_crystals_cave, RegionName.colored_crystals_cave, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(Entrance.island_west_to_shipwreck, RegionName.shipwreck, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(Entrance.island_west_to_qi_walnut_room, RegionName.qi_walnut_room, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(Entrance.island_east_to_leo_hut, RegionName.leo_hut, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(Entrance.island_east_to_island_shrine, RegionName.island_shrine, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(Entrance.island_southeast_to_pirate_cove, RegionName.pirate_cove, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(Entrance.island_north_to_field_office, RegionName.field_office, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(Entrance.island_north_to_dig_site, RegionName.dig_site),
|
||||||
|
ConnectionData(Entrance.dig_site_to_professor_snail_cave, RegionName.professor_snail_cave, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(Entrance.island_north_to_volcano, RegionName.volcano, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(Entrance.volcano_to_secret_beach, RegionName.volcano_secret_beach, flag=RandomizationFlag.BUILDINGS),
|
||||||
|
ConnectionData(Entrance.talk_to_island_trader, RegionName.island_trader),
|
||||||
|
ConnectionData(Entrance.climb_to_volcano_5, RegionName.volcano_floor_5),
|
||||||
|
ConnectionData(Entrance.talk_to_volcano_dwarf, RegionName.volcano_dwarf_shop),
|
||||||
|
ConnectionData(Entrance.climb_to_volcano_10, RegionName.volcano_floor_10),
|
||||||
|
ConnectionData(Entrance.parrot_express_jungle_to_docks, RegionName.island_south),
|
||||||
|
ConnectionData(Entrance.parrot_express_dig_site_to_docks, RegionName.island_south),
|
||||||
|
ConnectionData(Entrance.parrot_express_volcano_to_docks, RegionName.island_south),
|
||||||
|
ConnectionData(Entrance.parrot_express_volcano_to_jungle, RegionName.island_west),
|
||||||
|
ConnectionData(Entrance.parrot_express_docks_to_jungle, RegionName.island_west),
|
||||||
|
ConnectionData(Entrance.parrot_express_dig_site_to_jungle, RegionName.island_west),
|
||||||
|
ConnectionData(Entrance.parrot_express_docks_to_dig_site, RegionName.dig_site),
|
||||||
|
ConnectionData(Entrance.parrot_express_volcano_to_dig_site, RegionName.dig_site),
|
||||||
|
ConnectionData(Entrance.parrot_express_jungle_to_dig_site, RegionName.dig_site),
|
||||||
|
ConnectionData(Entrance.parrot_express_dig_site_to_volcano, RegionName.island_north),
|
||||||
|
ConnectionData(Entrance.parrot_express_docks_to_volcano, RegionName.island_north),
|
||||||
|
ConnectionData(Entrance.parrot_express_jungle_to_volcano, RegionName.island_north),
|
||||||
|
ConnectionData(LogicEntrance.grow_spring_crops_on_island, LogicRegion.spring_farming),
|
||||||
|
ConnectionData(LogicEntrance.grow_summer_crops_on_island, LogicRegion.summer_farming),
|
||||||
|
ConnectionData(LogicEntrance.grow_fall_crops_on_island, LogicRegion.fall_farming),
|
||||||
|
ConnectionData(LogicEntrance.grow_winter_crops_on_island, LogicRegion.winter_farming),
|
||||||
|
ConnectionData(LogicEntrance.grow_indoor_crops_on_island, LogicRegion.indoor_farming),
|
||||||
|
ConnectionData(LogicEntrance.island_cooking, LogicRegion.kitchen),
|
||||||
|
)
|
||||||
|
|
||||||
|
connections_without_ginger_island_by_name: Mapping[str, ConnectionData] = MappingProxyType({
|
||||||
|
connection.name: connection
|
||||||
|
for connection in vanilla_connections
|
||||||
|
})
|
||||||
|
regions_without_ginger_island_by_name: Mapping[str, RegionData] = MappingProxyType({
|
||||||
|
region.name: region
|
||||||
|
for region in vanilla_regions
|
||||||
|
})
|
||||||
|
|
||||||
|
connections_with_ginger_island_by_name: Mapping[str, ConnectionData] = MappingProxyType({
|
||||||
|
connection.name: connection
|
||||||
|
for connection in vanilla_connections + ginger_island_connections
|
||||||
|
})
|
||||||
|
regions_with_ginger_island_by_name: Mapping[str, RegionData] = MappingProxyType({
|
||||||
|
region.name: region
|
||||||
|
for region in vanilla_regions + ginger_island_regions
|
||||||
|
})
|
||||||
@@ -195,6 +195,7 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S
|
|||||||
set_entrance_rule(multiworld, player, Entrance.enter_tide_pools, logic.received("Beach Bridge") | (logic.mod.magic.can_blink()))
|
set_entrance_rule(multiworld, player, Entrance.enter_tide_pools, logic.received("Beach Bridge") | (logic.mod.magic.can_blink()))
|
||||||
set_entrance_rule(multiworld, player, Entrance.enter_quarry, logic.received("Bridge Repair") | (logic.mod.magic.can_blink()))
|
set_entrance_rule(multiworld, player, Entrance.enter_quarry, logic.received("Bridge Repair") | (logic.mod.magic.can_blink()))
|
||||||
set_entrance_rule(multiworld, player, Entrance.enter_secret_woods, logic.tool.has_tool(Tool.axe, "Iron") | (logic.mod.magic.can_blink()))
|
set_entrance_rule(multiworld, player, Entrance.enter_secret_woods, logic.tool.has_tool(Tool.axe, "Iron") | (logic.mod.magic.can_blink()))
|
||||||
|
set_entrance_rule(multiworld, player, Entrance.forest_to_wizard_tower, logic.region.can_reach(Region.community_center))
|
||||||
set_entrance_rule(multiworld, player, Entrance.forest_to_sewer, logic.wallet.has_rusty_key())
|
set_entrance_rule(multiworld, player, Entrance.forest_to_sewer, logic.wallet.has_rusty_key())
|
||||||
set_entrance_rule(multiworld, player, Entrance.town_to_sewer, logic.wallet.has_rusty_key())
|
set_entrance_rule(multiworld, player, Entrance.town_to_sewer, logic.wallet.has_rusty_key())
|
||||||
set_entrance_rule(multiworld, player, Entrance.enter_abandoned_jojamart, logic.has_abandoned_jojamart())
|
set_entrance_rule(multiworld, player, Entrance.enter_abandoned_jojamart, logic.has_abandoned_jojamart())
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
import random
|
|
||||||
import unittest
|
|
||||||
from typing import Set
|
|
||||||
|
|
||||||
from BaseClasses import get_seed
|
|
||||||
from .bases import SVTestCase
|
|
||||||
from .options.utils import fill_dataclass_with_default
|
|
||||||
from .. import create_content
|
|
||||||
from ..options import EntranceRandomization, ExcludeGingerIsland, SkillProgression
|
|
||||||
from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag, create_final_connections_and_regions
|
|
||||||
from ..strings.entrance_names import Entrance as EntranceName
|
|
||||||
from ..strings.region_names import Region as RegionName
|
|
||||||
|
|
||||||
connections_by_name = {connection.name for connection in vanilla_connections}
|
|
||||||
regions_by_name = {region.name for region in vanilla_regions}
|
|
||||||
|
|
||||||
|
|
||||||
class TestRegions(unittest.TestCase):
|
|
||||||
def test_region_exits_lead_somewhere(self):
|
|
||||||
for region in vanilla_regions:
|
|
||||||
with self.subTest(region=region):
|
|
||||||
for exit in region.exits:
|
|
||||||
self.assertIn(exit, connections_by_name,
|
|
||||||
f"{region.name} is leading to {exit} but it does not exist.")
|
|
||||||
|
|
||||||
def test_connection_lead_somewhere(self):
|
|
||||||
for connection in vanilla_connections:
|
|
||||||
with self.subTest(connection=connection):
|
|
||||||
self.assertIn(connection.destination, regions_by_name,
|
|
||||||
f"{connection.name} is leading to {connection.destination} but it does not exist.")
|
|
||||||
|
|
||||||
|
|
||||||
def explore_connections_tree_up_to_blockers(blocked_entrances: Set[str], connections_by_name, regions_by_name):
|
|
||||||
explored_entrances = set()
|
|
||||||
explored_regions = set()
|
|
||||||
entrances_to_explore = set()
|
|
||||||
current_node_name = "Menu"
|
|
||||||
current_node = regions_by_name[current_node_name]
|
|
||||||
entrances_to_explore.update(current_node.exits)
|
|
||||||
while entrances_to_explore:
|
|
||||||
current_entrance_name = entrances_to_explore.pop()
|
|
||||||
current_entrance = connections_by_name[current_entrance_name]
|
|
||||||
current_node_name = current_entrance.destination
|
|
||||||
|
|
||||||
explored_entrances.add(current_entrance_name)
|
|
||||||
explored_regions.add(current_node_name)
|
|
||||||
|
|
||||||
if current_entrance_name in blocked_entrances:
|
|
||||||
continue
|
|
||||||
|
|
||||||
current_node = regions_by_name[current_node_name]
|
|
||||||
entrances_to_explore.update({entrance for entrance in current_node.exits if entrance not in explored_entrances})
|
|
||||||
return explored_regions
|
|
||||||
|
|
||||||
|
|
||||||
class TestEntranceRando(SVTestCase):
|
|
||||||
|
|
||||||
def test_entrance_randomization(self):
|
|
||||||
for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
|
|
||||||
(EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION),
|
|
||||||
(EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS),
|
|
||||||
(EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]:
|
|
||||||
sv_options = fill_dataclass_with_default({
|
|
||||||
EntranceRandomization.internal_name: option,
|
|
||||||
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
|
|
||||||
SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
|
|
||||||
})
|
|
||||||
content = create_content(sv_options)
|
|
||||||
seed = get_seed()
|
|
||||||
rand = random.Random(seed)
|
|
||||||
with self.subTest(flag=flag, msg=f"Seed: {seed}"):
|
|
||||||
entrances, regions = create_final_connections_and_regions(sv_options)
|
|
||||||
_, randomized_connections = randomize_connections(rand, sv_options, content, regions, entrances)
|
|
||||||
|
|
||||||
for connection in vanilla_connections:
|
|
||||||
if flag in connection.flag:
|
|
||||||
connection_in_randomized = connection.name in randomized_connections
|
|
||||||
reverse_in_randomized = connection.reverse in randomized_connections
|
|
||||||
self.assertTrue(connection_in_randomized, f"Connection {connection.name} should be randomized but it is not in the output.")
|
|
||||||
self.assertTrue(reverse_in_randomized, f"Connection {connection.reverse} should be randomized but it is not in the output.")
|
|
||||||
|
|
||||||
self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()),
|
|
||||||
f"Connections are duplicated in randomization.")
|
|
||||||
|
|
||||||
def test_entrance_randomization_without_island(self):
|
|
||||||
for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
|
|
||||||
(EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION),
|
|
||||||
(EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS),
|
|
||||||
(EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]:
|
|
||||||
|
|
||||||
sv_options = fill_dataclass_with_default({
|
|
||||||
EntranceRandomization.internal_name: option,
|
|
||||||
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
|
|
||||||
SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
|
|
||||||
})
|
|
||||||
content = create_content(sv_options)
|
|
||||||
seed = get_seed()
|
|
||||||
rand = random.Random(seed)
|
|
||||||
with self.subTest(option=option, flag=flag, seed=seed):
|
|
||||||
entrances, regions = create_final_connections_and_regions(sv_options)
|
|
||||||
_, randomized_connections = randomize_connections(rand, sv_options, content, regions, entrances)
|
|
||||||
|
|
||||||
for connection in vanilla_connections:
|
|
||||||
if flag in connection.flag:
|
|
||||||
if RandomizationFlag.GINGER_ISLAND in connection.flag:
|
|
||||||
self.assertNotIn(connection.name, randomized_connections,
|
|
||||||
f"Connection {connection.name} should not be randomized but it is in the output.")
|
|
||||||
self.assertNotIn(connection.reverse, randomized_connections,
|
|
||||||
f"Connection {connection.reverse} should not be randomized but it is in the output.")
|
|
||||||
else:
|
|
||||||
self.assertIn(connection.name, randomized_connections,
|
|
||||||
f"Connection {connection.name} should be randomized but it is not in the output.")
|
|
||||||
self.assertIn(connection.reverse, randomized_connections,
|
|
||||||
f"Connection {connection.reverse} should be randomized but it is not in the output.")
|
|
||||||
|
|
||||||
self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()),
|
|
||||||
f"Connections are duplicated in randomization.")
|
|
||||||
|
|
||||||
def test_cannot_put_island_access_on_island(self):
|
|
||||||
sv_options = fill_dataclass_with_default({
|
|
||||||
EntranceRandomization.internal_name: EntranceRandomization.option_buildings,
|
|
||||||
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
|
|
||||||
SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
|
|
||||||
})
|
|
||||||
content = create_content(sv_options)
|
|
||||||
|
|
||||||
for i in range(0, 100 if self.skip_long_tests else 10000):
|
|
||||||
seed = get_seed()
|
|
||||||
rand = random.Random(seed)
|
|
||||||
with self.subTest(msg=f"Seed: {seed}"):
|
|
||||||
entrances, regions = create_final_connections_and_regions(sv_options)
|
|
||||||
randomized_connections, randomized_data = randomize_connections(rand, sv_options, content, regions, entrances)
|
|
||||||
connections_by_name = {connection.name: connection for connection in randomized_connections}
|
|
||||||
|
|
||||||
blocked_entrances = {EntranceName.use_island_obelisk, EntranceName.boat_to_ginger_island}
|
|
||||||
required_regions = {RegionName.wizard_tower, RegionName.boat_tunnel}
|
|
||||||
self.assert_can_reach_any_region_before_blockers(required_regions, blocked_entrances, connections_by_name, regions)
|
|
||||||
|
|
||||||
def assert_can_reach_any_region_before_blockers(self, required_regions, blocked_entrances, connections_by_name, regions_by_name):
|
|
||||||
explored_regions = explore_connections_tree_up_to_blockers(blocked_entrances, connections_by_name, regions_by_name)
|
|
||||||
self.assertTrue(any(region in explored_regions for region in required_regions))
|
|
||||||
|
|
||||||
|
|
||||||
class TestEntranceClassifications(SVTestCase):
|
|
||||||
|
|
||||||
def test_non_progression_are_all_accessible_with_empty_inventory(self):
|
|
||||||
for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
|
|
||||||
(EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION)]:
|
|
||||||
world_options = {
|
|
||||||
EntranceRandomization.internal_name: option
|
|
||||||
}
|
|
||||||
with self.solo_world_sub_test(world_options=world_options, flag=flag) as (multiworld, sv_world):
|
|
||||||
ap_entrances = {entrance.name: entrance for entrance in multiworld.get_entrances()}
|
|
||||||
for randomized_entrance in sv_world.randomized_entrances:
|
|
||||||
if randomized_entrance in ap_entrances:
|
|
||||||
ap_entrance_origin = ap_entrances[randomized_entrance]
|
|
||||||
self.assertTrue(ap_entrance_origin.access_rule(multiworld.state))
|
|
||||||
if sv_world.randomized_entrances[randomized_entrance] in ap_entrances:
|
|
||||||
ap_entrance_destination = multiworld.get_entrance(sv_world.randomized_entrances[randomized_entrance], 1)
|
|
||||||
self.assertTrue(ap_entrance_destination.access_rule(multiworld.state))
|
|
||||||
|
|
||||||
def test_no_ginger_island_entrances_when_excluded(self):
|
|
||||||
world_options = {
|
|
||||||
EntranceRandomization.internal_name: EntranceRandomization.option_disabled,
|
|
||||||
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true
|
|
||||||
}
|
|
||||||
with self.solo_world_sub_test(world_options=world_options) as (multiworld, _):
|
|
||||||
ap_entrances = {entrance.name: entrance for entrance in multiworld.get_entrances()}
|
|
||||||
entrance_data_by_name = {entrance.name: entrance for entrance in vanilla_connections}
|
|
||||||
for entrance_name in ap_entrances:
|
|
||||||
entrance_data = entrance_data_by_name[entrance_name]
|
|
||||||
with self.subTest(f"{entrance_name}: {entrance_data.flag}"):
|
|
||||||
self.assertFalse(entrance_data.flag & RandomizationFlag.GINGER_ISLAND)
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from BaseClasses import CollectionState, Location, Region
|
from BaseClasses import CollectionState, Location, Region, Entrance
|
||||||
from ...stardew_rule import StardewRule, false_, MISSING_ITEM, Reach
|
from ...stardew_rule import StardewRule, false_, MISSING_ITEM, Reach
|
||||||
from ...stardew_rule.rule_explain import explain
|
from ...stardew_rule.rule_explain import explain
|
||||||
|
|
||||||
@@ -79,3 +79,13 @@ class RuleAssertMixin(TestCase):
|
|||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
raise AssertionError(f"Error while checking region {region_name}: {e}"
|
raise AssertionError(f"Error while checking region {region_name}: {e}"
|
||||||
f"\nExplanation: {expl}")
|
f"\nExplanation: {expl}")
|
||||||
|
|
||||||
|
def assert_can_reach_entrance(self, entrance: Entrance | str, state: CollectionState) -> None:
|
||||||
|
entrance_name = entrance.name if isinstance(entrance, Entrance) else entrance
|
||||||
|
expl = explain(Reach(entrance_name, "Entrance", 1), state)
|
||||||
|
try:
|
||||||
|
can_reach = state.can_reach_entrance(entrance_name, 1)
|
||||||
|
self.assertTrue(can_reach, expl)
|
||||||
|
except KeyError as e:
|
||||||
|
raise AssertionError(f"Error while checking entrance {entrance_name}: {e}"
|
||||||
|
f"\nExplanation: {expl}")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import unittest
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Optional, Dict, Union, Any, List, Iterable
|
from typing import Optional, Dict, Union, Any, List, Iterable
|
||||||
|
|
||||||
from BaseClasses import get_seed, MultiWorld, Location, Item, CollectionState
|
from BaseClasses import get_seed, MultiWorld, Location, Item, CollectionState, Entrance
|
||||||
from test.bases import WorldTestBase
|
from test.bases import WorldTestBase
|
||||||
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
|
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
|
||||||
from worlds.AutoWorld import call_all
|
from worlds.AutoWorld import call_all
|
||||||
@@ -179,6 +179,11 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
|
|||||||
state = self.multiworld.state
|
state = self.multiworld.state
|
||||||
super().assert_cannot_reach_location(location, state)
|
super().assert_cannot_reach_location(location, state)
|
||||||
|
|
||||||
|
def assert_can_reach_entrance(self, entrance: Entrance | str, state: CollectionState | None = None) -> None:
|
||||||
|
if state is None:
|
||||||
|
state = self.multiworld.state
|
||||||
|
super().assert_can_reach_entrance(entrance, state)
|
||||||
|
|
||||||
|
|
||||||
pre_generated_worlds = {}
|
pre_generated_worlds = {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import random
|
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
|
|
||||||
from BaseClasses import get_seed
|
|
||||||
from test.param import classvar_matrix
|
from test.param import classvar_matrix
|
||||||
from ..TestGeneration import get_all_permanent_progression_items
|
from ..TestGeneration import get_all_permanent_progression_items
|
||||||
from ..assertion import ModAssertMixin, WorldAssertMixin
|
from ..assertion import ModAssertMixin, WorldAssertMixin
|
||||||
from ..bases import SVTestCase, SVTestBase, solo_multiworld
|
from ..bases import SVTestCase, SVTestBase, solo_multiworld
|
||||||
from ..options.presets import allsanity_mods_6_x_x
|
from ..options.presets import allsanity_mods_6_x_x
|
||||||
from ..options.utils import fill_dataclass_with_default
|
from ... import options, Group
|
||||||
from ... import options, Group, create_content
|
|
||||||
from ...mods.mod_data import ModNames
|
from ...mods.mod_data import ModNames
|
||||||
from ...options.options import all_mods
|
from ...options.options import all_mods
|
||||||
from ...regions import RandomizationFlag, randomize_connections, create_final_connections_and_regions
|
|
||||||
|
|
||||||
|
|
||||||
class TestCanGenerateAllsanityWithMods(WorldAssertMixin, ModAssertMixin, SVTestCase):
|
class TestCanGenerateAllsanityWithMods(WorldAssertMixin, ModAssertMixin, SVTestCase):
|
||||||
@@ -117,39 +113,6 @@ class TestNoGingerIslandModItemGeneration(SVTestBase):
|
|||||||
self.assertIn(progression_item.name, all_created_items)
|
self.assertIn(progression_item.name, all_created_items)
|
||||||
|
|
||||||
|
|
||||||
class TestModEntranceRando(SVTestCase):
|
|
||||||
|
|
||||||
def test_mod_entrance_randomization(self):
|
|
||||||
for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
|
|
||||||
(options.EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION),
|
|
||||||
(options.EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS),
|
|
||||||
(options.EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]:
|
|
||||||
sv_options = fill_dataclass_with_default({
|
|
||||||
options.EntranceRandomization.internal_name: option,
|
|
||||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
|
|
||||||
options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries,
|
|
||||||
options.Mods.internal_name: frozenset(options.Mods.valid_keys)
|
|
||||||
})
|
|
||||||
content = create_content(sv_options)
|
|
||||||
seed = get_seed()
|
|
||||||
rand = random.Random(seed)
|
|
||||||
with self.subTest(option=option, flag=flag, seed=seed):
|
|
||||||
final_connections, final_regions = create_final_connections_and_regions(sv_options)
|
|
||||||
|
|
||||||
_, randomized_connections = randomize_connections(rand, sv_options, content, final_regions, final_connections)
|
|
||||||
|
|
||||||
for connection_name in final_connections:
|
|
||||||
connection = final_connections[connection_name]
|
|
||||||
if flag in connection.flag:
|
|
||||||
connection_in_randomized = connection_name in randomized_connections
|
|
||||||
reverse_in_randomized = connection.reverse in randomized_connections
|
|
||||||
self.assertTrue(connection_in_randomized, f"Connection {connection_name} should be randomized but it is not in the output")
|
|
||||||
self.assertTrue(reverse_in_randomized, f"Connection {connection.reverse} should be randomized but it is not in the output.")
|
|
||||||
|
|
||||||
self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()),
|
|
||||||
f"Connections are duplicated in randomization.")
|
|
||||||
|
|
||||||
|
|
||||||
class TestVanillaLogicAlternativeWhenQuestsAreNotRandomized(WorldAssertMixin, SVTestBase):
|
class TestVanillaLogicAlternativeWhenQuestsAreNotRandomized(WorldAssertMixin, SVTestBase):
|
||||||
"""We often forget to add an alternative rule that works when quests are not randomized. When this happens, some
|
"""We often forget to add an alternative rule that works when quests are not randomized. When this happens, some
|
||||||
Location are not reachable because they depend on items that are only added to the pool when quests are randomized.
|
Location are not reachable because they depend on items that are only added to the pool when quests are randomized.
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from ..bases import SVTestBase
|
||||||
|
from ... import options
|
||||||
|
from ...regions.model import RandomizationFlag
|
||||||
|
from ...regions.regions import create_all_connections
|
||||||
|
|
||||||
|
|
||||||
|
class EntranceRandomizationAssertMixin:
|
||||||
|
|
||||||
|
def assert_non_progression_are_all_accessible_with_empty_inventory(self: SVTestBase):
|
||||||
|
all_connections = create_all_connections(self.world.content.registered_packs)
|
||||||
|
non_progression_connections = [connection for connection in all_connections.values() if RandomizationFlag.BIT_NON_PROGRESSION in connection.flag]
|
||||||
|
|
||||||
|
for non_progression_connections in non_progression_connections:
|
||||||
|
with self.subTest(connection=non_progression_connections):
|
||||||
|
self.assert_can_reach_entrance(non_progression_connections.name)
|
||||||
|
|
||||||
|
|
||||||
|
# This test does not actually need to generate with entrance randomization. Entrances rules are the same regardless of the randomization.
|
||||||
|
class TestVanillaEntranceClassifications(EntranceRandomizationAssertMixin, SVTestBase):
|
||||||
|
options = {
|
||||||
|
options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false,
|
||||||
|
options.Mods: frozenset()
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_non_progression_are_all_accessible_with_empty_inventory(self):
|
||||||
|
self.assert_non_progression_are_all_accessible_with_empty_inventory()
|
||||||
|
|
||||||
|
|
||||||
|
class TestModdedEntranceClassifications(EntranceRandomizationAssertMixin, SVTestBase):
|
||||||
|
options = {
|
||||||
|
options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false,
|
||||||
|
options.Mods: frozenset(options.Mods.valid_keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_non_progression_are_all_accessible_with_empty_inventory(self):
|
||||||
|
self.assert_non_progression_are_all_accessible_with_empty_inventory()
|
||||||
167
worlds/stardew_valley/test/regions/TestEntranceRandomization.py
Normal file
167
worlds/stardew_valley/test/regions/TestEntranceRandomization.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
from collections import deque
|
||||||
|
from collections.abc import Collection
|
||||||
|
from unittest.mock import patch, Mock
|
||||||
|
|
||||||
|
from BaseClasses import get_seed, MultiWorld, Entrance
|
||||||
|
from ..assertion import WorldAssertMixin
|
||||||
|
from ..bases import SVTestCase, solo_multiworld
|
||||||
|
from ... import options
|
||||||
|
from ...mods.mod_data import ModNames
|
||||||
|
from ...options import EntranceRandomization, ExcludeGingerIsland, SkillProgression
|
||||||
|
from ...options.options import all_mods
|
||||||
|
from ...regions.entrance_rando import create_entrance_rando_target, prepare_mod_data, connect_regions
|
||||||
|
from ...regions.model import RegionData, ConnectionData, RandomizationFlag
|
||||||
|
from ...strings.entrance_names import Entrance as EntranceName
|
||||||
|
from ...strings.region_names import Region as RegionName
|
||||||
|
|
||||||
|
|
||||||
|
class TestEntranceRando(SVTestCase):
|
||||||
|
|
||||||
|
def test_given_connection_matching_randomization_when_connect_regions_then_make_connection_entrance_rando_target(self):
|
||||||
|
region_data_by_name = {
|
||||||
|
"Region1": RegionData("Region1", ("randomized_connection", "not_randomized")),
|
||||||
|
"Region2": RegionData("Region2"),
|
||||||
|
"Region3": RegionData("Region3"),
|
||||||
|
}
|
||||||
|
connection_data_by_name = {
|
||||||
|
"randomized_connection": ConnectionData("randomized_connection", "Region2", flag=RandomizationFlag.PELICAN_TOWN),
|
||||||
|
"not_randomized": ConnectionData("not_randomized", "Region2", flag=RandomizationFlag.BUILDINGS),
|
||||||
|
}
|
||||||
|
regions_by_name = {
|
||||||
|
"Region1": Mock(),
|
||||||
|
"Region2": Mock(),
|
||||||
|
"Region3": Mock(),
|
||||||
|
}
|
||||||
|
player_randomization_flag = RandomizationFlag.BIT_PELICAN_TOWN
|
||||||
|
|
||||||
|
with patch("worlds.stardew_valley.regions.entrance_rando.create_entrance_rando_target") as mock_create_entrance_rando_target:
|
||||||
|
connect_regions(region_data_by_name, connection_data_by_name, regions_by_name, player_randomization_flag)
|
||||||
|
|
||||||
|
expected_origin, expected_destination = regions_by_name["Region1"], regions_by_name["Region2"]
|
||||||
|
expected_connection = connection_data_by_name["randomized_connection"]
|
||||||
|
mock_create_entrance_rando_target.assert_called_once_with(expected_origin, expected_destination, expected_connection)
|
||||||
|
|
||||||
|
def test_when_create_entrance_rando_target_then_create_exit_and_er_target(self):
|
||||||
|
origin = Mock()
|
||||||
|
destination = Mock()
|
||||||
|
connection_data = ConnectionData("origin to destination", "destination")
|
||||||
|
|
||||||
|
create_entrance_rando_target(origin, destination, connection_data)
|
||||||
|
|
||||||
|
origin.create_exit.assert_called_once_with("origin to destination")
|
||||||
|
destination.create_er_target.assert_called_once_with("destination to origin")
|
||||||
|
|
||||||
|
def test_when_prepare_mod_data_then_swapped_connections_contains_both_directions(self):
|
||||||
|
placements = Mock(pairings=[("A to B", "C to A"), ("C to D", "A to C")])
|
||||||
|
|
||||||
|
swapped_connections = prepare_mod_data(placements)
|
||||||
|
|
||||||
|
self.assertEqual({"A to B": "A to C", "C to A": "B to A", "C to D": "C to A", "A to C": "D to C"}, swapped_connections)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEntranceRandoCreatesValidWorlds(WorldAssertMixin, SVTestCase):
|
||||||
|
|
||||||
|
# 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_ginger_island_excluded_buildings(self):
|
||||||
|
world_options = {
|
||||||
|
options.EntranceRandomization: options.EntranceRandomization.option_buildings,
|
||||||
|
options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true
|
||||||
|
}
|
||||||
|
with solo_multiworld(world_options) as (multi_world, _):
|
||||||
|
self.assert_basic_checks(multi_world)
|
||||||
|
|
||||||
|
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 solo_multiworld(world_options) as (multi_world, _):
|
||||||
|
self.assert_basic_checks(multi_world)
|
||||||
|
|
||||||
|
|
||||||
|
# GER should have this covered, but it's good to have a backup
|
||||||
|
class TestGingerIslandEntranceRando(SVTestCase):
|
||||||
|
def test_cannot_put_island_access_on_island(self):
|
||||||
|
test_options = {
|
||||||
|
options.EntranceRandomization: EntranceRandomization.option_buildings,
|
||||||
|
options.ExcludeGingerIsland: ExcludeGingerIsland.option_false,
|
||||||
|
options.SkillProgression: SkillProgression.option_progressive_with_masteries,
|
||||||
|
}
|
||||||
|
|
||||||
|
blocked_entrances = {EntranceName.use_island_obelisk, EntranceName.boat_to_ginger_island}
|
||||||
|
required_regions = {RegionName.wizard_tower, RegionName.boat_tunnel}
|
||||||
|
|
||||||
|
for i in range(0, 10 if self.skip_long_tests else 1000):
|
||||||
|
seed = get_seed()
|
||||||
|
with self.solo_world_sub_test(f"Seed: {seed}", world_options=test_options, world_caching=False, seed=seed) as (multiworld, world):
|
||||||
|
self.assert_can_reach_any_region_before_blockers(required_regions, blocked_entrances, multiworld)
|
||||||
|
|
||||||
|
def assert_can_reach_any_region_before_blockers(self, required_regions: Collection[str], blocked_entrances: Collection[str], multiworld: MultiWorld):
|
||||||
|
explored_regions = explore_regions_up_to_blockers(blocked_entrances, multiworld)
|
||||||
|
self.assertTrue(any(region in explored_regions for region in required_regions))
|
||||||
|
|
||||||
|
|
||||||
|
def explore_regions_up_to_blockers(blocked_entrances: Collection[str], multiworld: MultiWorld) -> set[str]:
|
||||||
|
explored_regions: set[str] = set()
|
||||||
|
regions_by_name = multiworld.regions.region_cache[1]
|
||||||
|
regions_to_explore = deque([regions_by_name["Menu"]])
|
||||||
|
|
||||||
|
while regions_to_explore:
|
||||||
|
region = regions_to_explore.pop()
|
||||||
|
|
||||||
|
if region.name in explored_regions:
|
||||||
|
continue
|
||||||
|
|
||||||
|
explored_regions.add(region.name)
|
||||||
|
|
||||||
|
for exit_ in region.exits:
|
||||||
|
exit_: Entrance
|
||||||
|
if exit_.name in blocked_entrances:
|
||||||
|
continue
|
||||||
|
regions_to_explore.append(exit_.connected_region)
|
||||||
|
|
||||||
|
return explored_regions
|
||||||
88
worlds/stardew_valley/test/regions/TestRandomizationFlag.py
Normal file
88
worlds/stardew_valley/test/regions/TestRandomizationFlag.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from ..options.utils import fill_dataclass_with_default
|
||||||
|
from ... import create_content, options
|
||||||
|
from ...regions.entrance_rando import create_player_randomization_flag
|
||||||
|
from ...regions.model import RandomizationFlag, ConnectionData
|
||||||
|
|
||||||
|
|
||||||
|
class TestConnectionData(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_given_entrances_not_randomized_when_is_eligible_for_randomization_then_not_eligible(self):
|
||||||
|
player_flag = RandomizationFlag.NOT_RANDOMIZED
|
||||||
|
|
||||||
|
connection = ConnectionData("Go to Somewhere", "Somewhere", RandomizationFlag.PELICAN_TOWN)
|
||||||
|
is_eligible = connection.is_eligible_for_randomization(player_flag)
|
||||||
|
|
||||||
|
self.assertFalse(is_eligible)
|
||||||
|
|
||||||
|
def test_given_pelican_town_connection_when_is_eligible_for_pelican_town_randomization_then_eligible(self):
|
||||||
|
player_flag = RandomizationFlag.BIT_PELICAN_TOWN
|
||||||
|
connection = ConnectionData("Go to Somewhere", "Somewhere", RandomizationFlag.PELICAN_TOWN)
|
||||||
|
|
||||||
|
is_eligible = connection.is_eligible_for_randomization(player_flag)
|
||||||
|
|
||||||
|
self.assertTrue(is_eligible)
|
||||||
|
|
||||||
|
def test_given_pelican_town_connection_when_is_eligible_for_buildings_randomization_then_eligible(self):
|
||||||
|
player_flag = RandomizationFlag.BIT_BUILDINGS
|
||||||
|
connection = ConnectionData("Go to Somewhere", "Somewhere", RandomizationFlag.PELICAN_TOWN)
|
||||||
|
|
||||||
|
is_eligible = connection.is_eligible_for_randomization(player_flag)
|
||||||
|
|
||||||
|
self.assertTrue(is_eligible)
|
||||||
|
|
||||||
|
def test_given_non_progression_connection_when_is_eligible_for_pelican_town_randomization_then_not_eligible(self):
|
||||||
|
player_flag = RandomizationFlag.BIT_PELICAN_TOWN
|
||||||
|
connection = ConnectionData("Go to Somewhere", "Somewhere", RandomizationFlag.NON_PROGRESSION)
|
||||||
|
|
||||||
|
is_eligible = connection.is_eligible_for_randomization(player_flag)
|
||||||
|
|
||||||
|
self.assertFalse(is_eligible)
|
||||||
|
|
||||||
|
def test_given_non_progression_masteries_connection_when_is_eligible_for_non_progression_randomization_then_eligible(self):
|
||||||
|
player_flag = RandomizationFlag.BIT_NON_PROGRESSION
|
||||||
|
connection = ConnectionData("Go to Somewhere", "Somewhere", RandomizationFlag.NON_PROGRESSION ^ RandomizationFlag.EXCLUDE_MASTERIES)
|
||||||
|
|
||||||
|
is_eligible = connection.is_eligible_for_randomization(player_flag)
|
||||||
|
|
||||||
|
self.assertTrue(is_eligible)
|
||||||
|
|
||||||
|
def test_given_non_progression_masteries_connection_when_is_eligible_for_non_progression_without_masteries_randomization_then_not_eligible(self):
|
||||||
|
player_flag = RandomizationFlag.BIT_NON_PROGRESSION | RandomizationFlag.EXCLUDE_MASTERIES
|
||||||
|
connection = ConnectionData("Go to Somewhere", "Somewhere", RandomizationFlag.NON_PROGRESSION ^ RandomizationFlag.EXCLUDE_MASTERIES)
|
||||||
|
|
||||||
|
is_eligible = connection.is_eligible_for_randomization(player_flag)
|
||||||
|
|
||||||
|
self.assertFalse(is_eligible)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRandomizationFlag(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_given_entrance_randomization_choice_when_create_player_randomization_flag_then_only_relevant_bit_is_enabled(self):
|
||||||
|
for entrance_randomization_choice, expected_bit in (
|
||||||
|
(options.EntranceRandomization.option_disabled, RandomizationFlag.NOT_RANDOMIZED),
|
||||||
|
(options.EntranceRandomization.option_pelican_town, RandomizationFlag.BIT_PELICAN_TOWN),
|
||||||
|
(options.EntranceRandomization.option_non_progression, RandomizationFlag.BIT_NON_PROGRESSION),
|
||||||
|
(options.EntranceRandomization.option_buildings_without_house, RandomizationFlag.BIT_BUILDINGS),
|
||||||
|
(options.EntranceRandomization.option_buildings, RandomizationFlag.BIT_BUILDINGS),
|
||||||
|
(options.EntranceRandomization.option_chaos, RandomizationFlag.BIT_BUILDINGS),
|
||||||
|
):
|
||||||
|
player_options = fill_dataclass_with_default({options.EntranceRandomization: entrance_randomization_choice})
|
||||||
|
content = create_content(player_options)
|
||||||
|
|
||||||
|
flag = create_player_randomization_flag(player_options.entrance_randomization, content)
|
||||||
|
|
||||||
|
self.assertEqual(flag, expected_bit)
|
||||||
|
|
||||||
|
def test_given_masteries_not_randomized_when_create_player_randomization_flag_then_exclude_masteries_bit_enabled(self):
|
||||||
|
for entrance_randomization_choice in set(options.EntranceRandomization.options.values()) ^ {options.EntranceRandomization.option_disabled}:
|
||||||
|
player_options = fill_dataclass_with_default({
|
||||||
|
options.EntranceRandomization: entrance_randomization_choice,
|
||||||
|
options.SkillProgression: options.SkillProgression.option_progressive
|
||||||
|
})
|
||||||
|
content = create_content(player_options)
|
||||||
|
|
||||||
|
flag = create_player_randomization_flag(player_options.entrance_randomization, content)
|
||||||
|
|
||||||
|
self.assertIn(RandomizationFlag.EXCLUDE_MASTERIES, flag)
|
||||||
66
worlds/stardew_valley/test/regions/TestRegionConnections.py
Normal file
66
worlds/stardew_valley/test/regions/TestRegionConnections.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from ..options.utils import fill_dataclass_with_default
|
||||||
|
from ... import options
|
||||||
|
from ...content import create_content
|
||||||
|
from ...mods.region_data import region_data_by_content_pack
|
||||||
|
from ...regions import vanilla_data
|
||||||
|
from ...regions.model import MergeFlag
|
||||||
|
from ...regions.regions import create_all_regions, create_all_connections
|
||||||
|
|
||||||
|
|
||||||
|
class TestVanillaRegionsConnectionsWithGingerIsland(unittest.TestCase):
|
||||||
|
def test_region_exits_lead_somewhere(self):
|
||||||
|
for region in vanilla_data.regions_with_ginger_island_by_name.values():
|
||||||
|
with self.subTest(region=region):
|
||||||
|
for exit_ in region.exits:
|
||||||
|
self.assertIn(exit_, vanilla_data.connections_with_ginger_island_by_name,
|
||||||
|
f"{region.name} is leading to {exit_} but it does not exist.")
|
||||||
|
|
||||||
|
def test_connection_lead_somewhere(self):
|
||||||
|
for connection in vanilla_data.connections_with_ginger_island_by_name.values():
|
||||||
|
with self.subTest(connection=connection):
|
||||||
|
self.assertIn(connection.destination, vanilla_data.regions_with_ginger_island_by_name,
|
||||||
|
f"{connection.name} is leading to {connection.destination} but it does not exist.")
|
||||||
|
|
||||||
|
|
||||||
|
class TestVanillaRegionsConnectionsWithoutGingerIsland(unittest.TestCase):
|
||||||
|
def test_region_exits_lead_somewhere(self):
|
||||||
|
for region in vanilla_data.regions_without_ginger_island_by_name.values():
|
||||||
|
with self.subTest(region=region):
|
||||||
|
for exit_ in region.exits:
|
||||||
|
self.assertIn(exit_, vanilla_data.connections_without_ginger_island_by_name,
|
||||||
|
f"{region.name} is leading to {exit_} but it does not exist.")
|
||||||
|
|
||||||
|
def test_connection_lead_somewhere(self):
|
||||||
|
for connection in vanilla_data.connections_without_ginger_island_by_name.values():
|
||||||
|
with self.subTest(connection=connection):
|
||||||
|
self.assertIn(connection.destination, vanilla_data.regions_without_ginger_island_by_name,
|
||||||
|
f"{connection.name} is leading to {connection.destination} but it does not exist.")
|
||||||
|
|
||||||
|
|
||||||
|
class TestModsConnections(unittest.TestCase):
|
||||||
|
options = {
|
||||||
|
options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false,
|
||||||
|
options.Mods: frozenset(options.Mods.valid_keys)
|
||||||
|
}
|
||||||
|
content = create_content(fill_dataclass_with_default(options))
|
||||||
|
all_regions_by_name = create_all_regions(content.registered_packs)
|
||||||
|
all_connections_by_name = create_all_connections(content.registered_packs)
|
||||||
|
|
||||||
|
def test_region_exits_lead_somewhere(self):
|
||||||
|
for mod_region_data in region_data_by_content_pack.values():
|
||||||
|
for region in mod_region_data.regions:
|
||||||
|
if MergeFlag.REMOVE_EXITS in region.flag:
|
||||||
|
continue
|
||||||
|
|
||||||
|
with self.subTest(mod=mod_region_data.mod_name, region=region.name):
|
||||||
|
for exit_ in region.exits:
|
||||||
|
self.assertIn(exit_, self.all_connections_by_name, f"{region.name} is leading to {exit_} but it does not exist.")
|
||||||
|
|
||||||
|
def test_connection_lead_somewhere(self):
|
||||||
|
for mod_region_data in region_data_by_content_pack.values():
|
||||||
|
for connection in mod_region_data.connections:
|
||||||
|
with self.subTest(mod=mod_region_data.mod_name, connection=connection.name):
|
||||||
|
self.assertIn(connection.destination, self.all_regions_by_name,
|
||||||
|
f"{connection.name} is leading to {connection.destination} but it does not exist.")
|
||||||
0
worlds/stardew_valley/test/regions/__init__.py
Normal file
0
worlds/stardew_valley/test/regions/__init__.py
Normal file
@@ -92,7 +92,7 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio
|
|||||||
LocationData('Military Fortress (hangar)', 'Military Fortress: Pedestal', 1337065, lambda state: state.has('Water Mask', player) if flooded.flood_lab else (logic.has_doublejump_of_npc(state) or logic.has_forwarddash_doublejump(state))),
|
LocationData('Military Fortress (hangar)', 'Military Fortress: Pedestal', 1337065, lambda state: state.has('Water Mask', player) if flooded.flood_lab else (logic.has_doublejump_of_npc(state) or logic.has_forwarddash_doublejump(state))),
|
||||||
LocationData('The lab', 'Lab: Coffee break', 1337066),
|
LocationData('The lab', 'Lab: Coffee break', 1337066),
|
||||||
LocationData('The lab', 'Lab: Lower trash right', 1337067, logic.has_doublejump),
|
LocationData('The lab', 'Lab: Lower trash right', 1337067, logic.has_doublejump),
|
||||||
LocationData('The lab', 'Lab: Lower trash left', 1337068, lambda state: logic.has_doublejump_of_npc(state) if options.lock_key_amadeus else logic.has_upwarddash ),
|
LocationData('The lab', 'Lab: Lower trash left', 1337068, lambda state: logic.has_doublejump_of_npc(state) if options.lock_key_amadeus else logic.has_upwarddash(state) ),
|
||||||
LocationData('The lab', 'Lab: Below lab entrance', 1337069, logic.has_doublejump),
|
LocationData('The lab', 'Lab: Below lab entrance', 1337069, logic.has_doublejump),
|
||||||
LocationData('The lab (power off)', 'Lab: Trash jump room', 1337070, lambda state: not options.lock_key_amadeus or logic.has_doublejump_of_npc(state) ),
|
LocationData('The lab (power off)', 'Lab: Trash jump room', 1337070, lambda state: not options.lock_key_amadeus or logic.has_doublejump_of_npc(state) ),
|
||||||
LocationData('The lab (power off)', 'Lab: Dynamo Works', 1337071, lambda state: not options.lock_key_amadeus or (state.has_all(('Lab Access Research', 'Lab Access Dynamo'), player)) ),
|
LocationData('The lab (power off)', 'Lab: Dynamo Works', 1337071, lambda state: not options.lock_key_amadeus or (state.has_all(('Lab Access Research', 'Lab Access Dynamo'), player)) ),
|
||||||
@@ -100,7 +100,7 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio
|
|||||||
LocationData('The lab (power off)', 'Lab: Experiment #13', 1337073, lambda state: not options.lock_key_amadeus or state.has('Lab Access Experiment', player) ),
|
LocationData('The lab (power off)', 'Lab: Experiment #13', 1337073, lambda state: not options.lock_key_amadeus or state.has('Lab Access Experiment', player) ),
|
||||||
LocationData('The lab (upper)', 'Lab: Download and chest room chest', 1337074),
|
LocationData('The lab (upper)', 'Lab: Download and chest room chest', 1337074),
|
||||||
LocationData('The lab (upper)', 'Lab: Lab secret', 1337075, logic.can_break_walls),
|
LocationData('The lab (upper)', 'Lab: Lab secret', 1337075, logic.can_break_walls),
|
||||||
LocationData('The lab (power off)', 'Lab: Spider Hell', 1337076, lambda state: logic.has_keycard_A and not options.lock_key_amadeus or state.has('Lab Access Research', player)),
|
LocationData('The lab (power off)', 'Lab: Spider Hell', 1337076, lambda state: logic.has_keycard_A(state) and not options.lock_key_amadeus or state.has('Lab Access Research', player)),
|
||||||
LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard bottom chest', 1337077),
|
LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard bottom chest', 1337077),
|
||||||
LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard floor secret', 1337078, lambda state: logic.has_upwarddash(state) and logic.can_break_walls(state)),
|
LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard floor secret', 1337078, lambda state: logic.has_upwarddash(state) and logic.can_break_walls(state)),
|
||||||
LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard upper chest', 1337079, lambda state: logic.has_upwarddash(state)),
|
LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard upper chest', 1337079, lambda state: logic.has_upwarddash(state)),
|
||||||
@@ -150,10 +150,10 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio
|
|||||||
LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 3', 1337118, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)),
|
LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 3', 1337118, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)),
|
||||||
LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 4', 1337119, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)),
|
LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 4', 1337119, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)),
|
||||||
LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Pedestal', 1337120, lambda state: not flooded.flood_maw or state.has('Water Mask', player)),
|
LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Pedestal', 1337120, lambda state: not flooded.flood_maw or state.has('Water Mask', player)),
|
||||||
LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Last chance before Maw', 1337121, lambda state: state.has('Water Mask', player) if flooded.flood_maw else logic.has_doublejump(state)),
|
LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Last chance before Maw', 1337121, lambda state: flooded.flood_maw or logic.has_doublejump(state)),
|
||||||
LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Plasma Crystal', 1337173, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (not flooded.flood_maw or state.has('Water Mask', player))),
|
LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Plasma Crystal', 1337173, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player)),
|
||||||
LocationData('Caves of Banishment (Maw)', 'Killed Maw', EventId, lambda state: state.has('Gas Mask', player) and (not flooded.flood_maw or state.has('Water Mask', player))),
|
LocationData('Caves of Banishment (Maw)', 'Killed Maw', EventId, lambda state: state.has('Gas Mask', player)),
|
||||||
LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Mineshaft', 1337122, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (not flooded.flood_maw or state.has('Water Mask', player))),
|
LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Mineshaft', 1337122, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player)),
|
||||||
LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Wyvern room', 1337123),
|
LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Wyvern room', 1337123),
|
||||||
LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room above water chest', 1337124),
|
LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room above water chest', 1337124),
|
||||||
LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room underwater left chest', 1337125, lambda state: state.has('Water Mask', player)),
|
LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room underwater left chest', 1337125, lambda state: state.has('Water Mask', player)),
|
||||||
@@ -251,7 +251,7 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio
|
|||||||
LocationData('Royal towers (upper)', 'Royal Towers: Journal - Top Struggle Juggle Base (War of the Sisters)', 1337195),
|
LocationData('Royal towers (upper)', 'Royal Towers: Journal - Top Struggle Juggle Base (War of the Sisters)', 1337195),
|
||||||
LocationData('Royal towers (upper)', 'Royal Towers: Journal - Aelana Boss (Stained Letter)', 1337196),
|
LocationData('Royal towers (upper)', 'Royal Towers: Journal - Aelana Boss (Stained Letter)', 1337196),
|
||||||
LocationData('Royal towers', 'Royal Towers: Journal - Near Bottom Struggle Juggle (Mission Findings)', 1337197, lambda state: flooded.flood_courtyard or logic.has_doublejump_of_npc(state)),
|
LocationData('Royal towers', 'Royal Towers: Journal - Near Bottom Struggle Juggle (Mission Findings)', 1337197, lambda state: flooded.flood_courtyard or logic.has_doublejump_of_npc(state)),
|
||||||
LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Journal - Lower Left Caves (Naivety)', 1337198, lambda state: not flooded.flood_maw or state.has('Water Mask', player))
|
LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Journal - Lower Left Caves (Naivety)', 1337198)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 1337199 - 1337232 Reserved for future use
|
# 1337199 - 1337232 Reserved for future use
|
||||||
|
|||||||
@@ -88,12 +88,15 @@ class PreCalculatedWeights:
|
|||||||
|
|
||||||
if options.risky_warps:
|
if options.risky_warps:
|
||||||
past_teleportation_gates.append("GateLakeSereneLeft")
|
past_teleportation_gates.append("GateLakeSereneLeft")
|
||||||
present_teleportation_gates.append("GateDadsTower")
|
|
||||||
if not is_xarion_flooded:
|
if not is_xarion_flooded:
|
||||||
present_teleportation_gates.append("GateXarion")
|
present_teleportation_gates.append("GateXarion")
|
||||||
if not is_lab_flooded:
|
# Prevent going past the lazers without a way to the past
|
||||||
present_teleportation_gates.append("GateLabEntrance")
|
if options.unchained_keys or options.prism_break or not options.pyramid_start:
|
||||||
|
present_teleportation_gates.append("GateDadsTower")
|
||||||
|
if not is_lab_flooded:
|
||||||
|
present_teleportation_gates.append("GateLabEntrance")
|
||||||
|
|
||||||
|
# Prevent getting stuck in the past without a way back to the future
|
||||||
if options.inverted or (options.pyramid_start and not options.back_to_the_future):
|
if options.inverted or (options.pyramid_start and not options.back_to_the_future):
|
||||||
all_gates: Tuple[str, ...] = present_teleportation_gates
|
all_gates: Tuple[str, ...] = present_teleportation_gates
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, options: Timesp
|
|||||||
connect(world, player, 'The lab', 'The lab (power off)', lambda state: options.lock_key_amadeus or logic.has_doublejump_of_npc(state))
|
connect(world, player, 'The lab', 'The lab (power off)', lambda state: options.lock_key_amadeus or logic.has_doublejump_of_npc(state))
|
||||||
connect(world, player, 'The lab (power off)', 'The lab', lambda state: not flooded.flood_lab or state.has('Water Mask', player))
|
connect(world, player, 'The lab (power off)', 'The lab', lambda state: not flooded.flood_lab or state.has('Water Mask', player))
|
||||||
connect(world, player, 'The lab (power off)', 'The lab (upper)', lambda state: logic.has_forwarddash_doublejump(state) and ((not options.lock_key_amadeus) or state.has('Lab Access Genza', player)))
|
connect(world, player, 'The lab (power off)', 'The lab (upper)', lambda state: logic.has_forwarddash_doublejump(state) and ((not options.lock_key_amadeus) or state.has('Lab Access Genza', player)))
|
||||||
connect(world, player, 'The lab (upper)', 'The lab (power off)')
|
connect(world, player, 'The lab (upper)', 'The lab (power off)', lambda state: options.lock_key_amadeus and state.has('Lab Access Genza', player))
|
||||||
connect(world, player, 'The lab (upper)', 'Emperors tower', logic.has_forwarddash_doublejump)
|
connect(world, player, 'The lab (upper)', 'Emperors tower', logic.has_forwarddash_doublejump)
|
||||||
connect(world, player, 'The lab (upper)', 'Ancient Pyramid (entrance)', lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player))
|
connect(world, player, 'The lab (upper)', 'Ancient Pyramid (entrance)', lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player))
|
||||||
connect(world, player, 'Emperors tower', 'The lab (upper)')
|
connect(world, player, 'Emperors tower', 'The lab (upper)')
|
||||||
@@ -141,7 +141,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, options: Timesp
|
|||||||
connect(world, player, 'Lower Lake Serene', 'Left Side forest Caves')
|
connect(world, player, 'Lower Lake Serene', 'Left Side forest Caves')
|
||||||
connect(world, player, 'Lower Lake Serene', 'Caves of Banishment (upper)', lambda state: flooded.flood_lake_serene or logic.has_doublejump(state))
|
connect(world, player, 'Lower Lake Serene', 'Caves of Banishment (upper)', lambda state: flooded.flood_lake_serene or logic.has_doublejump(state))
|
||||||
connect(world, player, 'Caves of Banishment (upper)', 'Lower Lake Serene', lambda state: not flooded.flood_lake_serene or state.has('Water Mask', player))
|
connect(world, player, 'Caves of Banishment (upper)', 'Lower Lake Serene', lambda state: not flooded.flood_lake_serene or state.has('Water Mask', player))
|
||||||
connect(world, player, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: logic.has_doublejump(state) or state.has_any({'Gas Mask', 'Talaria Attachment'} or logic.has_teleport(state), player))
|
connect(world, player, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: not flooded.flood_maw or state.has('Water Mask', player))
|
||||||
connect(world, player, 'Caves of Banishment (upper)', 'Space time continuum', logic.has_teleport)
|
connect(world, player, 'Caves of Banishment (upper)', 'Space time continuum', logic.has_teleport)
|
||||||
connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: logic.has_doublejump(state) if not flooded.flood_maw else state.has('Water Mask', player))
|
connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: logic.has_doublejump(state) if not flooded.flood_maw else state.has('Water Mask', player))
|
||||||
connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (Sirens)', lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) )
|
connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (Sirens)', lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) )
|
||||||
@@ -178,7 +178,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, options: Timesp
|
|||||||
connect(world, player, 'Space time continuum', 'Upper Lake Serene', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneLeft"))
|
connect(world, player, 'Space time continuum', 'Upper Lake Serene', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneLeft"))
|
||||||
connect(world, player, 'Space time continuum', 'Left Side forest Caves', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneRight"))
|
connect(world, player, 'Space time continuum', 'Left Side forest Caves', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneRight"))
|
||||||
connect(world, player, 'Space time continuum', 'Refugee Camp', lambda state: logic.can_teleport_to(state, "Past", "GateAccessToPast"))
|
connect(world, player, 'Space time continuum', 'Refugee Camp', lambda state: logic.can_teleport_to(state, "Past", "GateAccessToPast"))
|
||||||
connect(world, player, 'Space time continuum', 'Castle Ramparts', lambda state: logic.can_teleport_to(state, "Past", "GateCastleRamparts"))
|
connect(world, player, 'Space time continuum', 'Forest', lambda state: logic.can_teleport_to(state, "Past", "GateCastleRamparts"))
|
||||||
connect(world, player, 'Space time continuum', 'Castle Keep', lambda state: logic.can_teleport_to(state, "Past", "GateCastleKeep"))
|
connect(world, player, 'Space time continuum', 'Castle Keep', lambda state: logic.can_teleport_to(state, "Past", "GateCastleKeep"))
|
||||||
connect(world, player, 'Space time continuum', 'Royal towers (lower)', lambda state: logic.can_teleport_to(state, "Past", "GateRoyalTowers"))
|
connect(world, player, 'Space time continuum', 'Royal towers (lower)', lambda state: logic.can_teleport_to(state, "Past", "GateRoyalTowers"))
|
||||||
connect(world, player, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: logic.can_teleport_to(state, "Past", "GateMaw"))
|
connect(world, player, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: logic.can_teleport_to(state, "Past", "GateMaw"))
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class TimespinnerWorld(World):
|
|||||||
topology_present = True
|
topology_present = True
|
||||||
web = TimespinnerWebWorld()
|
web = TimespinnerWebWorld()
|
||||||
required_client_version = (0, 4, 2)
|
required_client_version = (0, 4, 2)
|
||||||
|
ut_can_gen_without_yaml = True
|
||||||
|
|
||||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||||
location_name_to_id = {location.name: location.code for location in get_location_datas(-1, None, None)}
|
location_name_to_id = {location.name: location.code for location in get_location_datas(-1, None, None)}
|
||||||
|
|||||||
@@ -56,18 +56,18 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||||||
for portal1, portal2 in portal_pairs.items():
|
for portal1, portal2 in portal_pairs.items():
|
||||||
if portal1.scene_destination() == portal_sd:
|
if portal1.scene_destination() == portal_sd:
|
||||||
return portal1.name, get_portal_outlet_region(portal2, world)
|
return portal1.name, get_portal_outlet_region(portal2, world)
|
||||||
if portal2.scene_destination() == portal_sd:
|
if portal2.scene_destination() == portal_sd and not (options.decoupled and options.entrance_rando):
|
||||||
return portal2.name, get_portal_outlet_region(portal1, world)
|
return portal2.name, get_portal_outlet_region(portal1, world)
|
||||||
raise Exception("No matches found in get_portal_info")
|
raise Exception(f"No matches found in get_portal_info for {portal_sd}")
|
||||||
|
|
||||||
# input scene destination tag, returns paired portal's name and region
|
# input scene destination tag, returns paired portal's name and region
|
||||||
def get_paired_portal(portal_sd: str) -> Tuple[str, str]:
|
def get_paired_portal(portal_sd: str) -> Tuple[str, str]:
|
||||||
for portal1, portal2 in portal_pairs.items():
|
for portal1, portal2 in portal_pairs.items():
|
||||||
if portal1.scene_destination() == portal_sd:
|
if portal1.scene_destination() == portal_sd:
|
||||||
return portal2.name, portal2.region
|
return portal2.name, portal2.region
|
||||||
if portal2.scene_destination() == portal_sd:
|
if portal2.scene_destination() == portal_sd and not (options.decoupled and options.entrance_rando):
|
||||||
return portal1.name, portal1.region
|
return portal1.name, portal1.region
|
||||||
raise Exception("no matches found in get_paired_portal")
|
raise Exception(f"No matches found in get_paired_portal for {portal_sd}")
|
||||||
|
|
||||||
regions["Menu"].connect(
|
regions["Menu"].connect(
|
||||||
connecting_region=regions["Overworld"])
|
connecting_region=regions["Overworld"])
|
||||||
|
|||||||
@@ -755,6 +755,53 @@ class TWWOptions(PerGameCommonOptions):
|
|||||||
remove_music: RemoveMusic
|
remove_music: RemoveMusic
|
||||||
death_link: DeathLink
|
death_link: DeathLink
|
||||||
|
|
||||||
|
def get_slot_data_dict(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Returns a dictionary of option name to value to be placed in
|
||||||
|
the slot data network package.
|
||||||
|
|
||||||
|
:return: Dictionary of option name to value for the slot data.
|
||||||
|
"""
|
||||||
|
return self.as_dict(
|
||||||
|
"progression_dungeons",
|
||||||
|
"progression_tingle_chests",
|
||||||
|
"progression_dungeon_secrets",
|
||||||
|
"progression_puzzle_secret_caves",
|
||||||
|
"progression_combat_secret_caves",
|
||||||
|
"progression_savage_labyrinth",
|
||||||
|
"progression_great_fairies",
|
||||||
|
"progression_short_sidequests",
|
||||||
|
"progression_long_sidequests",
|
||||||
|
"progression_spoils_trading",
|
||||||
|
"progression_minigames",
|
||||||
|
"progression_battlesquid",
|
||||||
|
"progression_free_gifts",
|
||||||
|
"progression_mail",
|
||||||
|
"progression_platforms_rafts",
|
||||||
|
"progression_submarines",
|
||||||
|
"progression_eye_reef_chests",
|
||||||
|
"progression_big_octos_gunboats",
|
||||||
|
"progression_triforce_charts",
|
||||||
|
"progression_treasure_charts",
|
||||||
|
"progression_expensive_purchases",
|
||||||
|
"progression_island_puzzles",
|
||||||
|
"progression_misc",
|
||||||
|
"sword_mode",
|
||||||
|
"required_bosses",
|
||||||
|
"logic_obscurity",
|
||||||
|
"logic_precision",
|
||||||
|
"enable_tuner_logic",
|
||||||
|
"randomize_dungeon_entrances",
|
||||||
|
"randomize_secret_cave_entrances",
|
||||||
|
"randomize_miniboss_entrances",
|
||||||
|
"randomize_boss_entrances",
|
||||||
|
"randomize_secret_cave_inner_entrances",
|
||||||
|
"randomize_fairy_fountain_entrances",
|
||||||
|
"swift_sail",
|
||||||
|
"skip_rematch_bosses",
|
||||||
|
"remove_music",
|
||||||
|
)
|
||||||
|
|
||||||
def get_output_dict(self) -> dict[str, Any]:
|
def get_output_dict(self) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Returns a dictionary of option name to value to be placed in
|
Returns a dictionary of option name to value to be placed in
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from BaseClasses import ItemClassification as IC
|
|||||||
from BaseClasses import MultiWorld, Region, Tutorial
|
from BaseClasses import MultiWorld, Region, Tutorial
|
||||||
from Options import Toggle
|
from Options import Toggle
|
||||||
from worlds.AutoWorld import WebWorld, World
|
from worlds.AutoWorld import WebWorld, World
|
||||||
from worlds.Files import APContainer, AutoPatchRegister
|
from worlds.Files import APPlayerContainer
|
||||||
from worlds.generic.Rules import add_item_rule
|
from worlds.generic.Rules import add_item_rule
|
||||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, icon_paths, launch_subprocess
|
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, icon_paths, launch_subprocess
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ components.append(
|
|||||||
icon_paths["The Wind Waker"] = "ap:worlds.tww/assets/icon.png"
|
icon_paths["The Wind Waker"] = "ap:worlds.tww/assets/icon.png"
|
||||||
|
|
||||||
|
|
||||||
class TWWContainer(APContainer, metaclass=AutoPatchRegister):
|
class TWWContainer(APPlayerContainer):
|
||||||
"""
|
"""
|
||||||
This class defines the container file for The Wind Waker.
|
This class defines the container file for The Wind Waker.
|
||||||
"""
|
"""
|
||||||
@@ -586,7 +586,7 @@ class TWWWorld(World):
|
|||||||
|
|
||||||
:return: A dictionary to be sent to the client when it connects to the server.
|
:return: A dictionary to be sent to the client when it connects to the server.
|
||||||
"""
|
"""
|
||||||
slot_data = self.options.as_dict(*self.options_dataclass.type_hints)
|
slot_data = self.options.get_slot_data_dict()
|
||||||
|
|
||||||
# Add entrances to `slot_data`. This is the same data that is written to the .aptww file.
|
# Add entrances to `slot_data`. This is the same data that is written to the .aptww file.
|
||||||
entrances = {
|
entrances = {
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ def get_pool_core(world: "TWWWorld") -> tuple[list[str], list[str]]:
|
|||||||
world.filler_pool = filler_pool
|
world.filler_pool = filler_pool
|
||||||
|
|
||||||
# Add filler items to place into excluded locations.
|
# Add filler items to place into excluded locations.
|
||||||
pool.extend([world.get_filler_item_name() for _ in world.options.exclude_locations])
|
excluded_locations = world.progress_locations.intersection(world.options.exclude_locations)
|
||||||
|
pool.extend([world.get_filler_item_name() for _ in excluded_locations])
|
||||||
|
|
||||||
# The remaining of items left to place should be the same as the number of non-excluded locations in the world.
|
# The remaining of items left to place should be the same as the number of non-excluded locations in the world.
|
||||||
nonexcluded_locations = [
|
nonexcluded_locations = [
|
||||||
|
|||||||
Reference in New Issue
Block a user