mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-11 18:13:48 -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:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
env:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
|
||||
7
Fill.py
7
Fill.py
@@ -937,13 +937,16 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
|
||||
|
||||
count = block.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):
|
||||
count = {"min": count, "max": count}
|
||||
if "min" not in count:
|
||||
count["min"] = 0
|
||||
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
|
||||
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 logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -41,13 +42,17 @@ def open_host_yaml():
|
||||
if is_linux:
|
||||
exe = which('sensible-editor') or which('gedit') or \
|
||||
which('xdg-open') or which('gnome-open') or which('kde-open')
|
||||
subprocess.Popen([exe, file])
|
||||
elif is_macos:
|
||||
exe = which("open")
|
||||
subprocess.Popen([exe, file])
|
||||
else:
|
||||
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():
|
||||
suffixes = []
|
||||
@@ -92,7 +97,11 @@ def open_folder(folder_path):
|
||||
return
|
||||
|
||||
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:
|
||||
logging.warning(f"No file browser available to open {folder_path}")
|
||||
|
||||
@@ -104,14 +113,21 @@ def update_settings():
|
||||
|
||||
components.extend([
|
||||
# Functions
|
||||
Component("Open host.yaml", func=open_host_yaml),
|
||||
Component("Open Patch", func=open_patch),
|
||||
Component("Generate Template Options", func=generate_yamls),
|
||||
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")),
|
||||
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
||||
Component("Open host.yaml", func=open_host_yaml,
|
||||
description="Open the host.yaml file to change settings for generation, games, and more."),
|
||||
Component("Open Patch", func=open_patch,
|
||||
description="Open a patch file, downloaded from the room page or provided by the host."),
|
||||
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",
|
||||
func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
||||
Component("Browse Files", func=browse_files),
|
||||
func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4"),
|
||||
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):
|
||||
if in_terminal:
|
||||
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
|
||||
elif is_linux:
|
||||
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)}")
|
||||
locations = item.get("locations", [])
|
||||
if not locations:
|
||||
locations = item.get("location", ["Everywhere"])
|
||||
locations = item.get("location", [])
|
||||
if locations:
|
||||
count = 1
|
||||
else:
|
||||
locations = ["Everywhere"]
|
||||
if isinstance(locations, str):
|
||||
locations = [locations]
|
||||
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
|
||||
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."
|
||||
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
|
||||
@@ -708,25 +713,30 @@ def _mp_open_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *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 = "") \
|
||||
-> typing.Optional[str]:
|
||||
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:
|
||||
# prefer native dialog
|
||||
from shutil import which
|
||||
kdialog = which("kdialog")
|
||||
if kdialog:
|
||||
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")
|
||||
if zenity:
|
||||
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
|
||||
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
|
||||
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 run(*args: str):
|
||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
||||
|
||||
if is_linux:
|
||||
# prefer native dialog
|
||||
from shutil import which
|
||||
kdialog = which("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 ".")
|
||||
zenity = which("zenity")
|
||||
if zenity:
|
||||
z_filters = ("--directory",)
|
||||
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
|
||||
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 run(*args: str):
|
||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
||||
|
||||
if is_kivy_running():
|
||||
from kvui import MessageBox
|
||||
MessageBox(title, text, error).open()
|
||||
@@ -814,10 +818,10 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||
from shutil import which
|
||||
kdialog = which("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")
|
||||
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:
|
||||
import ctypes
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
flask>=3.1.0
|
||||
flask>=3.1.1
|
||||
werkzeug>=3.1.3
|
||||
pony>=0.7.19
|
||||
waitress>=3.0.2
|
||||
|
||||
@@ -119,9 +119,9 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||
# AP Container
|
||||
elif handler:
|
||||
data = zfile.open(file, "r").read()
|
||||
patch = handler(BytesIO(data))
|
||||
patch.read()
|
||||
files[patch.player] = data
|
||||
with zipfile.ZipFile(BytesIO(data)) as container:
|
||||
player = json.loads(container.open("archipelago.json").read())["player"]
|
||||
files[player] = data
|
||||
|
||||
# Spoiler
|
||||
elif file.filename.endswith(".txt"):
|
||||
|
||||
@@ -365,18 +365,14 @@ request_handlers = {
|
||||
["PREFERRED_CORES"] = function (req)
|
||||
local res = {}
|
||||
local preferred_cores = client.getconfig().PreferredCores
|
||||
local systems_enumerator = preferred_cores.Keys:GetEnumerator()
|
||||
|
||||
res["type"] = "PREFERRED_CORES_RESPONSE"
|
||||
res["value"] = {}
|
||||
res["value"]["NES"] = preferred_cores.NES
|
||||
res["value"]["SNES"] = preferred_cores.SNES
|
||||
res["value"]["GB"] = preferred_cores.GB
|
||||
res["value"]["GBC"] = preferred_cores.GBC
|
||||
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
|
||||
|
||||
while systems_enumerator:MoveNext() do
|
||||
res["value"][systems_enumerator.Current] = preferred_cores[systems_enumerator.Current]
|
||||
end
|
||||
|
||||
return res
|
||||
end,
|
||||
|
||||
@@ -158,6 +158,7 @@ class APContainer:
|
||||
class APPlayerContainer(APContainer):
|
||||
"""A zipfile containing at least archipelago.json meant for a player"""
|
||||
game: ClassVar[Optional[str]] = None
|
||||
patch_file_ending: str = ""
|
||||
|
||||
player: Optional[int]
|
||||
player_name: str
|
||||
@@ -184,6 +185,7 @@ class APPlayerContainer(APContainer):
|
||||
"player": self.player,
|
||||
"player_name": self.player_name,
|
||||
"game": self.game,
|
||||
"patch_file_ending": self.patch_file_ending,
|
||||
})
|
||||
return manifest
|
||||
|
||||
@@ -223,7 +225,6 @@ class APProcedurePatch(APAutoPatchInterface):
|
||||
"""
|
||||
hash: Optional[str] # base checksum of source file
|
||||
source_data: bytes
|
||||
patch_file_ending: str = ""
|
||||
files: Dict[str, bytes]
|
||||
|
||||
@classmethod
|
||||
@@ -245,7 +246,6 @@ class APProcedurePatch(APAutoPatchInterface):
|
||||
manifest = super(APProcedurePatch, self).get_manifest()
|
||||
manifest["base_checksum"] = self.hash
|
||||
manifest["result_file_ending"] = self.result_file_ending
|
||||
manifest["patch_file_ending"] = self.patch_file_ending
|
||||
manifest["procedure"] = self.procedure
|
||||
if self.procedure == APDeltaPatch.procedure:
|
||||
manifest["compatible_version"] = 5
|
||||
|
||||
@@ -210,10 +210,14 @@ components: List[Component] = [
|
||||
Component('Launcher', 'Launcher', component_type=Type.HIDDEN),
|
||||
# Core
|
||||
Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True,
|
||||
file_identifier=SuffixIdentifier('.archipelago', '.zip')),
|
||||
Component('Generate', 'Generate', cli=True),
|
||||
Component("Install APWorld", func=install_apworld, file_identifier=SuffixIdentifier(".apworld")),
|
||||
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient),
|
||||
file_identifier=SuffixIdentifier('.archipelago', '.zip'),
|
||||
description="Host a generated multiworld on your computer."),
|
||||
Component('Generate', 'Generate', cli=True,
|
||||
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',
|
||||
file_identifier=SuffixIdentifier('.apladx')),
|
||||
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,
|
||||
file_identifier=SuffixIdentifier())
|
||||
file_identifier=SuffixIdentifier(),
|
||||
description="Open the BizHawk client, to play games using the Bizhawk emulator.")
|
||||
components.append(component)
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
import io
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, cast
|
||||
import zipfile
|
||||
from BaseClasses import Location
|
||||
from worlds.Files import APContainer, AutoPatchRegister
|
||||
from worlds.Files import APPlayerContainer
|
||||
|
||||
from .Enum import CivVICheckType
|
||||
from .Locations import CivVILocation, CivVILocationData
|
||||
@@ -26,22 +25,19 @@ class CivTreeItem:
|
||||
ui_tree_row: int
|
||||
|
||||
|
||||
class CivVIContainer(APContainer, metaclass=AutoPatchRegister):
|
||||
class CivVIContainer(APPlayerContainer):
|
||||
"""
|
||||
Responsible for generating the dynamic mod files for the Civ VI multiworld
|
||||
"""
|
||||
game: Optional[str] = "Civilization VI"
|
||||
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 = ""):
|
||||
if isinstance(patch_data, io.BytesIO):
|
||||
super().__init__(patch_data, player, player_name, server)
|
||||
else:
|
||||
self.patch_data = patch_data
|
||||
self.file_path = base_path
|
||||
container_path = os.path.join(output_directory, base_path + ".apcivvi")
|
||||
super().__init__(container_path, player, player_name, server)
|
||||
self.patch_data = patch_data
|
||||
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:
|
||||
for filename, yml in self.patch_data.items():
|
||||
|
||||
@@ -2893,3 +2893,18 @@ dog_bite_ice_trap_fix = [
|
||||
0x25291CB8, # ADDIU T1, T1, 0x1CB8
|
||||
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):
|
||||
"""
|
||||
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"
|
||||
|
||||
|
||||
@@ -607,9 +607,10 @@ class CV64PatchExtensions(APPatchExtension):
|
||||
rom_data.write_int32(0xAA530, 0x080FF880) # J 0x803FE200
|
||||
rom_data.write_int32s(0xBFE200, patches.coffin_cutscene_skipper)
|
||||
|
||||
# Increase shimmy speed
|
||||
# Shimmy speed increase hack
|
||||
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
|
||||
if options["fall_guard"]:
|
||||
|
||||
@@ -211,7 +211,8 @@ class CVCotMWorld(World):
|
||||
"ignore_cleansing": self.options.ignore_cleansing.value,
|
||||
"skip_tutorials": self.options.skip_tutorials.value,
|
||||
"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:
|
||||
return self.random.choice(FILLER_ITEM_NAMES)
|
||||
|
||||
@@ -48,11 +48,17 @@ class OtherGameAppearancesInfo(TypedDict):
|
||||
|
||||
|
||||
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,
|
||||
"appearance": 0x01},
|
||||
"Heart Vessel": {"type": 0xE4,
|
||||
"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,
|
||||
"appearance": 0x01},
|
||||
"Max Aura": {"type": 0xE4,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Quick Links
|
||||
- [Setup](/tutorial/Castlevania%20-%20Circle%20of%20the%20Moon/setup/en)
|
||||
- [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)
|
||||
- [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)
|
||||
|
||||
@@ -22,7 +22,7 @@ clear it.
|
||||
|
||||
## 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).
|
||||
|
||||
## 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.
|
||||
|
||||
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).
|
||||
2. Put the tracker pack into `packs/` in your PopTracker install.
|
||||
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: Titanite Shard - woods, surrounded by enemies", "Titanite Shard"),
|
||||
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",
|
||||
hidden=True), # Hidden fall
|
||||
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",
|
||||
"Twinkling Titanite", lizard=True),
|
||||
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",
|
||||
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"):
|
||||
self._add_item_rule(
|
||||
"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
|
||||
|
||||
@@ -802,8 +802,10 @@ def connect_regions(world: World, level_list):
|
||||
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[-1],
|
||||
lambda state: (state.can_reach(LocationName.riverside_race_flag, "Location", world.player)))
|
||||
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)))
|
||||
world.multiworld.register_indirect_condition(world.get_location(LocationName.riverside_race_flag).parent_region,
|
||||
connection)
|
||||
|
||||
# Cotton-Top Cove Connections
|
||||
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,
|
||||
lambda state: (state.has(ItemName.bowling_ball, world.player, 1)))
|
||||
else:
|
||||
connect(world, world.player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
|
||||
lambda state: (state.can_reach(LocationName.bleaks_house, "Location", world.player)))
|
||||
connection = connect(world, world.player, names, LocationName.mekanos_region,
|
||||
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_levels = [
|
||||
@@ -946,3 +951,4 @@ def connect(world: World, player: int, used_names: typing.Dict[str, int], source
|
||||
|
||||
source_region.exits.append(connection)
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
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))
|
||||
set_rule(world.get_entrance("Behind Rocks", player),
|
||||
set_rule(world.get_entrance("Behind Rocks"),
|
||||
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
|
||||
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):
|
||||
|
||||
@@ -69,7 +69,9 @@ class FactorioContext(CommonContext):
|
||||
# updated by spinup server
|
||||
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)
|
||||
self.send_index: int = 0
|
||||
self.rcon_client = None
|
||||
@@ -82,6 +84,10 @@ class FactorioContext(CommonContext):
|
||||
self.filter_item_sends: bool = filter_item_sends
|
||||
self.multiplayer: bool = False # whether multiple different players have connected
|
||||
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
|
||||
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] "
|
||||
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
|
||||
def energy_link_status(self) -> str:
|
||||
if not self.energy_link_increment:
|
||||
@@ -311,7 +329,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
executable, "--create", savegame_name, "--preset", "archipelago"
|
||||
))
|
||||
factorio_process = subprocess.Popen((executable, "--start-server", savegame_name,
|
||||
*(str(elem) for elem in server_args)),
|
||||
*ctx.server_args),
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stdin=subprocess.DEVNULL,
|
||||
@@ -331,7 +349,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
factorio_queue.task_done()
|
||||
|
||||
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)
|
||||
if not ctx.server:
|
||||
logger.info("Established bridge to Factorio Server. "
|
||||
@@ -422,7 +440,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
||||
executable, "--create", savegame_name
|
||||
))
|
||||
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,
|
||||
stdout=subprocess.PIPE,
|
||||
stdin=subprocess.DEVNULL,
|
||||
@@ -451,7 +469,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
||||
"or a Factorio sharing data directories is already running. "
|
||||
"Server could not start up.")
|
||||
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:
|
||||
raise Exception("No Archipelago mod was loaded. Aborting.")
|
||||
await get_info(ctx, rcon_client)
|
||||
@@ -474,9 +492,8 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
async def main(args, filter_item_sends: bool, filter_bridge_chat_out: bool):
|
||||
ctx = FactorioContext(args.connect, args.password, filter_item_sends, filter_bridge_chat_out)
|
||||
|
||||
async def main(make_context):
|
||||
ctx = make_context()
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
|
||||
if gui_enabled:
|
||||
@@ -509,38 +526,42 @@ class FactorioJSONtoTextParser(JSONtoTextParser):
|
||||
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")
|
||||
settings: FactorioSettings = get_settings().factorio_options
|
||||
if os.path.samefile(settings.executable, sys.executable):
|
||||
selected_executable = settings.executable
|
||||
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
|
||||
|
||||
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():
|
||||
def launch(*new_args: str):
|
||||
import colorama
|
||||
global executable, server_settings, server_args
|
||||
global executable
|
||||
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:
|
||||
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_bridge_chat_out = bool(settings.bridge_chat_out)
|
||||
|
||||
@@ -554,14 +575,9 @@ def launch():
|
||||
else:
|
||||
raise FileNotFoundError(f"Path {executable} is not an executable file.")
|
||||
|
||||
if server_settings and os.path.isfile(server_settings):
|
||||
server_args = (
|
||||
"--rcon-port", rcon_port,
|
||||
"--rcon-password", rcon_password,
|
||||
"--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))
|
||||
asyncio.run(main(lambda: FactorioContext(
|
||||
args.connect, args.password,
|
||||
initial_filter_item_sends, initial_bridge_chat_out,
|
||||
rcon_port, rcon_password, server_settings, rest
|
||||
)))
|
||||
colorama.deinit()
|
||||
|
||||
@@ -67,6 +67,7 @@ class FactorioModFile(worlds.Files.APPlayerContainer):
|
||||
game = "Factorio"
|
||||
compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives
|
||||
writing_tasks: List[Callable[[], Tuple[str, Union[str, bytes]]]]
|
||||
patch_file_ending = ".zip"
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -22,9 +22,9 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
|
||||
from .settings import FactorioSettings
|
||||
|
||||
|
||||
def launch_client():
|
||||
def launch_client(*args: str):
|
||||
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))
|
||||
|
||||
@@ -34,9 +34,9 @@ from .locations import (JakAndDaxterLocation,
|
||||
cache_location_table,
|
||||
orb_location_table)
|
||||
from .regions import create_regions
|
||||
from .rules import (enforce_multiplayer_limits,
|
||||
enforce_singleplayer_limits,
|
||||
verify_orb_trade_amounts,
|
||||
from .rules import (enforce_mp_absolute_limits,
|
||||
enforce_mp_friendly_limits,
|
||||
enforce_sp_limits,
|
||||
set_orb_trade_rule)
|
||||
from .locs import (cell_locations as cells,
|
||||
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.lava_tube_cell_count.value = self.power_cell_thresholds[2]
|
||||
|
||||
# Store this for remove function.
|
||||
self.power_cell_thresholds_minus_one = [x - 1 for x in self.power_cell_thresholds]
|
||||
|
||||
# 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.
|
||||
# We would have done this earlier, but we needed to sort the power cell thresholds first. Don't worry, we'll
|
||||
# come back to them.
|
||||
enforce_friendly_options = self.settings.enforce_friendly_options
|
||||
if enforce_friendly_options:
|
||||
if self.multiworld.players > 1:
|
||||
enforce_multiplayer_limits(self)
|
||||
if self.multiworld.players == 1:
|
||||
# For singleplayer games, always enforce/clamp the cell counts to valid values.
|
||||
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:
|
||||
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,
|
||||
# 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.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.
|
||||
if self.options.enable_orbsanity == options.EnableOrbsanity.option_per_level:
|
||||
self.orb_bundle_size = self.options.level_orbsanity_bundle_size.value
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
- [What do Traps do?](#what-do-traps-do)
|
||||
- [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)
|
||||
- [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 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)
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
editing the `host.yaml`. In the Archipelago Launcher, click `Open host.yaml`, then search for `jakanddaxter_options`,
|
||||
then search for `enforce_friendly_options`, then change this value from `true` to `false`. Disabling this allows for
|
||||
more disruptive and challenging options, but it may cause seed generation to fail. **Use at your own risk!**
|
||||
You can override **some**, not all, of those limits by editing the `host.yaml`. In the Archipelago Launcher, click
|
||||
`Open host.yaml`, then search for `jakanddaxter_options`, then search for `enforce_friendly_options`, then change this
|
||||
value from `true` to `false`. You can then generate a seed locally, and upload that to the Archipelago website to host
|
||||
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
|
||||
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.*
|
||||
- [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.
|
||||
|
||||
@@ -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
|
||||
|
||||
- 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:
|
||||
- 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.
|
||||
|
||||
@@ -1,22 +1,78 @@
|
||||
from dataclasses import dataclass
|
||||
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
|
||||
|
||||
|
||||
class StaticGetter:
|
||||
def __init__(self, func):
|
||||
self.fget = func
|
||||
class readonly_classproperty:
|
||||
"""This decorator is used for getting friendly or unfriendly range_end values for options like FireCanyonCellCount
|
||||
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):
|
||||
return self.fget(owner)
|
||||
return self.getter(owner)
|
||||
|
||||
|
||||
@StaticGetter
|
||||
@readonly_classproperty
|
||||
def determine_range_end(cls) -> int:
|
||||
from . import JakAndDaxterWorld
|
||||
enforce_friendly_options = JakAndDaxterWorld.settings.enforce_friendly_options
|
||||
return cls.friendly_maximum if enforce_friendly_options else cls.absolute_maximum
|
||||
from . import JakAndDaxterWorld # Avoid circular imports.
|
||||
friendly = JakAndDaxterWorld.settings.enforce_friendly_options
|
||||
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):
|
||||
@@ -44,12 +100,13 @@ class EnableOrbsanity(Choice):
|
||||
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."
|
||||
There are 2000 orbs in the game, so your bundle size must be a factor of 2000.
|
||||
|
||||
Multiplayer Minimum: 10
|
||||
Multiplayer Maximum: 200"""
|
||||
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 = "Global Orbsanity Bundle Size"
|
||||
option_1_orb = 1
|
||||
option_2_orbs = 2
|
||||
@@ -75,12 +132,33 @@ class GlobalOrbsanityBundleSize(Choice):
|
||||
friendly_maximum = 200
|
||||
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."
|
||||
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"
|
||||
option_1_orb = 1
|
||||
option_2_orbs = 2
|
||||
@@ -91,6 +169,18 @@ class PerLevelOrbsanityBundleSize(Choice):
|
||||
friendly_minimum = 10
|
||||
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):
|
||||
"""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_mountain_pass = 87
|
||||
option_cross_lava_tube = 89
|
||||
option_defeat_dark_eco_plant = 6
|
||||
# option_defeat_dark_eco_plant = 6
|
||||
option_defeat_klaww = 86
|
||||
option_defeat_gol_and_maia = 112
|
||||
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:
|
||||
multiworld.completion_condition[player] = lambda state: state.can_reach(gmc, "Region", player)
|
||||
|
||||
elif options.jak_completion_condition == CompletionCondition.option_defeat_dark_eco_plant:
|
||||
multiworld.completion_condition[player] = lambda state: state.can_reach(fjp, "Region", player)
|
||||
# elif options.jak_completion_condition == CompletionCondition.option_defeat_dark_eco_plant:
|
||||
# multiworld.completion_condition[player] = lambda state: state.can_reach(fjp, "Region", player)
|
||||
|
||||
elif options.jak_completion_condition == CompletionCondition.option_defeat_klaww:
|
||||
multiworld.completion_condition[player] = lambda state: state.can_reach(mp, "Region", player)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
import math
|
||||
import typing
|
||||
from BaseClasses import CollectionState
|
||||
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)
|
||||
|
||||
|
||||
def enforce_multiplayer_limits(world: "JakAndDaxterWorld"):
|
||||
def clamp_cell_limits(world: "JakAndDaxterWorld") -> str:
|
||||
options = world.options
|
||||
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:
|
||||
old_value = options.fire_canyon_cell_count.value
|
||||
options.fire_canyon_cell_count.value = FireCanyonCellCount.friendly_maximum
|
||||
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")
|
||||
f"{FireCanyonCellCount.friendly_maximum} (was {old_value}), "
|
||||
f"changed option to appropriate value.\n")
|
||||
|
||||
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" "
|
||||
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")
|
||||
f"{MountainPassCellCount.friendly_maximum} (was {old_value}), "
|
||||
f"changed option to appropriate value.\n")
|
||||
|
||||
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" "
|
||||
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")
|
||||
f"{LavaTubeCellCount.friendly_maximum} (was {old_value}), "
|
||||
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:
|
||||
old_value = options.citizen_orb_trade_amount.value
|
||||
options.citizen_orb_trade_amount.value = CitizenOrbTradeAmount.friendly_maximum
|
||||
friendly_message += (f" "
|
||||
f"{options.citizen_orb_trade_amount.display_name} must be no greater than "
|
||||
f"{CitizenOrbTradeAmount.friendly_maximum} (currently "
|
||||
f"{options.citizen_orb_trade_amount.value}).\n")
|
||||
f"{CitizenOrbTradeAmount.friendly_maximum} (was {old_value}), "
|
||||
f"changed option to appropriate value.\n")
|
||||
|
||||
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" "
|
||||
f"{options.oracle_orb_trade_amount.display_name} must be no greater than "
|
||||
f"{OracleOrbTradeAmount.friendly_maximum} (currently "
|
||||
f"{options.oracle_orb_trade_amount.value}).\n")
|
||||
f"{OracleOrbTradeAmount.friendly_maximum} (was {old_value}), "
|
||||
f"changed option to appropriate value.\n")
|
||||
|
||||
friendly_message += clamp_cell_limits(world)
|
||||
friendly_message += clamp_trade_total_limits(world)
|
||||
|
||||
if friendly_message != "":
|
||||
raise OptionError(f"{world.player_name}: The options you have chosen may disrupt the multiworld. \n"
|
||||
f"Please adjust the following Options for a multiplayer game. \n"
|
||||
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 the seed generator's host.yaml to false. "
|
||||
f"(Use at your own risk!)")
|
||||
logging.warning(f"{world.player_name}: Your options have been modified to avoid disrupting the multiworld.\n"
|
||||
f"{friendly_message}"
|
||||
f"You can access more advanced options by setting 'enforce_friendly_options' in the seed "
|
||||
f"generator's host.yaml to false and generating locally. (Use at your own risk!)")
|
||||
|
||||
|
||||
def enforce_singleplayer_limits(world: "JakAndDaxterWorld"):
|
||||
options = world.options
|
||||
def enforce_mp_absolute_limits(world: "JakAndDaxterWorld"):
|
||||
friendly_message = ""
|
||||
|
||||
if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum:
|
||||
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")
|
||||
friendly_message += clamp_trade_total_limits(world)
|
||||
|
||||
if friendly_message != "":
|
||||
raise OptionError(f"The options you have chosen may result in seed generation failures. \n"
|
||||
f"Please adjust the following Options for a singleplayer game. \n"
|
||||
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!)")
|
||||
logging.warning(f"{world.player_name}: Your options have been modified to avoid seed generation failures.\n"
|
||||
f"{friendly_message}")
|
||||
|
||||
|
||||
def verify_orb_trade_amounts(world: "JakAndDaxterWorld"):
|
||||
def enforce_sp_limits(world: "JakAndDaxterWorld"):
|
||||
friendly_message = ""
|
||||
|
||||
if world.total_trade_orbs > 2000:
|
||||
raise OptionError(f"{world.player_name}: Required number of orbs for all trades ({world.total_trade_orbs}) "
|
||||
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} "
|
||||
f"or {world.options.oracle_orb_trade_amount.display_name}.")
|
||||
friendly_message += clamp_cell_limits(world)
|
||||
friendly_message += clamp_trade_total_limits(world)
|
||||
|
||||
if friendly_message != "":
|
||||
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):
|
||||
options = {
|
||||
"enable_orbsanity": 2,
|
||||
"global_orbsanity_bundle_size": 5,
|
||||
"global_orbsanity_bundle_size": 10,
|
||||
"citizen_orb_trade_amount": 0,
|
||||
"oracle_orb_trade_amount": 0
|
||||
}
|
||||
|
||||
def test_orb_items_are_filler(self):
|
||||
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):
|
||||
self.assertTrue(self.multiworld
|
||||
@@ -22,15 +22,15 @@ class TradesCostNothingTest(JakAndDaxterTestBase):
|
||||
class TradesCostEverythingTest(JakAndDaxterTestBase):
|
||||
options = {
|
||||
"enable_orbsanity": 2,
|
||||
"global_orbsanity_bundle_size": 5,
|
||||
"global_orbsanity_bundle_size": 10,
|
||||
"citizen_orb_trade_amount": 120,
|
||||
"oracle_orb_trade_amount": 150
|
||||
}
|
||||
|
||||
def test_orb_items_are_progression(self):
|
||||
self.collect_all_but("")
|
||||
self.assertIn("5 Precursor Orbs", self.multiworld.state.prog_items[self.player])
|
||||
self.assertEqual(396, self.multiworld.state.prog_items[self.player]["5 Precursor Orbs"])
|
||||
self.assertIn("10 Precursor Orbs", self.multiworld.state.prog_items[self.player])
|
||||
self.assertEqual(198, self.multiworld.state.prog_items[self.player]["10 Precursor Orbs"])
|
||||
|
||||
def test_trades_are_accessible(self):
|
||||
self.collect_all_but("")
|
||||
|
||||
@@ -34,7 +34,7 @@ class KH2Context(CommonContext):
|
||||
self.growthlevel = None
|
||||
self.kh2connected = 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.location_name_to_data = {name: data for name, data, in all_locations.items()}
|
||||
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.sending = []
|
||||
self.slot_name = None
|
||||
self.disconnect_from_server = False
|
||||
# list used to keep track of locations+items player has. Used for disoneccting
|
||||
self.kh2_seed_save_cache = {
|
||||
"itemIndex": -1,
|
||||
@@ -185,11 +187,20 @@ class KH2Context(CommonContext):
|
||||
if password_requested and not self.password:
|
||||
await super(KH2Context, self).server_auth(password_requested)
|
||||
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):
|
||||
self.kh2connected = False
|
||||
self.serverconneced = False
|
||||
self.serverconnected = False
|
||||
if self.kh2seedname is not None and self.auth is not None:
|
||||
with open(self.kh2_seed_save_path_join, 'w') as f:
|
||||
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):
|
||||
self.kh2connected = False
|
||||
self.serverconneced = False
|
||||
self.serverconnected = False
|
||||
self.locations_checked = []
|
||||
if self.kh2seedname not in {None} and self.auth not in {None}:
|
||||
with open(self.kh2_seed_save_path_join, 'w') as f:
|
||||
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):
|
||||
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_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
|
||||
for item in args['items']:
|
||||
asyncio.create_task(self.give_item(item.item, item.location))
|
||||
@@ -370,12 +390,14 @@ class KH2Context(CommonContext):
|
||||
if not self.kh2:
|
||||
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
self.get_addresses()
|
||||
|
||||
#
|
||||
except Exception as e:
|
||||
if self.kh2connected:
|
||||
self.kh2connected = False
|
||||
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):
|
||||
self.kh2_loc_name_to_id = loc_to_id
|
||||
@@ -493,23 +515,38 @@ class KH2Context(CommonContext):
|
||||
|
||||
async def give_item(self, item, location):
|
||||
try:
|
||||
# todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites
|
||||
#sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts
|
||||
# sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts
|
||||
while not self.lookup_id_to_item:
|
||||
await asyncio.sleep(0.5)
|
||||
itemname = self.lookup_id_to_item[item]
|
||||
itemdata = self.item_name_to_data[itemname]
|
||||
# itemcode = self.kh2_item_name_to_id[itemname]
|
||||
if itemdata.ability:
|
||||
if location in self.all_weapon_location_id:
|
||||
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"}:
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Growth"][itemname] += 1
|
||||
return
|
||||
|
||||
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]:
|
||||
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]) < \
|
||||
self.AbilityQuantityDict[itemname]:
|
||||
if itemname in self.sora_ability_set:
|
||||
@@ -528,18 +565,21 @@ class KH2Context(CommonContext):
|
||||
if ability_slot in self.front_ability_slots:
|
||||
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}:
|
||||
# if memaddr is in a bitmask location in memory
|
||||
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]:
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Bitmask"].append(itemname)
|
||||
|
||||
# if itemdata in {magic}
|
||||
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
|
||||
|
||||
# equipment is a list instead of dict because you can only have 1 currently
|
||||
elif itemname in self.all_equipment:
|
||||
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:
|
||||
if itemname in self.keyblade_set:
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Sora"].append(itemname)
|
||||
@@ -548,9 +588,11 @@ class KH2Context(CommonContext):
|
||||
else:
|
||||
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:
|
||||
self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][itemname] += 1
|
||||
else:
|
||||
# "normal" items. They have a unique byte reserved for how many they have
|
||||
if itemname in self.kh2_seed_save_cache["AmountInvo"]["Amount"]:
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Amount"][itemname] += 1
|
||||
else:
|
||||
@@ -930,7 +972,7 @@ def finishedGame(ctx: KH2Context):
|
||||
async def kh2_watcher(ctx: KH2Context):
|
||||
while not ctx.exit_event.is_set():
|
||||
try:
|
||||
if ctx.kh2connected and ctx.serverconneced:
|
||||
if ctx.kh2connected and ctx.serverconnected:
|
||||
ctx.sending = []
|
||||
await asyncio.create_task(ctx.checkWorldLocations())
|
||||
await asyncio.create_task(ctx.checkLevels())
|
||||
@@ -944,13 +986,19 @@ async def kh2_watcher(ctx: KH2Context):
|
||||
if ctx.sending:
|
||||
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
|
||||
await ctx.send_msgs(message)
|
||||
elif not ctx.kh2connected and ctx.serverconneced:
|
||||
logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.")
|
||||
elif not ctx.kh2connected and ctx.serverconnected:
|
||||
logger.info("Game Connection lost. trying to reconnect.")
|
||||
ctx.kh2 = None
|
||||
while not ctx.kh2connected and ctx.serverconneced:
|
||||
await asyncio.sleep(15)
|
||||
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
ctx.get_addresses()
|
||||
while not ctx.kh2connected and ctx.serverconnected:
|
||||
try:
|
||||
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
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:
|
||||
if ctx.kh2connected:
|
||||
ctx.kh2connected = False
|
||||
|
||||
@@ -13,6 +13,7 @@ from worlds.Files import APPlayerContainer
|
||||
|
||||
class KH2Container(APPlayerContainer):
|
||||
game: str = 'Kingdom Hearts 2'
|
||||
patch_file_ending = ".zip"
|
||||
|
||||
def __init__(self, patch_data: dict, base_path: str, output_directory: str,
|
||||
player=None, player_name: str = "", server: str = ""):
|
||||
|
||||
@@ -277,9 +277,7 @@ class KH2World(World):
|
||||
if self.options.FillerItemsLocal:
|
||||
for item in filler_items:
|
||||
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:
|
||||
self.total_locations -= 6
|
||||
|
||||
@@ -400,6 +398,8 @@ class KH2World(World):
|
||||
# plando goofy get bonuses
|
||||
goofy_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in
|
||||
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:
|
||||
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)
|
||||
location.place_locked_item(random_ability)
|
||||
self.donald_weapon_abilities.remove(random_ability)
|
||||
|
||||
# if option is turned off
|
||||
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_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:
|
||||
random_ability = self.random.choice(self.donald_get_bonus_abilities)
|
||||
location.place_locked_item(random_ability)
|
||||
|
||||
@@ -4956,10 +4956,16 @@
|
||||
Outside The Initiated:
|
||||
room: Art Gallery
|
||||
door: Exit
|
||||
The Bearer (East): True
|
||||
The Bearer (North): True
|
||||
The Bearer (South): True
|
||||
The Bearer (West): True
|
||||
The Bearer (East):
|
||||
static_painting: True
|
||||
The Bearer (North):
|
||||
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
|
||||
panels:
|
||||
Achievement:
|
||||
@@ -5053,7 +5059,8 @@
|
||||
- MIDDLE
|
||||
The Bearer (East):
|
||||
entrances:
|
||||
Cross Tower (East): True
|
||||
Cross Tower (East):
|
||||
static_painting: True
|
||||
Bearer Side Area:
|
||||
door: Side Area Access
|
||||
Roof: True
|
||||
@@ -5084,7 +5091,8 @@
|
||||
panel: SPACE
|
||||
The Bearer (North):
|
||||
entrances:
|
||||
Cross Tower (East): True
|
||||
Cross Tower (North):
|
||||
static_painting: True
|
||||
Roof: True
|
||||
panels:
|
||||
SILENT (1):
|
||||
@@ -5128,7 +5136,8 @@
|
||||
panel: POTS
|
||||
The Bearer (South):
|
||||
entrances:
|
||||
Cross Tower (North): True
|
||||
Cross Tower (South):
|
||||
static_painting: True
|
||||
Bearer Side Area:
|
||||
door: Side Area Shortcut
|
||||
Roof: True
|
||||
@@ -5162,7 +5171,10 @@
|
||||
panel: SILENT (1)
|
||||
The Bearer (West):
|
||||
entrances:
|
||||
Cross Tower (West): True
|
||||
Cross Tower (West):
|
||||
static_painting: True
|
||||
The Bearer:
|
||||
door: Side Area Shortcut
|
||||
Bearer Side Area:
|
||||
door: Side Area Shortcut
|
||||
Roof: True
|
||||
@@ -5235,6 +5247,7 @@
|
||||
The Bearer:
|
||||
room: The Bearer
|
||||
door: East Entrance
|
||||
static_painting: True
|
||||
Roof: True
|
||||
panels:
|
||||
WINTER:
|
||||
@@ -5250,6 +5263,7 @@
|
||||
The Bearer (East):
|
||||
room: The Bearer (East)
|
||||
door: North Entrance
|
||||
static_painting: True
|
||||
Roof: True
|
||||
panels:
|
||||
NORTH:
|
||||
@@ -5270,6 +5284,7 @@
|
||||
The Bearer (North):
|
||||
room: The Bearer (North)
|
||||
door: South Entrance
|
||||
static_painting: True
|
||||
panels:
|
||||
FIRE:
|
||||
id: Cross Room/Panel_fire_fire
|
||||
@@ -5284,6 +5299,7 @@
|
||||
Bearer Side Area:
|
||||
room: Bearer Side Area
|
||||
door: West Entrance
|
||||
static_painting: True
|
||||
Roof: True
|
||||
panels:
|
||||
DIAMONDS:
|
||||
@@ -7108,6 +7124,8 @@
|
||||
entrances:
|
||||
Orange Tower Third Floor:
|
||||
warp: True
|
||||
Art Gallery (First Floor):
|
||||
warp: True
|
||||
Art Gallery (Second Floor):
|
||||
warp: True
|
||||
Art Gallery (Third Floor):
|
||||
@@ -7125,22 +7143,6 @@
|
||||
required_door:
|
||||
room: Number Hunt
|
||||
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:
|
||||
id: Painting Room/Panel_order_onepathmanyturns
|
||||
tag: forbid
|
||||
@@ -7159,15 +7161,8 @@
|
||||
- scenery_painting_2c
|
||||
skip_location: True
|
||||
panels:
|
||||
- EON
|
||||
First Floor Puzzles:
|
||||
skip_item: True
|
||||
location_name: Art Gallery - First Floor Puzzles
|
||||
panels:
|
||||
- EON
|
||||
- TRUSTWORTHY
|
||||
- FREE
|
||||
- OUR
|
||||
- room: Art Gallery (First Floor)
|
||||
panel: EON
|
||||
Third Floor:
|
||||
painting_id:
|
||||
- scenery_painting_3b
|
||||
@@ -7227,11 +7222,42 @@
|
||||
- Third Floor
|
||||
- Fourth 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):
|
||||
entrances:
|
||||
Art Gallery:
|
||||
room: Art Gallery
|
||||
door: Second Floor
|
||||
static_painting: True
|
||||
panels:
|
||||
HOUSE:
|
||||
id: Painting Room/Panel_house_neighborhood
|
||||
@@ -7263,6 +7289,7 @@
|
||||
Art Gallery:
|
||||
room: Art Gallery
|
||||
door: Third Floor
|
||||
static_painting: True
|
||||
panels:
|
||||
AN:
|
||||
id: Painting Room/Panel_an_many
|
||||
@@ -7294,6 +7321,7 @@
|
||||
Art Gallery:
|
||||
room: Art Gallery
|
||||
door: Fourth Floor
|
||||
static_painting: True
|
||||
panels:
|
||||
URNS:
|
||||
id: Painting Room/Panel_urns_turns
|
||||
|
||||
Binary file not shown.
@@ -727,11 +727,12 @@ panels:
|
||||
WANDER: 444975
|
||||
Art Gallery:
|
||||
EIGHT: 444976
|
||||
ORDER: 444981
|
||||
Art Gallery (First Floor):
|
||||
EON: 444977
|
||||
TRUSTWORTHY: 444978
|
||||
FREE: 444979
|
||||
OUR: 444980
|
||||
ORDER: 444981
|
||||
Art Gallery (Second Floor):
|
||||
HOUSE: 444982
|
||||
PATH: 444983
|
||||
@@ -1382,8 +1383,6 @@ doors:
|
||||
Art Gallery:
|
||||
Second Floor:
|
||||
item: 444558
|
||||
First Floor Puzzles:
|
||||
location: 445256
|
||||
Third Floor:
|
||||
item: 444559
|
||||
Fourth Floor:
|
||||
@@ -1393,6 +1392,9 @@ doors:
|
||||
Exit:
|
||||
item: 444562
|
||||
location: 444981
|
||||
Art Gallery (First Floor):
|
||||
Puzzles:
|
||||
location: 445256
|
||||
Art Gallery (Second Floor):
|
||||
Puzzles:
|
||||
location: 445257
|
||||
|
||||
@@ -23,6 +23,7 @@ class EntranceType(Flag):
|
||||
SUNWARP = auto()
|
||||
WARP = auto()
|
||||
CROSSROADS_ROOF_ACCESS = auto()
|
||||
STATIC_PAINTING = auto()
|
||||
|
||||
|
||||
class RoomEntrance(NamedTuple):
|
||||
|
||||
@@ -30,7 +30,7 @@ def is_acceptable_pilgrimage_entrance(entrance_type: EntranceType, world: "Lingo
|
||||
allowed_entrance_types = EntranceType.NORMAL
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
# 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:
|
||||
# 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}")
|
||||
else:
|
||||
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:
|
||||
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:
|
||||
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
|
||||
elif source_room == "Crossroads" and room_name == "Roof":
|
||||
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"]:
|
||||
PAINTING_EXIT_ROOMS.add(room_name)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# the file are consistent. It also checks that the panel and door IDs mentioned
|
||||
# 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 'yaml'
|
||||
|
||||
@@ -38,6 +38,7 @@ AP_JUNK = 0xD5
|
||||
|
||||
class OoTContainer(APPatch):
|
||||
game: str = 'Ocarina of Time'
|
||||
patch_file_ending = ".apz5"
|
||||
|
||||
def __init__(self, patch_data: bytes, base_path: str, output_directory: 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
|
||||
|
||||
- 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.
|
||||
- Windows-Benutzer müssen die Prerequisiten installiert haben. Diese können ebenfalls über
|
||||
den obrigen Link gefunden werden.
|
||||
- Der integrierte Archipelago-Client, welcher [hier](https://github.com/ArchipelagoMW/Archipelago/releases) installiert
|
||||
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
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
|
||||
## Required Software
|
||||
|
||||
- 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.
|
||||
- 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).
|
||||
- An Ocarina of Time v1.0 ROM.
|
||||
- A US Ocarina of Time v1.0 ROM.
|
||||
|
||||
## Configuring BizHawk
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@ Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windo
|
||||
## Logiciel requis
|
||||
|
||||
- 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.
|
||||
- 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)
|
||||
(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
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ class RaftWorld(World):
|
||||
options_dataclass = RaftOptions
|
||||
options: RaftOptions
|
||||
|
||||
extraItemNamePool: list[str] | None = None
|
||||
|
||||
required_client_version = (0, 3, 4)
|
||||
|
||||
def create_items(self):
|
||||
@@ -52,52 +54,52 @@ class RaftWorld(World):
|
||||
pool = []
|
||||
frequencyItems = []
|
||||
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"]:
|
||||
frequencyItems.append(raft_item)
|
||||
else:
|
||||
pool.append(raft_item)
|
||||
|
||||
extraItemNamePool = []
|
||||
self.extraItemNamePool = []
|
||||
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
|
||||
dupeItemPool = item_table.copy()
|
||||
# Remove frequencies if necessary
|
||||
if self.options.island_frequency_locations != self.options.island_frequency_locations.option_anywhere: # Not completely random locations
|
||||
# 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
|
||||
# instead add progressive-frequency as its own item a smaller amount of times to prevent
|
||||
# flooding the duplicate item pool with them.
|
||||
if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive:
|
||||
for _ in range(2):
|
||||
# Progressives are not in item_pool, need to create faux item for duplicate item pool
|
||||
# This can still be filtered out later by duplicate_items setting
|
||||
dupeItemPool.append({ "name": "progressive-frequency", "progression": True }) # Progressive frequencies need to be included
|
||||
# Always remove non-progressive Frequency items
|
||||
dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"])
|
||||
|
||||
# Remove progression or non-progression items if necessary
|
||||
if (self.options.duplicate_items == self.options.duplicate_items.option_progression): # Progression only
|
||||
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True)
|
||||
elif (self.options.duplicate_items == self.options.duplicate_items.option_non_progression): # Non-progression only
|
||||
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False)
|
||||
|
||||
dupeItemPool = list(dupeItemPool)
|
||||
# Finally, add items as necessary
|
||||
if len(dupeItemPool) > 0:
|
||||
for item in dupeItemPool:
|
||||
extraItemNamePool.append(item["name"])
|
||||
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):
|
||||
self.extraItemNamePool.append(createResourcePackName(i, packItem))
|
||||
|
||||
if self.options.filler_item_types != self.options.filler_item_types.option_resource_packs: # Use duplicate items
|
||||
dupeItemPool = item_table.copy()
|
||||
# Remove frequencies if necessary
|
||||
if self.options.island_frequency_locations != self.options.island_frequency_locations.option_anywhere: # Not completely random locations
|
||||
# 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
|
||||
# instead add progressive-frequency as its own item a smaller amount of times to prevent
|
||||
# flooding the duplicate item pool with them.
|
||||
if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive:
|
||||
for _ in range(2):
|
||||
# Progressives are not in item_pool, need to create faux item for duplicate item pool
|
||||
# This can still be filtered out later by duplicate_items setting
|
||||
dupeItemPool.append({ "name": "progressive-frequency", "progression": True }) # Progressive frequencies need to be included
|
||||
# Always remove non-progressive Frequency items
|
||||
dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"])
|
||||
|
||||
# Remove progression or non-progression items if necessary
|
||||
if (self.options.duplicate_items == self.options.duplicate_items.option_progression): # Progression only
|
||||
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True)
|
||||
elif (self.options.duplicate_items == self.options.duplicate_items.option_non_progression): # Non-progression only
|
||||
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False)
|
||||
|
||||
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):
|
||||
for randomItem in self.random.choices(extraItemNamePool, k=extras):
|
||||
raft_item = self.create_item_replaceAsNecessary(randomItem)
|
||||
pool.append(raft_item)
|
||||
assert self.extraItemNamePool, f"Don't know what extra items to create for {self.player_name}."
|
||||
|
||||
for randomItem in self.random.choices(self.extraItemNamePool, k=extras):
|
||||
raft_item = self.create_item(randomItem)
|
||||
pool.append(raft_item)
|
||||
|
||||
self.multiworld.itempool += pool
|
||||
|
||||
@@ -108,19 +110,35 @@ class RaftWorld(World):
|
||||
if 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):
|
||||
set_rules(self.multiworld, self.player)
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self.multiworld, self.player)
|
||||
|
||||
def create_item_replaceAsNecessary(self, name: str) -> Item:
|
||||
isFrequency = "Frequency" in name
|
||||
shouldUseProgressive = bool((isFrequency and self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive)
|
||||
or (not isFrequency and self.options.progressive_items))
|
||||
if shouldUseProgressive and name in progressive_table:
|
||||
name = progressive_table[name]
|
||||
return self.create_item(name)
|
||||
|
||||
def replace_item_name_as_necessary(self, name: str) -> str:
|
||||
if name not in progressive_table:
|
||||
return name
|
||||
if "Frequency" in name:
|
||||
if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive:
|
||||
return progressive_table[name]
|
||||
elif self.options.progressive_items:
|
||||
return progressive_table[name]
|
||||
return name
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
item = lookup_name_to_item[name]
|
||||
|
||||
@@ -92,17 +92,7 @@ class TestGlobalOptionsImport(TestCase):
|
||||
f"{max_levels_and_upgrades} instead.")
|
||||
|
||||
|
||||
class TestMinimum(ShapezTestBase):
|
||||
options = options_presets["Minimum checks"]
|
||||
|
||||
|
||||
class TestMaximum(ShapezTestBase):
|
||||
options = options_presets["Maximum checks"]
|
||||
|
||||
|
||||
class TestRestrictive(ShapezTestBase):
|
||||
options = options_presets["Restrictive start"]
|
||||
|
||||
# The following unittests are intended to test all code paths of the generator
|
||||
|
||||
class TestAllRelevantOptions1(ShapezTestBase):
|
||||
options = {
|
||||
|
||||
@@ -130,9 +130,7 @@ page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platfor
|
||||
|
||||
### Open the client
|
||||
|
||||
Open ap-soeclient ([Evermizer Archipelago Client Page](http://evermizer.com/apclient)) in a modern browser. Do not
|
||||
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.
|
||||
Open ap-soeclient ([Evermizer Archipelago Client Page](http://evermizer.com/apclient)) in a modern browser.
|
||||
|
||||
The client should automatically connect to SNI, the "SNES" status should change to green.
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import logging
|
||||
import typing
|
||||
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 worlds.AutoWorld import World, WebWorld
|
||||
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.presets import sv_options_presets
|
||||
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 .stardew_rule import True_, StardewRule, HasProgressionPercent
|
||||
from .strings.ap_names.event_names import Event
|
||||
@@ -124,18 +125,13 @@ class StardewValleyWorld(World):
|
||||
self.content = create_content(self.options)
|
||||
|
||||
def create_regions(self):
|
||||
def create_region(name: str, exits: Iterable[str]) -> Region:
|
||||
region = Region(name, self.player, self.multiworld)
|
||||
region.exits = [Entrance(self.player, exit_name, region) for exit_name in exits]
|
||||
return region
|
||||
def create_region(name: str) -> Region:
|
||||
return Region(name, self.player, self.multiworld)
|
||||
|
||||
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.modified_bundles = get_all_bundles(self.random,
|
||||
self.logic,
|
||||
self.content,
|
||||
self.options)
|
||||
self.modified_bundles = get_all_bundles(self.random, self.logic, self.content, self.options)
|
||||
|
||||
def add_location(name: str, code: Optional[int], region: str):
|
||||
region: Region = world_regions[region]
|
||||
@@ -308,6 +304,11 @@ class StardewValleyWorld(World):
|
||||
def 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):
|
||||
pass
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ from ...strings.skill_names import Skill
|
||||
from ...strings.tool_names import Tool, ToolMaterial
|
||||
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):
|
||||
|
||||
@@ -67,6 +70,10 @@ class SVEContentPack(ContentPack):
|
||||
content.game_items.pop(SVESeed.slime)
|
||||
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(
|
||||
ModNames.sve,
|
||||
@@ -80,8 +87,9 @@ register_mod_content_pack(SVEContentPack(
|
||||
ModEdible.lightning_elixir: (ShopSource(money_price=12000, shop_region=SVERegion.galmoran_outpost),),
|
||||
ModEdible.barbarian_elixir: (ShopSource(money_price=22000, shop_region=SVERegion.galmoran_outpost),),
|
||||
ModEdible.gravity_elixir: (ShopSource(money_price=4000, shop_region=SVERegion.galmoran_outpost),),
|
||||
SVEMeal.grampleton_orange_chicken: (
|
||||
ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),),
|
||||
SVEMeal.grampleton_orange_chicken: (ShopSource(money_price=650,
|
||||
shop_region=Region.saloon,
|
||||
other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),),
|
||||
ModEdible.hero_elixir: (ShopSource(money_price=8000, shop_region=SVERegion.isaac_shop),),
|
||||
ModEdible.aegis_elixir: (ShopSource(money_price=28000, shop_region=SVERegion.galmoran_outpost),),
|
||||
SVEBeverage.sports_drink: (ShopSource(money_price=750, shop_region=Region.hospital),),
|
||||
@@ -118,8 +126,8 @@ register_mod_content_pack(SVEContentPack(
|
||||
|
||||
ModLoot.green_mushroom: (ForagingSource(regions=(SVERegion.highlands_pond,), seasons=Season.not_winter),),
|
||||
ModLoot.ornate_treasure_chest: (ForagingSource(regions=(SVERegion.highlands_outside,),
|
||||
other_requirements=(
|
||||
CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),),
|
||||
other_requirements=(CombatRequirement(Performance.galaxy),
|
||||
ToolRequirement(Tool.axe, ToolMaterial.iron))),),
|
||||
ModLoot.swirl_stone: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.galaxy),)),),
|
||||
ModLoot.void_soul: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.good),)),),
|
||||
SVEForage.winter_star_rose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.winter,)),),
|
||||
@@ -139,8 +147,9 @@ register_mod_content_pack(SVEContentPack(
|
||||
SVEForage.thistle: (ForagingSource(regions=(SVERegion.summit,)),),
|
||||
ModLoot.void_pebble: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),),
|
||||
ModLoot.void_shard: (ForagingSource(regions=(SVERegion.crimson_badlands,),
|
||||
other_requirements=(
|
||||
CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),),
|
||||
other_requirements=(CombatRequirement(Performance.galaxy),
|
||||
SkillRequirement(Skill.combat, 10),
|
||||
YearRequirement(3),)),),
|
||||
SVEWaterItem.dulse_seaweed: (ForagingSource(regions=(Region.beach,), other_requirements=(FishingRequirement(Region.beach),)),),
|
||||
|
||||
# Fable Reef
|
||||
@@ -207,7 +216,6 @@ register_mod_content_pack(SVEContentPack(
|
||||
villagers_data.scarlett,
|
||||
villagers_data.susan,
|
||||
villagers_data.morris,
|
||||
# The wizard leaves his tower on sunday, for like 1 hour... Good enough for entrance rando!
|
||||
override(villagers_data.wizard, locations=(Region.wizard_tower, Region.forest), bachelor=True, mod_name=ModNames.sve),
|
||||
override(villagers_data.wizard, 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})
|
||||
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})
|
||||
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})
|
||||
|
||||
@@ -1129,8 +1129,8 @@ id,region,name,tags,mod_name
|
||||
2204,Leo's Hut,Leo's Parrot,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||
2205,Island South,Island West Turtle,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||
2206,Island West,Island Farmhouse,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||
2207,Island Farmhouse,Island Mailbox,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||
2208,Island Farmhouse,Farm Obelisk,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||
2207,Island West,Island Mailbox,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||
2208,Island West,Farm Obelisk,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||
2209,Island North,Dig Site Bridge,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||
2210,Island North,Island Trader,"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)
|
||||
# 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()),
|
||||
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) &
|
||||
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),
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
from typing import Tuple, Union
|
||||
from typing import Tuple
|
||||
|
||||
from Utils import cache_self1
|
||||
from .base_logic import BaseLogic, BaseLogicMixin
|
||||
from .has_logic import HasLogicMixin
|
||||
from ..options import EntranceRandomization
|
||||
from ..stardew_rule import StardewRule, Reach, false_, true_
|
||||
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,
|
||||
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,
|
||||
Region.vault, Region.bulletin_board, Region.mines, Region.hospital, Region.carpenter, Region.alex_house,
|
||||
Region.elliott_house, Region.ranch, Region.farm_cave, Region.wizard_tower, Region.tent, Region.pierre_store,
|
||||
Region.saloon, Region.blacksmith, Region.trailer, Region.museum, Region.mayor_house, Region.haley_house,
|
||||
Region.sam_house, Region.jojamart, Region.fish_shop}
|
||||
always_accessible_regions_with_non_progression_er = {*main_outside_area, Region.mines, Region.hospital, Region.carpenter, Region.alex_house,
|
||||
Region.ranch, Region.farm_cave, Region.wizard_tower, Region.tent,
|
||||
Region.pierre_store, Region.saloon, Region.blacksmith, Region.trailer, Region.museum, Region.mayor_house,
|
||||
Region.haley_house, 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,
|
||||
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: main_outside_area,
|
||||
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 ...strings.ap_names.mods.mod_items import SVELocation, SVERunes, SVEQuestItem
|
||||
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.wallet_item_names import Wallet
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
from typing import Dict, List
|
||||
|
||||
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, \
|
||||
JasperEntrance, AlecEntrance, YobaEntrance, JunaEntrance, MagicEntrance, AyeishaEntrance, RileyEntrance, SVEEntrance, AlectoEntrance
|
||||
from ..strings.region_names import Region, DeepWoodsRegion, EugeneRegion, JasperRegion, BoardingHouseRegion, \
|
||||
AlecRegion, YobaRegion, JunaRegion, MagicRegion, AyeishaRegion, RileyRegion, SVERegion, AlectoRegion, LaceyRegion
|
||||
|
||||
deep_woods_regions = [
|
||||
RegionData(Region.farm, [DeepWoodsEntrance.use_woods_obelisk]),
|
||||
RegionData(DeepWoodsRegion.woods_obelisk_menu, [DeepWoodsEntrance.deep_woods_depth_1,
|
||||
RegionData(Region.farm, (DeepWoodsEntrance.use_woods_obelisk,)),
|
||||
RegionData(DeepWoodsRegion.woods_obelisk_menu, (DeepWoodsEntrance.deep_woods_depth_1,
|
||||
DeepWoodsEntrance.deep_woods_depth_10,
|
||||
DeepWoodsEntrance.deep_woods_depth_20,
|
||||
DeepWoodsEntrance.deep_woods_depth_30,
|
||||
@@ -19,9 +18,9 @@ deep_woods_regions = [
|
||||
DeepWoodsEntrance.deep_woods_depth_70,
|
||||
DeepWoodsEntrance.deep_woods_depth_80,
|
||||
DeepWoodsEntrance.deep_woods_depth_90,
|
||||
DeepWoodsEntrance.deep_woods_depth_100]),
|
||||
RegionData(Region.secret_woods, [DeepWoodsEntrance.secret_woods_to_deep_woods]),
|
||||
RegionData(DeepWoodsRegion.main_lichtung, [DeepWoodsEntrance.deep_woods_house]),
|
||||
DeepWoodsEntrance.deep_woods_depth_100)),
|
||||
RegionData(Region.secret_woods, (DeepWoodsEntrance.secret_woods_to_deep_woods,)),
|
||||
RegionData(DeepWoodsRegion.main_lichtung, (DeepWoodsEntrance.deep_woods_house,)),
|
||||
RegionData(DeepWoodsRegion.abandoned_home),
|
||||
RegionData(DeepWoodsRegion.floor_10),
|
||||
RegionData(DeepWoodsRegion.floor_20),
|
||||
@@ -32,14 +31,13 @@ deep_woods_regions = [
|
||||
RegionData(DeepWoodsRegion.floor_70),
|
||||
RegionData(DeepWoodsRegion.floor_80),
|
||||
RegionData(DeepWoodsRegion.floor_90),
|
||||
RegionData(DeepWoodsRegion.floor_100)
|
||||
RegionData(DeepWoodsRegion.floor_100),
|
||||
]
|
||||
|
||||
deep_woods_entrances = [
|
||||
ConnectionData(DeepWoodsEntrance.use_woods_obelisk, DeepWoodsRegion.woods_obelisk_menu),
|
||||
ConnectionData(DeepWoodsEntrance.secret_woods_to_deep_woods, DeepWoodsRegion.main_lichtung),
|
||||
ConnectionData(DeepWoodsEntrance.deep_woods_house, DeepWoodsRegion.abandoned_home,
|
||||
flag=RandomizationFlag.NON_PROGRESSION),
|
||||
ConnectionData(DeepWoodsEntrance.deep_woods_house, DeepWoodsRegion.abandoned_home, flag=RandomizationFlag.BUILDINGS),
|
||||
ConnectionData(DeepWoodsEntrance.deep_woods_depth_1, DeepWoodsRegion.main_lichtung),
|
||||
ConnectionData(DeepWoodsEntrance.deep_woods_depth_10, DeepWoodsRegion.floor_10),
|
||||
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_80, DeepWoodsRegion.floor_80),
|
||||
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 = [
|
||||
RegionData(Region.forest, [EugeneEntrance.forest_to_garden]),
|
||||
RegionData(EugeneRegion.eugene_garden, [EugeneEntrance.garden_to_bedroom]),
|
||||
RegionData(EugeneRegion.eugene_bedroom)
|
||||
RegionData(Region.forest, (EugeneEntrance.forest_to_garden,)),
|
||||
RegionData(EugeneRegion.eugene_garden, (EugeneEntrance.garden_to_bedroom,)),
|
||||
RegionData(EugeneRegion.eugene_bedroom),
|
||||
]
|
||||
|
||||
eugene_entrances = [
|
||||
ConnectionData(EugeneEntrance.forest_to_garden, EugeneRegion.eugene_garden,
|
||||
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 = [
|
||||
RegionData(Region.pierre_store, [MagicEntrance.store_to_altar]),
|
||||
RegionData(MagicRegion.altar)
|
||||
RegionData(Region.pierre_store, (MagicEntrance.store_to_altar,)),
|
||||
RegionData(MagicRegion.altar),
|
||||
]
|
||||
|
||||
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 = [
|
||||
RegionData(Region.museum, [JasperEntrance.museum_to_bedroom]),
|
||||
RegionData(JasperRegion.jasper_bedroom)
|
||||
RegionData(Region.museum, (JasperEntrance.museum_to_bedroom,)),
|
||||
RegionData(JasperRegion.jasper_bedroom),
|
||||
]
|
||||
|
||||
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 = [
|
||||
RegionData(Region.forest, [AlecEntrance.forest_to_petshop]),
|
||||
RegionData(AlecRegion.pet_store, [AlecEntrance.petshop_to_bedroom]),
|
||||
RegionData(AlecRegion.alec_bedroom)
|
||||
RegionData(Region.forest, (AlecEntrance.forest_to_petshop,)),
|
||||
RegionData(AlecRegion.pet_store, (AlecEntrance.petshop_to_bedroom,)),
|
||||
RegionData(AlecRegion.alec_bedroom),
|
||||
]
|
||||
|
||||
alec_entrances = [
|
||||
ConnectionData(AlecEntrance.forest_to_petshop, AlecRegion.pet_store,
|
||||
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 = [
|
||||
RegionData(Region.secret_woods, [YobaEntrance.secret_woods_to_clearing]),
|
||||
RegionData(YobaRegion.yoba_clearing)
|
||||
RegionData(Region.secret_woods, (YobaEntrance.secret_woods_to_clearing,)),
|
||||
RegionData(YobaRegion.yoba_clearing),
|
||||
]
|
||||
|
||||
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 = [
|
||||
RegionData(Region.forest, [JunaEntrance.forest_to_juna_cave]),
|
||||
RegionData(JunaRegion.juna_cave)
|
||||
RegionData(Region.forest, (JunaEntrance.forest_to_juna_cave,)),
|
||||
RegionData(JunaRegion.juna_cave),
|
||||
]
|
||||
|
||||
juna_entrances = [
|
||||
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 = [
|
||||
RegionData(Region.bus_stop, [AyeishaEntrance.bus_stop_to_mail_van]),
|
||||
RegionData(AyeishaRegion.mail_van)
|
||||
RegionData(Region.bus_stop, (AyeishaEntrance.bus_stop_to_mail_van,)),
|
||||
RegionData(AyeishaRegion.mail_van),
|
||||
]
|
||||
|
||||
ayeisha_entrances = [
|
||||
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 = [
|
||||
RegionData(Region.town, [RileyEntrance.town_to_riley]),
|
||||
RegionData(RileyRegion.riley_house)
|
||||
RegionData(Region.town, (RileyEntrance.town_to_riley,)),
|
||||
RegionData(RileyRegion.riley_house),
|
||||
]
|
||||
|
||||
riley_entrances = [
|
||||
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 = [
|
||||
RegionData(Region.backwoods, [SVEEntrance.backwoods_to_grove]),
|
||||
RegionData(SVERegion.enchanted_grove, [SVEEntrance.grove_to_outpost_warp, SVEEntrance.grove_to_wizard_warp,
|
||||
sve_main_land_regions = [
|
||||
RegionData(Region.backwoods, (SVEEntrance.backwoods_to_grove,)),
|
||||
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_spring_warp, SVEEntrance.grove_to_aurora_warp]),
|
||||
RegionData(SVERegion.grove_farm_warp, [SVEEntrance.farm_warp_to_farm]),
|
||||
RegionData(SVERegion.grove_aurora_warp, [SVEEntrance.aurora_warp_to_aurora]),
|
||||
RegionData(SVERegion.grove_guild_warp, [SVEEntrance.guild_warp_to_guild]),
|
||||
RegionData(SVERegion.grove_junimo_warp, [SVEEntrance.junimo_warp_to_junimo]),
|
||||
RegionData(SVERegion.grove_spring_warp, [SVEEntrance.spring_warp_to_spring]),
|
||||
RegionData(SVERegion.grove_outpost_warp, [SVEEntrance.outpost_warp_to_outpost]),
|
||||
RegionData(SVERegion.grove_wizard_warp, [SVEEntrance.wizard_warp_to_wizard]),
|
||||
RegionData(SVERegion.galmoran_outpost, [SVEEntrance.outpost_to_badlands_entrance, SVEEntrance.use_alesia_shop,
|
||||
SVEEntrance.use_isaac_shop]),
|
||||
RegionData(SVERegion.badlands_entrance, [SVEEntrance.badlands_entrance_to_badlands]),
|
||||
RegionData(SVERegion.crimson_badlands, [SVEEntrance.badlands_to_cave]),
|
||||
SVEEntrance.grove_to_spring_warp, SVEEntrance.grove_to_aurora_warp)),
|
||||
RegionData(SVERegion.grove_farm_warp, (SVEEntrance.farm_warp_to_farm,)),
|
||||
RegionData(SVERegion.grove_aurora_warp, (SVEEntrance.aurora_warp_to_aurora,)),
|
||||
RegionData(SVERegion.grove_guild_warp, (SVEEntrance.guild_warp_to_guild,)),
|
||||
RegionData(SVERegion.grove_junimo_warp, (SVEEntrance.junimo_warp_to_junimo,)),
|
||||
RegionData(SVERegion.grove_spring_warp, (SVEEntrance.spring_warp_to_spring,)),
|
||||
RegionData(SVERegion.grove_outpost_warp, (SVEEntrance.outpost_warp_to_outpost,)),
|
||||
RegionData(SVERegion.grove_wizard_warp, (SVEEntrance.wizard_warp_to_wizard,)),
|
||||
RegionData(SVERegion.galmoran_outpost, (SVEEntrance.outpost_to_badlands_entrance, SVEEntrance.use_alesia_shop, SVEEntrance.use_isaac_shop)),
|
||||
RegionData(SVERegion.badlands_entrance, (SVEEntrance.badlands_entrance_to_badlands,)),
|
||||
RegionData(SVERegion.crimson_badlands, (SVEEntrance.badlands_to_cave,)),
|
||||
RegionData(SVERegion.badlands_cave),
|
||||
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_interior, [SVEEntrance.grandpa_interior_to_upstairs]),
|
||||
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_interior, (SVEEntrance.grandpa_interior_to_upstairs,)),
|
||||
RegionData(SVERegion.grandpas_shed_upstairs),
|
||||
RegionData(Region.forest,
|
||||
[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_fairhaven, SVEEntrance.forest_to_west, SVEEntrance.forest_to_lost_woods,
|
||||
SVEEntrance.forest_to_bmv, SVEEntrance.forest_to_marnie_shed)),
|
||||
RegionData(SVERegion.marnies_shed),
|
||||
RegionData(SVERegion.fairhaven_farm),
|
||||
RegionData(Region.town, [SVEEntrance.town_to_bmv, SVEEntrance.town_to_jenkins,
|
||||
SVEEntrance.town_to_bridge, SVEEntrance.town_to_plot]),
|
||||
RegionData(SVERegion.blue_moon_vineyard, [SVEEntrance.bmv_to_sophia, SVEEntrance.bmv_to_beach]),
|
||||
RegionData(Region.town, (SVEEntrance.town_to_bmv, SVEEntrance.town_to_jenkins, SVEEntrance.town_to_bridge, SVEEntrance.town_to_plot)),
|
||||
RegionData(SVERegion.blue_moon_vineyard, (SVEEntrance.bmv_to_sophia, SVEEntrance.bmv_to_beach)),
|
||||
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.unclaimed_plot, [SVEEntrance.plot_to_bridge]),
|
||||
RegionData(SVERegion.unclaimed_plot, (SVEEntrance.plot_to_bridge,)),
|
||||
RegionData(SVERegion.shearwater),
|
||||
RegionData(Region.museum, [SVEEntrance.museum_to_gunther_bedroom]),
|
||||
RegionData(Region.museum, (SVEEntrance.museum_to_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(Region.mountain, [SVEEntrance.mountain_to_guild_summit]),
|
||||
RegionData(SVERegion.guild_summit, [SVEEntrance.guild_to_interior, SVEEntrance.guild_to_mines,
|
||||
SVEEntrance.summit_to_highlands]),
|
||||
RegionData(Region.railroad, [SVEEntrance.to_susan_house, SVEEntrance.enter_summit, SVEEntrance.railroad_to_grampleton_station]),
|
||||
RegionData(SVERegion.grampleton_station, [SVEEntrance.grampleton_station_to_grampleton_suburbs]),
|
||||
RegionData(SVERegion.grampleton_suburbs, [SVEEntrance.grampleton_suburbs_to_scarlett_house]),
|
||||
RegionData(Region.mountain, (SVEEntrance.mountain_to_guild_summit,)),
|
||||
# These entrances are removed from the mountain region when SVE is enabled
|
||||
RegionData(Region.mountain, (Entrance.mountain_to_adventurer_guild, Entrance.mountain_to_the_mines), flag=MergeFlag.REMOVE_EXITS),
|
||||
RegionData(SVERegion.guild_summit, (SVEEntrance.guild_to_interior, SVEEntrance.guild_to_mines)),
|
||||
RegionData(Region.railroad, (SVEEntrance.to_susan_house, SVEEntrance.enter_summit, SVEEntrance.railroad_to_grampleton_station)),
|
||||
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(Region.wizard_basement, [SVEEntrance.wizard_to_fable_reef]),
|
||||
RegionData(SVERegion.fable_reef, [SVEEntrance.fable_reef_to_guild], is_ginger_island=True),
|
||||
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.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(Region.secret_woods, [SVEEntrance.secret_woods_to_west]),
|
||||
RegionData(Region.secret_woods, (SVEEntrance.secret_woods_to_west,)),
|
||||
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.lost_woods, [SVEEntrance.lost_woods_to_junimo_woods]),
|
||||
RegionData(SVERegion.junimo_woods, [SVEEntrance.use_purple_junimo]),
|
||||
RegionData(SVERegion.lost_woods, (SVEEntrance.lost_woods_to_junimo_woods,)),
|
||||
RegionData(SVERegion.junimo_woods, (SVEEntrance.use_purple_junimo,)),
|
||||
RegionData(SVERegion.purple_junimo_shop),
|
||||
RegionData(SVERegion.alesia_shop),
|
||||
RegionData(SVERegion.isaac_shop),
|
||||
RegionData(SVERegion.summit),
|
||||
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.jenkins_to_cellar, SVERegion.jenkins_cellar, flag=RandomizationFlag.BUILDINGS),
|
||||
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_shed_to_town, Region.town),
|
||||
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.backwoods_to_grove, SVERegion.enchanted_grove, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
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.grove_to_spring_warp, SVERegion.grove_spring_warp),
|
||||
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.badlands_entrance_to_badlands, SVERegion.crimson_badlands, 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.enter_summit, SVERegion.summit, flag=RandomizationFlag.BUILDINGS),
|
||||
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_purple_junimo, SVERegion.purple_junimo_shop),
|
||||
ConnectionData(SVEEntrance.use_alesia_shop, SVERegion.alesia_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.grampleton_station_to_grampleton_suburbs, SVERegion.grampleton_suburbs),
|
||||
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.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.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 = [
|
||||
RegionData(Region.witch_hut, [AlectoEntrance.witch_hut_to_witch_attic]),
|
||||
RegionData(AlectoRegion.witch_attic)
|
||||
RegionData(Region.witch_hut, (AlectoEntrance.witch_hut_to_witch_attic,)),
|
||||
RegionData(AlectoRegion.witch_attic),
|
||||
]
|
||||
|
||||
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 = [
|
||||
RegionData(Region.forest, [LaceyEntrance.forest_to_hat_house]),
|
||||
RegionData(LaceyRegion.hat_house)
|
||||
RegionData(Region.forest, (LaceyEntrance.forest_to_hat_house,)),
|
||||
RegionData(LaceyRegion.hat_house),
|
||||
]
|
||||
|
||||
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 = [
|
||||
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(Region.bus_stop, (BoardingHouseEntrance.bus_stop_to_boarding_house_plateau,)),
|
||||
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_abandoned_mines_entrance]),
|
||||
RegionData(BoardingHouseRegion.boarding_house_first, [BoardingHouseEntrance.boarding_house_first_to_boarding_house_second]),
|
||||
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_second),
|
||||
RegionData(BoardingHouseRegion.buffalo_ranch),
|
||||
RegionData(BoardingHouseRegion.abandoned_mines_entrance, [BoardingHouseEntrance.abandoned_mines_entrance_to_abandoned_mines_1a,
|
||||
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_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_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_4, [BoardingHouseEntrance.abandoned_mines_4_to_abandoned_mines_5]),
|
||||
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.abandoned_mines_entrance, (BoardingHouseEntrance.abandoned_mines_entrance_to_abandoned_mines_1a,
|
||||
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_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_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_4, (BoardingHouseEntrance.abandoned_mines_4_to_abandoned_mines_5,)),
|
||||
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,
|
||||
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.lost_valley_ruins, [BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1,
|
||||
BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2]),
|
||||
RegionData(BoardingHouseRegion.lost_valley_ruins, (BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1,
|
||||
BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2)),
|
||||
RegionData(BoardingHouseRegion.lost_valley_minecart),
|
||||
RegionData(BoardingHouseRegion.lost_valley_house_1),
|
||||
RegionData(BoardingHouseRegion.lost_valley_house_2)
|
||||
RegionData(BoardingHouseRegion.lost_valley_house_2),
|
||||
]
|
||||
|
||||
boarding_house_entrances = [
|
||||
@@ -351,30 +352,29 @@ boarding_house_entrances = [
|
||||
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.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]] = {
|
||||
ModNames.sve: [
|
||||
ConnectionData(Entrance.mountain_to_the_mines, Region.mines,
|
||||
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild,
|
||||
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
]
|
||||
vanilla_connections_to_remove_by_content_pack: dict[str, tuple[str, ...]] = {
|
||||
ModNames.sve: (
|
||||
Entrance.mountain_to_the_mines,
|
||||
Entrance.mountain_to_adventurer_guild,
|
||||
)
|
||||
}
|
||||
|
||||
ModDataList = {
|
||||
ModNames.deepwoods: ModRegionData(ModNames.deepwoods, deep_woods_regions, deep_woods_entrances),
|
||||
ModNames.eugene: ModRegionData(ModNames.eugene, eugene_regions, eugene_entrances),
|
||||
ModNames.jasper: ModRegionData(ModNames.jasper, jasper_regions, jasper_entrances),
|
||||
ModNames.alec: ModRegionData(ModNames.alec, alec_regions, alec_entrances),
|
||||
ModNames.yoba: ModRegionData(ModNames.yoba, yoba_regions, yoba_entrances),
|
||||
ModNames.juna: ModRegionData(ModNames.juna, juna_regions, juna_entrances),
|
||||
ModNames.magic: ModRegionData(ModNames.magic, magic_regions, magic_entrances),
|
||||
ModNames.ayeisha: ModRegionData(ModNames.ayeisha, ayeisha_regions, ayeisha_entrances),
|
||||
ModNames.riley: ModRegionData(ModNames.riley, riley_regions, riley_entrances),
|
||||
ModNames.sve: ModRegionData(ModNames.sve, stardew_valley_expanded_regions, mandatory_sve_connections),
|
||||
ModNames.alecto: ModRegionData(ModNames.alecto, alecto_regions, alecto_entrances),
|
||||
ModNames.lacey: ModRegionData(ModNames.lacey, lacey_regions, lacey_entrances),
|
||||
ModNames.boarding_house: ModRegionData(ModNames.boarding_house, boarding_house_regions, boarding_house_entrances),
|
||||
region_data_by_content_pack = {
|
||||
ModNames.deepwoods: ModRegionsData(ModNames.deepwoods, deep_woods_regions, deep_woods_entrances),
|
||||
ModNames.eugene: ModRegionsData(ModNames.eugene, eugene_regions, eugene_entrances),
|
||||
ModNames.jasper: ModRegionsData(ModNames.jasper, jasper_regions, jasper_entrances),
|
||||
ModNames.alec: ModRegionsData(ModNames.alec, alec_regions, alec_entrances),
|
||||
ModNames.yoba: ModRegionsData(ModNames.yoba, yoba_regions, yoba_entrances),
|
||||
ModNames.juna: ModRegionsData(ModNames.juna, juna_regions, juna_entrances),
|
||||
ModNames.magic: ModRegionsData(ModNames.magic, magic_regions, magic_entrances),
|
||||
ModNames.ayeisha: ModRegionsData(ModNames.ayeisha, ayeisha_regions, ayeisha_entrances),
|
||||
ModNames.riley: ModRegionsData(ModNames.riley, riley_regions, riley_entrances),
|
||||
ModNames.sve: ModRegionsData(ModNames.sve, sve_main_land_regions, sve_main_land_connections),
|
||||
SVE_GINGER_ISLAND_PACK: ModRegionsData(SVE_GINGER_ISLAND_PACK, sve_ginger_island_regions, sve_ginger_island_connections),
|
||||
ModNames.alecto: ModRegionsData(ModNames.alecto, alecto_regions, alecto_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_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.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.town_to_sewer, logic.wallet.has_rusty_key())
|
||||
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 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.rule_explain import explain
|
||||
|
||||
@@ -79,3 +79,13 @@ class RuleAssertMixin(TestCase):
|
||||
except KeyError as e:
|
||||
raise AssertionError(f"Error while checking region {region_name}: {e}"
|
||||
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 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.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
|
||||
from worlds.AutoWorld import call_all
|
||||
@@ -179,6 +179,11 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
|
||||
state = self.multiworld.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 = {}
|
||||
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import random
|
||||
from typing import ClassVar
|
||||
|
||||
from BaseClasses import get_seed
|
||||
from test.param import classvar_matrix
|
||||
from ..TestGeneration import get_all_permanent_progression_items
|
||||
from ..assertion import ModAssertMixin, WorldAssertMixin
|
||||
from ..bases import SVTestCase, SVTestBase, solo_multiworld
|
||||
from ..options.presets import allsanity_mods_6_x_x
|
||||
from ..options.utils import fill_dataclass_with_default
|
||||
from ... import options, Group, create_content
|
||||
from ... import options, Group
|
||||
from ...mods.mod_data import ModNames
|
||||
from ...options.options import all_mods
|
||||
from ...regions import RandomizationFlag, randomize_connections, create_final_connections_and_regions
|
||||
|
||||
|
||||
class TestCanGenerateAllsanityWithMods(WorldAssertMixin, ModAssertMixin, SVTestCase):
|
||||
@@ -117,39 +113,6 @@ class TestNoGingerIslandModItemGeneration(SVTestBase):
|
||||
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):
|
||||
"""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.
|
||||
|
||||
@@ -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('The lab', 'Lab: Coffee break', 1337066),
|
||||
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 (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)) ),
|
||||
@@ -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 (upper)', 'Lab: Download and chest room chest', 1337074),
|
||||
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 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)),
|
||||
@@ -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 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 (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): 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)', '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)', '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): 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)),
|
||||
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)),
|
||||
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 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 - 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('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
|
||||
|
||||
@@ -88,12 +88,15 @@ class PreCalculatedWeights:
|
||||
|
||||
if options.risky_warps:
|
||||
past_teleportation_gates.append("GateLakeSereneLeft")
|
||||
present_teleportation_gates.append("GateDadsTower")
|
||||
if not is_xarion_flooded:
|
||||
present_teleportation_gates.append("GateXarion")
|
||||
if not is_lab_flooded:
|
||||
present_teleportation_gates.append("GateLabEntrance")
|
||||
# Prevent going past the lazers without a way to the past
|
||||
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):
|
||||
all_gates: Tuple[str, ...] = present_teleportation_gates
|
||||
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 (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 (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)', '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)')
|
||||
@@ -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', '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)', '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 (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) )
|
||||
@@ -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', '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', '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', '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"))
|
||||
|
||||
@@ -42,6 +42,7 @@ class TimespinnerWorld(World):
|
||||
topology_present = True
|
||||
web = TimespinnerWebWorld()
|
||||
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()}
|
||||
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():
|
||||
if portal1.scene_destination() == portal_sd:
|
||||
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)
|
||||
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
|
||||
def get_paired_portal(portal_sd: str) -> Tuple[str, str]:
|
||||
for portal1, portal2 in portal_pairs.items():
|
||||
if portal1.scene_destination() == portal_sd:
|
||||
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
|
||||
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(
|
||||
connecting_region=regions["Overworld"])
|
||||
|
||||
@@ -755,6 +755,53 @@ class TWWOptions(PerGameCommonOptions):
|
||||
remove_music: RemoveMusic
|
||||
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]:
|
||||
"""
|
||||
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 Options import Toggle
|
||||
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.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"
|
||||
|
||||
|
||||
class TWWContainer(APContainer, metaclass=AutoPatchRegister):
|
||||
class TWWContainer(APPlayerContainer):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
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.
|
||||
entrances = {
|
||||
|
||||
@@ -117,7 +117,8 @@ def get_pool_core(world: "TWWWorld") -> tuple[list[str], list[str]]:
|
||||
world.filler_pool = filler_pool
|
||||
|
||||
# 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.
|
||||
nonexcluded_locations = [
|
||||
|
||||
Reference in New Issue
Block a user