fix things

This commit is contained in:
CookieCat
2023-11-05 09:38:45 -05:00
parent d2e9bfb196
commit b8948bc495
19 changed files with 6041 additions and 0 deletions

3
.gitignore vendored
View File

@@ -60,6 +60,7 @@ Output Logs/
/installdelete.iss
/data/user.kv
/datapackage
/oot/
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -196,3 +197,5 @@ minecraft_versions.json
.LSOverride
Thumbs.db
[Dd]esktop.ini
A Hat in Time.yaml
ahit.apworld

236
AHITClient.py Normal file
View File

@@ -0,0 +1,236 @@
import asyncio
import Utils
import websockets
import functools
from copy import deepcopy
from typing import List, Any, Iterable
from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem
from MultiServer import Endpoint
from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser
DEBUG = False
class AHITJSONToTextParser(JSONtoTextParser):
def _handle_color(self, node: JSONMessagePart):
return self._handle_text(node) # No colors for the in-game text
class AHITCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx: CommonContext):
super().__init__(ctx)
def _cmd_ahit(self):
"""Check AHIT Connection State"""
if isinstance(self.ctx, AHITContext):
logger.info(f"AHIT Status: {self.ctx.get_ahit_status()}")
class AHITContext(CommonContext):
command_processor = AHITCommandProcessor
game = "A Hat in Time"
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.proxy = None
self.proxy_task = None
self.gamejsontotext = AHITJSONToTextParser(self)
self.autoreconnect_task = None
self.endpoint = None
self.items_handling = 0b111
self.room_info = None
self.connected_msg = None
self.game_connected = False
self.awaiting_info = False
self.full_inventory: List[Any] = []
self.server_msgs: List[Any] = []
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(AHITContext, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
def get_ahit_status(self) -> str:
if not self.is_proxy_connected():
return "Not connected to A Hat in Time"
return "Connected to A Hat in Time"
async def send_msgs_proxy(self, msgs: Iterable[dict]) -> bool:
""" `msgs` JSON serializable """
if not self.endpoint or not self.endpoint.socket.open or self.endpoint.socket.closed:
return False
if DEBUG:
logger.info(f"Outgoing message: {msgs}")
await self.endpoint.socket.send(msgs)
return True
async def disconnect(self, allow_autoreconnect: bool = False):
await super().disconnect(allow_autoreconnect)
async def disconnect_proxy(self):
if self.endpoint and not self.endpoint.socket.closed:
await self.endpoint.socket.close()
if self.proxy_task is not None:
await self.proxy_task
def is_connected(self) -> bool:
return self.server and self.server.socket.open
def is_proxy_connected(self) -> bool:
return self.endpoint and self.endpoint.socket.open
def on_print_json(self, args: dict):
text = self.gamejsontotext(deepcopy(args["data"]))
msg = {"cmd": "PrintJSON", "data": [{"text": text}], "type": "Chat"}
self.server_msgs.append(encode([msg]))
if self.ui:
self.ui.print_json(args["data"])
else:
text = self.jsontotextparser(args["data"])
logger.info(text)
def update_items(self):
# just to be safe - we might still have an inventory from a different room
if not self.is_connected():
return
self.server_msgs.append(encode([{"cmd": "ReceivedItems", "index": 0, "items": self.full_inventory}]))
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
self.connected_msg = encode([args])
if self.awaiting_info:
self.server_msgs.append(self.room_info)
self.update_items()
self.awaiting_info = False
elif cmd == "ReceivedItems":
if args["index"] == 0:
self.full_inventory.clear()
for item in args["items"]:
self.full_inventory.append(NetworkItem(*item))
self.server_msgs.append(encode([args]))
elif cmd == "RoomInfo":
self.seed_name = args["seed_name"]
self.room_info = encode([args])
else:
if cmd != "PrintJSON":
self.server_msgs.append(encode([args]))
def run_gui(self):
from kvui import GameManager
class AHITManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago A Hat in Time Client"
self.ui = AHITManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def proxy(websocket, path: str = "/", ctx: AHITContext = None):
ctx.endpoint = Endpoint(websocket)
try:
await on_client_connected(ctx)
if ctx.is_proxy_connected():
async for data in websocket:
if DEBUG:
logger.info(f"Incoming message: {data}")
for msg in decode(data):
if msg["cmd"] == "Connect":
# Proxy is connecting, make sure it is valid
if msg["game"] != "A Hat in Time":
logger.info("Aborting proxy connection: game is not A Hat in Time")
await ctx.disconnect_proxy()
break
if ctx.seed_name:
seed_name = msg.get("seed_name", "")
if seed_name != "" and seed_name != ctx.seed_name:
logger.info("Aborting proxy connection: seed mismatch from save file")
logger.info(f"Expected: {ctx.seed_name}, got: {seed_name}")
text = encode([{"cmd": "PrintJSON",
"data": [{"text": "Connection aborted - save file to seed mismatch"}]}])
await ctx.send_msgs_proxy(text)
await ctx.disconnect_proxy()
break
if ctx.connected_msg and ctx.is_connected():
await ctx.send_msgs_proxy(ctx.connected_msg)
ctx.update_items()
continue
if not ctx.is_proxy_connected():
break
await ctx.send_msgs([msg])
except Exception as e:
if not isinstance(e, websockets.WebSocketException):
logger.exception(e)
finally:
await ctx.disconnect_proxy()
async def on_client_connected(ctx: AHITContext):
if ctx.room_info and ctx.is_connected():
await ctx.send_msgs_proxy(ctx.room_info)
else:
ctx.awaiting_info = True
async def main():
parser = get_base_parser()
args = parser.parse_args()
ctx = AHITContext(args.connect, args.password)
logger.info("Starting A Hat in Time proxy server")
ctx.proxy = websockets.serve(functools.partial(proxy, ctx=ctx),
host="localhost", port=11311, ping_timeout=999999, ping_interval=999999)
ctx.proxy_task = asyncio.create_task(proxy_loop(ctx), name="ProxyLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
await ctx.proxy
await ctx.proxy_task
await ctx.exit_event.wait()
async def proxy_loop(ctx: AHITContext):
try:
while not ctx.exit_event.is_set():
if len(ctx.server_msgs) > 0:
for msg in ctx.server_msgs:
await ctx.send_msgs_proxy(msg)
ctx.server_msgs.clear()
await asyncio.sleep(0.1)
except Exception as e:
logger.exception(e)
logger.info("Aborting AHIT Proxy Client due to errors")
if __name__ == '__main__':
Utils.init_logging("AHITClient")
options = Utils.get_options()
import colorama
colorama.init()
asyncio.run(main())
colorama.deinit()

BIN
data/yatta.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

BIN
data/yatta.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

642
setup-ahitclient.py Normal file
View File

@@ -0,0 +1,642 @@
import base64
import datetime
import os
import platform
import shutil
import sys
import sysconfig
import typing
import warnings
import zipfile
import urllib.request
import io
import json
import threading
import subprocess
from collections.abc import Iterable
from hashlib import sha3_512
from pathlib import Path
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
try:
requirement = 'cx-Freeze>=6.15.2'
import pkg_resources
try:
pkg_resources.require(requirement)
install_cx_freeze = False
except pkg_resources.ResolutionError:
install_cx_freeze = True
except ImportError:
install_cx_freeze = True
pkg_resources = None # type: ignore [assignment]
if install_cx_freeze:
# check if pip is available
try:
import pip # noqa: F401
except ImportError:
raise RuntimeError("pip not available. Please install pip.")
# install and import cx_freeze
if '--yes' not in sys.argv and '-y' not in sys.argv:
input(f'Requirement {requirement} is not satisfied, press enter to install it')
subprocess.call([sys.executable, '-m', 'pip', 'install', requirement, '--upgrade'])
import pkg_resources
import cx_Freeze
# .build only exists if cx-Freeze is the right version, so we have to update/install that first before this line
import setuptools.command.build
if __name__ == "__main__":
# need to run this early to import from Utils and Launcher
# TODO: move stuff to not require this
import ModuleUpdate
ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv)
ModuleUpdate.update_ran = False # restore for later
from worlds.LauncherComponents import components, icon_paths
from Utils import version_tuple, is_windows, is_linux
from Cython.Build import cythonize
# On Python < 3.10 LogicMixin is not currently supported.
non_apworlds: set = {
"A Link to the Past",
"Adventure",
"ArchipIDLE",
"Archipelago",
"ChecksFinder",
"Clique",
"DLCQuest",
"Final Fantasy",
"Hylics 2",
"Kingdom Hearts 2",
"Lufia II Ancient Cave",
"Meritous",
"Ocarina of Time",
"Overcooked! 2",
"Raft",
"Secret of Evermore",
"Slay the Spire",
"Starcraft 2 Wings of Liberty",
"Sudoku",
"Super Mario 64",
"VVVVVV",
"Wargroove",
"Zillion",
}
# LogicMixin is broken before 3.10 import revamp
if sys.version_info < (3,10):
non_apworlds.add("Hollow Knight")
def download_SNI():
print("Updating SNI")
machine_to_go = {
"x86_64": "amd64",
"aarch64": "arm64",
"armv7l": "arm"
}
platform_name = platform.system().lower()
machine_name = platform.machine().lower()
# force amd64 on macos until we have universal2 sni, otherwise resolve to GOARCH
machine_name = "amd64" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name)
with urllib.request.urlopen("https://api.github.com/repos/alttpo/sni/releases/latest") as request:
data = json.load(request)
files = data["assets"]
source_url = None
for file in files:
download_url: str = file["browser_download_url"]
machine_match = download_url.rsplit("-", 1)[1].split(".", 1)[0] == machine_name
if platform_name in download_url and machine_match:
# prefer "many" builds
if "many" in download_url:
source_url = download_url
break
source_url = download_url
if source_url and source_url.endswith(".zip"):
with urllib.request.urlopen(source_url) as download:
with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf:
for member in zf.infolist():
zf.extract(member, path="SNI")
print(f"Downloaded SNI from {source_url}")
elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")):
import tarfile
mode = "r:xz" if source_url.endswith(".tar.xz") else "r:gz"
with urllib.request.urlopen(source_url) as download:
sni_dir = None
with tarfile.open(fileobj=io.BytesIO(download.read()), mode=mode) as tf:
for member in tf.getmembers():
if member.name.startswith("/") or "../" in member.name:
raise ValueError(f"Unexpected file '{member.name}' in {source_url}")
elif member.isdir() and not sni_dir:
sni_dir = member.name
elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir):
raise ValueError(f"Expected folder before '{member.name}' in {source_url}")
elif member.isfile() and sni_dir:
tf.extract(member)
# sadly SNI is in its own folder on non-windows, so we need to rename
shutil.rmtree("SNI", True)
os.rename(sni_dir, "SNI")
print(f"Downloaded SNI from {source_url}")
elif source_url:
print(f"Don't know how to extract SNI from {source_url}")
else:
print(f"No SNI found for system spec {platform_name} {machine_name}")
signtool: typing.Optional[str]
if os.path.exists("X:/pw.txt"):
print("Using signtool")
with open("X:/pw.txt", encoding="utf-8-sig") as f:
pw = f.read()
signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + \
r'" /fd sha256 /tr http://timestamp.digicert.com/ '
else:
signtool = None
build_platform = sysconfig.get_platform()
arch_folder = "exe.{platform}-{version}".format(platform=build_platform,
version=sysconfig.get_python_version())
buildfolder = Path("build", arch_folder)
build_arch = build_platform.split('-')[-1] if '-' in build_platform else platform.machine()
# see Launcher.py on how to add scripts to setup.py
def resolve_icon(icon_name: str):
base_path = icon_paths[icon_name]
if is_windows:
path, extension = os.path.splitext(base_path)
ico_file = path + ".ico"
assert os.path.exists(ico_file), f"ico counterpart of {base_path} should exist."
return ico_file
else:
return base_path
exes = [
cx_Freeze.Executable(
script=f"{c.script_name}.py",
target_name="ArchipelagoAHITClient.exe",
#target_name=c.frozen_name + (".exe" if is_windows else ""),
icon=resolve_icon(c.icon),
base="Win32GUI" if is_windows and not c.cli else None
) for c in components if c.script_name and c.frozen_name and "AHITClient" in c.script_name
]
#if is_windows:
if False:
# create a duplicate Launcher for Windows, which has a working stdout/stderr, for debugging and --help
c = next(component for component in components if component.script_name == "Launcher")
exes.append(cx_Freeze.Executable(
script=f"{c.script_name}.py",
target_name=f"{c.frozen_name}(DEBUG).exe",
icon=resolve_icon(c.icon),
))
extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
def remove_sprites_from_folder(folder):
for file in os.listdir(folder):
if file != ".gitignore":
os.remove(folder / file)
def _threaded_hash(filepath):
hasher = sha3_512()
hasher.update(open(filepath, "rb").read())
return base64.b85encode(hasher.digest()).decode()
# cx_Freeze's build command runs other commands. Override to accept --yes and store that.
class BuildCommand(setuptools.command.build.build):
user_options = [
('yes', 'y', 'Answer "yes" to all questions.'),
]
yes: bool
last_yes: bool = False # used by sub commands of build
def initialize_options(self):
super().initialize_options()
type(self).last_yes = self.yes = False
def finalize_options(self):
super().finalize_options()
type(self).last_yes = self.yes
# Override cx_Freeze's build_exe command for pre and post build steps
class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
user_options = cx_Freeze.command.build_exe.BuildEXE.user_options + [
('yes', 'y', 'Answer "yes" to all questions.'),
('extra-data=', None, 'Additional files to add.'),
]
yes: bool
extra_data: Iterable # [any] not available in 3.8
extra_libs: Iterable # work around broken include_files
buildfolder: Path
libfolder: Path
library: Path
buildtime: datetime.datetime
def initialize_options(self):
super().initialize_options()
self.yes = BuildCommand.last_yes
self.extra_data = []
self.extra_libs = []
def finalize_options(self):
super().finalize_options()
self.buildfolder = self.build_exe
self.libfolder = Path(self.buildfolder, "lib")
self.library = Path(self.libfolder, "library.zip")
def installfile(self, path, subpath=None, keep_content: bool = False):
folder = self.buildfolder
if subpath:
folder /= subpath
print('copying', path, '->', folder)
if path.is_dir():
folder /= path.name
if folder.is_dir() and not keep_content:
shutil.rmtree(folder)
shutil.copytree(path, folder, dirs_exist_ok=True)
elif path.is_file():
shutil.copy(path, folder)
else:
print('Warning,', path, 'not found')
def create_manifest(self, create_hashes=False):
# Since the setup is now split into components and the manifest is not,
# it makes most sense to just remove the hashes for now. Not aware of anyone using them.
hashes = {}
manifestpath = os.path.join(self.buildfolder, "manifest.json")
if create_hashes:
from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor()
for dirpath, dirnames, filenames in os.walk(self.buildfolder):
for filename in filenames:
path = os.path.join(dirpath, filename)
hashes[os.path.relpath(path, start=self.buildfolder)] = pool.submit(_threaded_hash, path)
import json
manifest = {
"buildtime": self.buildtime.isoformat(sep=" ", timespec="seconds"),
"hashes": {path: hash.result() for path, hash in hashes.items()},
"version": version_tuple}
json.dump(manifest, open(manifestpath, "wt"), indent=4)
print("Created Manifest")
def run(self):
# start downloading sni asap
sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader")
sni_thread.start()
# pre-build steps
print(f"Outputting to: {self.buildfolder}")
os.makedirs(self.buildfolder, exist_ok=True)
import ModuleUpdate
ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
ModuleUpdate.update(yes=self.yes)
# auto-build cython modules
build_ext = self.distribution.get_command_obj("build_ext")
build_ext.inplace = False
self.run_command("build_ext")
# find remains of previous in-place builds, try to delete and warn otherwise
for path in build_ext.get_outputs():
parts = os.path.split(path)[-1].split(".")
pattern = parts[0] + ".*." + parts[-1]
for match in Path().glob(pattern):
try:
match.unlink()
print(f"Removed {match}")
except Exception as ex:
warnings.warn(f"Could not delete old build output: {match}\n"
f"{ex}\nPlease close all AP instances and delete manually.")
# regular cx build
self.buildtime = datetime.datetime.utcnow()
super().run()
# manually copy built modules to lib folder. cx_Freeze does not know they exist.
for src in build_ext.get_outputs():
print(f"copying {src} -> {self.libfolder}")
shutil.copy(src, self.libfolder, follow_symlinks=False)
# need to finish download before copying
sni_thread.join()
# include_files seems to not be done automatically. implement here
for src, dst in self.include_files:
print(f"copying {src} -> {self.buildfolder / dst}")
shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False)
# now that include_files is completely broken, run find_libs here
for src, dst in find_libs(*self.extra_libs):
print(f"copying {src} -> {self.buildfolder / dst}")
shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False)
# post build steps
if is_windows: # kivy_deps is win32 only, linux picks them up automatically
from kivy_deps import sdl2, glew
for folder in sdl2.dep_bins + glew.dep_bins:
shutil.copytree(folder, self.libfolder, dirs_exist_ok=True)
print(f"copying {folder} -> {self.libfolder}")
for data in self.extra_data:
self.installfile(Path(data))
# kivi data files
import kivy
shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"),
self.buildfolder / "data",
dirs_exist_ok=True)
os.makedirs(self.buildfolder / "Players" / "Templates", exist_ok=True)
from Options import generate_yaml_templates
from worlds.AutoWorld import AutoWorldRegister
assert not non_apworlds - set(AutoWorldRegister.world_types), \
f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld"
folders_to_remove: typing.List[str] = []
generate_yaml_templates(self.buildfolder / "Players" / "Templates", False)
for worldname, worldtype in AutoWorldRegister.world_types.items():
if worldname not in non_apworlds:
file_name = os.path.split(os.path.dirname(worldtype.__file__))[1]
world_directory = self.libfolder / "worlds" / file_name
# this method creates an apworld that cannot be moved to a different OS or minor python version,
# which should be ok
with zipfile.ZipFile(self.libfolder / "worlds" / (file_name + ".apworld"), "x", zipfile.ZIP_DEFLATED,
compresslevel=9) as zf:
for path in world_directory.rglob("*.*"):
relative_path = os.path.join(*path.parts[path.parts.index("worlds")+1:])
zf.write(path, relative_path)
folders_to_remove.append(file_name)
shutil.rmtree(world_directory)
shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml")
# TODO: fix LttP options one day
shutil.copyfile("playerSettings.yaml", self.buildfolder / "Players" / "Templates" / "A Link to the Past.yaml")
try:
from maseya import z3pr
except ImportError:
print("Maseya Palette Shuffle not found, skipping data files.")
else:
# maseya Palette Shuffle exists and needs its data files
print("Maseya Palette Shuffle found, including data files...")
file = z3pr.__file__
self.installfile(Path(os.path.dirname(file)) / "data", keep_content=True)
if signtool:
for exe in self.distribution.executables:
print(f"Signing {exe.target_name}")
os.system(signtool + os.path.join(self.buildfolder, exe.target_name))
print("Signing SNI")
os.system(signtool + os.path.join(self.buildfolder, "SNI", "SNI.exe"))
print("Signing OoT Utils")
for exe_path in (("Compress", "Compress.exe"), ("Decompress", "Decompress.exe")):
os.system(signtool + os.path.join(self.buildfolder, "lib", "worlds", "oot", "data", *exe_path))
remove_sprites_from_folder(self.buildfolder / "data" / "sprites" / "alttpr")
self.create_manifest()
if is_windows:
# Inno setup stuff
with open("setup.ini", "w") as f:
min_supported_windows = "6.2.9200" if sys.version_info > (3, 9) else "6.0.6000"
f.write(f"[Data]\nsource_path={self.buildfolder}\nmin_windows={min_supported_windows}\n")
with open("installdelete.iss", "w") as f:
f.writelines("Type: filesandordirs; Name: \"{app}\\lib\\worlds\\"+world_directory+"\"\n"
for world_directory in folders_to_remove)
else:
# make sure extra programs are executable
enemizer_exe = self.buildfolder / 'EnemizerCLI/EnemizerCLI.Core'
sni_exe = self.buildfolder / 'SNI/sni'
extra_exes = (enemizer_exe, sni_exe)
for extra_exe in extra_exes:
if extra_exe.is_file():
extra_exe.chmod(0o755)
class AppImageCommand(setuptools.Command):
description = "build an app image from build output"
user_options = [
("build-folder=", None, "Folder to convert to AppImage."),
("dist-file=", None, "AppImage output file."),
("app-dir=", None, "Folder to use for packaging."),
("app-icon=", None, "The icon to use for the AppImage."),
("app-exec=", None, "The application to run inside the image."),
("yes", "y", 'Answer "yes" to all questions.'),
]
build_folder: typing.Optional[Path]
dist_file: typing.Optional[Path]
app_dir: typing.Optional[Path]
app_name: str
app_exec: typing.Optional[Path]
app_icon: typing.Optional[Path] # source file
app_id: str # lower case name, used for icon and .desktop
yes: bool
def write_desktop(self):
assert self.app_dir, "Invalid app_dir"
desktop_filename = self.app_dir / f"{self.app_id}.desktop"
with open(desktop_filename, 'w', encoding="utf-8") as f:
f.write("\n".join((
"[Desktop Entry]",
f'Name={self.app_name}',
f'Exec={self.app_exec}',
"Type=Application",
"Categories=Game",
f'Icon={self.app_id}',
''
)))
desktop_filename.chmod(0o755)
def write_launcher(self, default_exe: Path):
assert self.app_dir, "Invalid app_dir"
launcher_filename = self.app_dir / "AppRun"
with open(launcher_filename, 'w', encoding="utf-8") as f:
f.write(f"""#!/bin/sh
exe="{default_exe}"
match="${{1#--executable=}}"
if [ "${{#match}}" -lt "${{#1}}" ]; then
exe="$match"
shift
elif [ "$1" = "-executable" ] || [ "$1" = "--executable" ]; then
exe="$2"
shift; shift
fi
tmp="${{exe#*/}}"
if [ ! "${{#tmp}}" -lt "${{#exe}}" ]; then
exe="{default_exe.parent}/$exe"
fi
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$APPDIR/{default_exe.parent}/lib"
$APPDIR/$exe "$@"
""")
launcher_filename.chmod(0o755)
def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None):
assert self.app_dir, "Invalid app_dir"
try:
from PIL import Image
except ModuleNotFoundError:
if not self.yes:
input("Requirement PIL is not satisfied, press enter to install it")
subprocess.call([sys.executable, '-m', 'pip', 'install', 'Pillow', '--upgrade'])
from PIL import Image
im = Image.open(src)
res, _ = im.size
if not name:
name = src.stem
ext = src.suffix
dest_dir = Path(self.app_dir / f'usr/share/icons/hicolor/{res}x{res}/apps')
dest_dir.mkdir(parents=True, exist_ok=True)
dest_file = dest_dir / f'{name}{ext}'
shutil.copy(src, dest_file)
if symlink:
symlink.symlink_to(dest_file.relative_to(symlink.parent))
def initialize_options(self):
self.build_folder = None
self.app_dir = None
self.app_name = self.distribution.metadata.name
self.app_icon = self.distribution.executables[0].icon
self.app_exec = Path('opt/{app_name}/{exe}'.format(
app_name=self.distribution.metadata.name, exe=self.distribution.executables[0].target_name
))
self.dist_file = Path("dist", "{app_name}_{app_version}_{platform}.AppImage".format(
app_name=self.distribution.metadata.name, app_version=self.distribution.metadata.version,
platform=sysconfig.get_platform()
))
self.yes = False
def finalize_options(self):
if not self.app_dir:
self.app_dir = self.build_folder.parent / "AppDir"
self.app_id = self.app_name.lower()
def run(self):
self.dist_file.parent.mkdir(parents=True, exist_ok=True)
if self.app_dir.is_dir():
shutil.rmtree(self.app_dir)
self.app_dir.mkdir(parents=True)
opt_dir = self.app_dir / "opt" / self.distribution.metadata.name
shutil.copytree(self.build_folder, opt_dir)
root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}'
self.install_icon(self.app_icon, self.app_id, symlink=root_icon)
shutil.copy(root_icon, self.app_dir / '.DirIcon')
self.write_desktop()
self.write_launcher(self.app_exec)
print(f'{self.app_dir} -> {self.dist_file}')
subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True)
def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]:
"""Try to find system libraries to be included."""
if not args:
return []
arch = build_arch.replace('_', '-')
libc = 'libc6' # we currently don't support musl
def parse(line):
lib, path = line.strip().split(' => ')
lib, typ = lib.split(' ', 1)
for test_arch in ('x86-64', 'i386', 'aarch64'):
if test_arch in typ:
lib_arch = test_arch
break
else:
lib_arch = ''
for test_libc in ('libc6',):
if test_libc in typ:
lib_libc = test_libc
break
else:
lib_libc = ''
return (lib, lib_arch, lib_libc), path
if not hasattr(find_libs, "cache"):
ldconfig = shutil.which("ldconfig")
assert ldconfig, "Make sure ldconfig is in PATH"
data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:]
find_libs.cache = { # type: ignore [attr-defined]
k: v for k, v in (parse(line) for line in data if "=>" in line)
}
def find_lib(lib, arch, libc):
for k, v in find_libs.cache.items():
if k == (lib, arch, libc):
return v
for k, v, in find_libs.cache.items():
if k[0].startswith(lib) and k[1] == arch and k[2] == libc:
return v
return None
res = []
for arg in args:
# try exact match, empty libc, empty arch, empty arch and libc
file = find_lib(arg, arch, libc)
file = file or find_lib(arg, arch, '')
file = file or find_lib(arg, '', libc)
file = file or find_lib(arg, '', '')
# resolve symlinks
for n in range(0, 5):
res.append((file, os.path.join('lib', os.path.basename(file))))
if not os.path.islink(file):
break
dirname = os.path.dirname(file)
file = os.readlink(file)
if not os.path.isabs(file):
file = os.path.join(dirname, file)
return res
cx_Freeze.setup(
name="Archipelago",
version=f"{version_tuple.major}.{version_tuple.minor}.{version_tuple.build}",
description="Archipelago",
executables=exes,
ext_modules=cythonize("_speedups.pyx"),
options={
"build_exe": {
"packages": ["worlds", "kivy", "cymem", "websockets"],
"includes": [],
"excludes": ["numpy", "Cython", "PySide2", "PIL",
"pandas"],
"zip_include_packages": ["*"],
"zip_exclude_packages": ["worlds", "sc2"],
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
"include_msvcr": False,
"replace_paths": ["*."],
"optimize": 1,
"build_exe": buildfolder,
"extra_data": extra_data,
"extra_libs": extra_libs,
"bin_includes": ["libffi.so", "libcrypt.so"] if is_linux else []
},
"bdist_appimage": {
"build_folder": buildfolder,
},
},
# override commands to get custom stuff in
cmdclass={
"build": BuildCommand,
"build_exe": BuildExeCommand,
"bdist_appimage": AppImageCommand,
},
)

View File

@@ -0,0 +1,262 @@
from .Types import HatInTimeLocation, HatInTimeItem
from .Regions import connect_regions, create_region
from BaseClasses import Region, LocationProgressType, ItemClassification
from worlds.generic.Rules import add_rule
from worlds.AutoWorld import World
from typing import List
from .Locations import death_wishes
dw_prereqs = {
"So You're Back From Outer Space": ["Beat the Heat"],
"Snatcher's Hit List": ["Beat the Heat"],
"Snatcher Coins in Mafia Town": ["So You're Back From Outer Space"],
"Rift Collapse: Mafia of Cooks": ["So You're Back From Outer Space"],
"Collect-a-thon": ["So You're Back From Outer Space"],
"She Speedran from Outer Space": ["Rift Collapse: Mafia of Cooks"],
"Mafia's Jumps": ["She Speedran from Outer Space"],
"Vault Codes in the Wind": ["Collect-a-thon", "She Speedran from Outer Space"],
"Encore! Encore!": ["Collect-a-thon"],
"Security Breach": ["Beat the Heat"],
"Rift Collapse: Dead Bird Studio": ["Security Breach"],
"The Great Big Hootenanny": ["Security Breach"],
"10 Seconds until Self-Destruct": ["The Great Big Hootenanny"],
"Killing Two Birds": ["Rift Collapse: Dead Bird Studio", "10 Seconds until Self-Destruct"],
"Community Rift: Rhythm Jump Studio": ["10 Seconds until Self-Destruct"],
"Snatcher Coins in Battle of the Birds": ["The Great Big Hootenanny"],
"Zero Jumps": ["Rift Collapse: Dead Bird Studio"],
"Snatcher Coins in Nyakuza Metro": ["Killing Two Birds"],
"Speedrun Well": ["Beat the Heat"],
"Rift Collapse: Sleepy Subcon": ["Speedrun Well"],
"Boss Rush": ["Speedrun Well"],
"Quality Time with Snatcher": ["Rift Collapse: Sleepy Subcon"],
"Breaching the Contract": ["Boss Rush", "Quality Time with Snatcher"],
"Community Rift: Twilight Travels": ["Quality Time with Snatcher"],
"Snatcher Coins in Subcon Forest": ["Rift Collapse: Sleepy Subcon"],
"Bird Sanctuary": ["Beat the Heat"],
"Snatcher Coins in Alpine Skyline": ["Bird Sanctuary"],
"Wound-Up Windmill": ["Bird Sanctuary"],
"Rift Collapse: Alpine Skyline": ["Bird Sanctuary"],
"Camera Tourist": ["Rift Collapse: Alpine Skyline"],
"Community Rift: The Mountain Rift": ["Rift Collapse: Alpine Skyline"],
"The Illness has Speedrun": ["Rift Collapse: Alpine Skyline", "Wound-Up Windmill"],
"The Mustache Gauntlet": ["Wound-Up Windmill"],
"No More Bad Guys": ["The Mustache Gauntlet"],
"Seal the Deal": ["Encore! Encore!", "Killing Two Birds",
"Breaching the Contract", "No More Bad Guys"],
"Rift Collapse: Deep Sea": ["Rift Collapse: Mafia of Cooks", "Rift Collapse: Dead Bird Studio",
"Rift Collapse: Sleepy Subcon", "Rift Collapse: Alpine Skyline"],
"Cruisin' for a Bruisin'": ["Rift Collapse: Deep Sea"],
}
dw_candles = [
"Snatcher's Hit List",
"Zero Jumps",
"Camera Tourist",
"Snatcher Coins in Mafia Town",
"Snatcher Coins in Battle of the Birds",
"Snatcher Coins in Subcon Forest",
"Snatcher Coins in Alpine Skyline",
"Snatcher Coins in Nyakuza Metro",
]
annoying_dws = [
"Vault Codes in the Wind",
"Boss Rush",
"Camera Tourist",
"The Mustache Gauntlet",
"Rift Collapse: Deep Sea",
"Cruisin' for a Bruisin'",
"Seal the Deal", # Non-excluded if goal
]
# includes the above as well
annoying_bonuses = [
"So You're Back From Outer Space",
"Encore! Encore!",
"Snatcher's Hit List",
"10 Seconds until Self-Destruct",
"Killing Two Birds",
"Zero Jumps",
"Bird Sanctuary",
"Wound-Up Windmill",
"Seal the Deal",
]
dw_classes = {
"Beat the Heat": "Hat_SnatcherContract_DeathWish_HeatingUpHarder",
"So You're Back From Outer Space": "Hat_SnatcherContract_DeathWish_BackFromSpace",
"Snatcher's Hit List": "Hat_SnatcherContract_DeathWish_KillEverybody",
"Collect-a-thon": "Hat_SnatcherContract_DeathWish_PonFrenzy",
"Rift Collapse: Mafia of Cooks": "Hat_SnatcherContract_DeathWish_RiftCollapse_MafiaTown",
"Encore! Encore!": "Hat_SnatcherContract_DeathWish_MafiaBossEX",
"She Speedran from Outer Space": "Hat_SnatcherContract_DeathWish_Speedrun_MafiaAlien",
"Mafia's Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses_MafiaAlien",
"Vault Codes in the Wind": "Hat_SnatcherContract_DeathWish_MovingVault",
"Snatcher Coins in Mafia Town": "Hat_SnatcherContract_DeathWish_Tokens_MafiaTown",
"Security Breach": "Hat_SnatcherContract_DeathWish_DeadBirdStudioMoreGuards",
"The Great Big Hootenanny": "Hat_SnatcherContract_DeathWish_DifficultParade",
"Rift Collapse: Dead Bird Studio": "Hat_SnatcherContract_DeathWish_RiftCollapse_Birds",
"10 Seconds until Self-Destruct": "Hat_SnatcherContract_DeathWish_TrainRushShortTime",
"Killing Two Birds": "Hat_SnatcherContract_DeathWish_BirdBossEX",
"Snatcher Coins in Battle of the Birds": "Hat_SnatcherContract_DeathWish_Tokens_Birds",
"Zero Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses",
"Speedrun Well": "Hat_SnatcherContract_DeathWish_Speedrun_SubWell",
"Rift Collapse: Sleepy Subcon": "Hat_SnatcherContract_DeathWish_RiftCollapse_Subcon",
"Boss Rush": "Hat_SnatcherContract_DeathWish_BossRush",
"Quality Time with Snatcher": "Hat_SnatcherContract_DeathWish_SurvivalOfTheFittest",
"Breaching the Contract": "Hat_SnatcherContract_DeathWish_SnatcherEX",
"Snatcher Coins in Subcon Forest": "Hat_SnatcherContract_DeathWish_Tokens_Subcon",
"Bird Sanctuary": "Hat_SnatcherContract_DeathWish_NiceBirdhouse",
"Rift Collapse: Alpine Skyline": "Hat_SnatcherContract_DeathWish_RiftCollapse_Alps",
"Wound-Up Windmill": "Hat_SnatcherContract_DeathWish_FastWindmill",
"The Illness has Speedrun": "Hat_SnatcherContract_DeathWish_Speedrun_Illness",
"Snatcher Coins in Alpine Skyline": "Hat_SnatcherContract_DeathWish_Tokens_Alps",
"Camera Tourist": "Hat_SnatcherContract_DeathWish_CameraTourist_1",
"The Mustache Gauntlet": "Hat_SnatcherContract_DeathWish_HardCastle",
"No More Bad Guys": "Hat_SnatcherContract_DeathWish_MuGirlEX",
"Seal the Deal": "Hat_SnatcherContract_DeathWish_BossRushEX",
"Rift Collapse: Deep Sea": "Hat_SnatcherContract_DeathWish_RiftCollapse_Cruise",
"Cruisin' for a Bruisin'": "Hat_SnatcherContract_DeathWish_EndlessTasks",
"Community Rift: Rhythm Jump Studio": "Hat_SnatcherContract_DeathWish_CommunityRift_RhythmJump",
"Community Rift: Twilight Travels": "Hat_SnatcherContract_DeathWish_CommunityRift_TwilightTravels",
"Community Rift: The Mountain Rift": "Hat_SnatcherContract_DeathWish_CommunityRift_MountainRift",
"Snatcher Coins in Nyakuza Metro": "Hat_SnatcherContract_DeathWish_Tokens_Metro",
}
def create_dw_regions(world: World):
if world.multiworld.DWExcludeAnnoyingContracts[world.player].value > 0:
for name in annoying_dws:
world.get_excluded_dws().append(name)
if world.multiworld.DWEnableBonus[world.player].value == 0 \
or world.multiworld.DWAutoCompleteBonuses[world.player].value > 0:
for name in death_wishes:
world.get_excluded_bonuses().append(name)
elif world.multiworld.DWExcludeAnnoyingBonuses[world.player].value > 0:
for name in annoying_bonuses:
world.get_excluded_bonuses().append(name)
if world.multiworld.DWExcludeCandles[world.player].value > 0:
for name in dw_candles:
if name in world.get_excluded_dws():
continue
world.get_excluded_dws().append(name)
spaceship = world.multiworld.get_region("Spaceship", world.player)
dw_map: Region = create_region(world, "Death Wish Map")
entrance = connect_regions(spaceship, dw_map, "-> Death Wish Map", world.player)
add_rule(entrance, lambda state: state.has("Time Piece", world.player,
world.multiworld.DWTimePieceRequirement[world.player].value))
if world.multiworld.DWShuffle[world.player].value > 0:
dw_list: List[str] = []
for name in death_wishes.keys():
if not world.is_dlc2() and name == "Snatcher Coins in Nyakuza Metro" or world.is_dw_excluded(name):
continue
dw_list.append(name)
world.random.shuffle(dw_list)
count = world.random.randint(world.multiworld.DWShuffleCountMin[world.player].value,
world.multiworld.DWShuffleCountMax[world.player].value)
dw_shuffle: List[str] = []
total = min(len(dw_list), count)
for i in range(total):
dw_shuffle.append(dw_list[i])
# Seal the Deal is always last if it's the goal
if world.multiworld.EndGoal[world.player].value == 3:
if "Seal the Deal" in dw_shuffle:
dw_shuffle.remove("Seal the Deal")
dw_shuffle.append("Seal the Deal")
world.set_dw_shuffle(dw_shuffle)
prev_dw: Region
for i in range(len(dw_shuffle)):
name = dw_shuffle[i]
dw = create_region(world, name)
if i == 0:
connect_regions(dw_map, dw, f"-> {name}", world.player)
else:
connect_regions(prev_dw, dw, f"{prev_dw.name} -> {name}", world.player)
loc_id = death_wishes[name]
main_objective = HatInTimeLocation(world.player, f"{name} - Main Objective", loc_id, dw)
full_clear = HatInTimeLocation(world.player, f"{name} - All Clear", loc_id + 1, dw)
main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {name}", None, dw)
bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {name}", None, dw)
main_stamp.show_in_spoiler = False
bonus_stamps.show_in_spoiler = False
dw.locations.append(main_stamp)
dw.locations.append(bonus_stamps)
main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {name}",
ItemClassification.progression, None, world.player))
bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {name}",
ItemClassification.progression, None, world.player))
if name in world.get_excluded_dws():
main_objective.progress_type = LocationProgressType.EXCLUDED
full_clear.progress_type = LocationProgressType.EXCLUDED
elif world.is_bonus_excluded(name):
full_clear.progress_type = LocationProgressType.EXCLUDED
dw.locations.append(main_objective)
dw.locations.append(full_clear)
prev_dw = dw
else:
for key, loc_id in death_wishes.items():
if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2():
world.get_excluded_dws().append(key)
continue
dw = create_region(world, key)
if key == "Beat the Heat":
connect_regions(dw_map, dw, "-> Beat the Heat", world.player)
elif key in dw_prereqs.keys():
for name in dw_prereqs[key]:
parent = world.multiworld.get_region(name, world.player)
connect_regions(parent, dw, f"{parent.name} -> {key}", world.player)
main_objective = HatInTimeLocation(world.player, f"{key} - Main Objective", loc_id, dw)
full_clear = HatInTimeLocation(world.player, f"{key} - All Clear", loc_id+1, dw)
main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {key}", None, dw)
bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {key}", None, dw)
main_stamp.show_in_spoiler = False
bonus_stamps.show_in_spoiler = False
dw.locations.append(main_stamp)
dw.locations.append(bonus_stamps)
main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {key}",
ItemClassification.progression, None, world.player))
bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {key}",
ItemClassification.progression, None, world.player))
if key in world.get_excluded_dws():
main_objective.progress_type = LocationProgressType.EXCLUDED
full_clear.progress_type = LocationProgressType.EXCLUDED
elif world.is_bonus_excluded(key):
full_clear.progress_type = LocationProgressType.EXCLUDED
dw.locations.append(main_objective)
dw.locations.append(full_clear)

View File

@@ -0,0 +1,539 @@
from worlds.AutoWorld import World, CollectionState
from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, get_difficulty, has_paintings
from .Types import HatType, Difficulty, HatInTimeLocation, HatInTimeItem, LocData
from .DeathWishLocations import dw_prereqs, dw_candles
from BaseClasses import Entrance, Location, ItemClassification
from worlds.generic.Rules import add_rule, set_rule
from typing import List, Callable
from .Regions import act_chapters
from .Locations import zero_jumps, zero_jumps_expert, zero_jumps_hard, death_wishes
# Any speedruns expect the player to have Sprint Hat
dw_requirements = {
"Beat the Heat": LocData(umbrella=True),
"So You're Back From Outer Space": LocData(hookshot=True),
"She Speedran from Outer Space": LocData(required_hats=[HatType.SPRINT]),
"Mafia's Jumps": LocData(required_hats=[HatType.ICE]),
"Vault Codes in the Wind": LocData(required_hats=[HatType.SPRINT]),
"Security Breach": LocData(hit_requirement=1),
"10 Seconds until Self-Destruct": LocData(hookshot=True),
"Community Rift: Rhythm Jump Studio": LocData(required_hats=[HatType.ICE]),
"Speedrun Well": LocData(hookshot=True, hit_requirement=1, required_hats=[HatType.SPRINT]),
"Boss Rush": LocData(umbrella=True, hookshot=True),
"Community Rift: Twilight Travels": LocData(hookshot=True, required_hats=[HatType.DWELLER]),
"Bird Sanctuary": LocData(hookshot=True),
"Wound-Up Windmill": LocData(hookshot=True),
"The Illness has Speedrun": LocData(hookshot=True),
"Community Rift: The Mountain Rift": LocData(hookshot=True, required_hats=[HatType.DWELLER]),
"Camera Tourist": LocData(misc_required=["Camera Badge"]),
"The Mustache Gauntlet": LocData(hookshot=True, required_hats=[HatType.DWELLER]),
"Rift Collapse - Deep Sea": LocData(hookshot=True),
}
# Includes main objective requirements
dw_bonus_requirements = {
# Some One-Hit Hero requirements need badge pins as well because of Hookshot
"So You're Back From Outer Space": LocData(required_hats=[HatType.SPRINT]),
"Encore! Encore!": LocData(misc_required=["One-Hit Hero Badge"]),
"10 Seconds until Self-Destruct": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]),
"Boss Rush": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]),
"Community Rift: Twilight Travels": LocData(required_hats=[HatType.BREWING]),
"Bird Sanctuary": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"], required_hats=[HatType.DWELLER]),
"Wound-Up Windmill": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]),
"The Illness has Speedrun": LocData(required_hats=[HatType.SPRINT]),
"The Mustache Gauntlet": LocData(required_hats=[HatType.ICE]),
"Rift Collapse - Deep Sea": LocData(required_hats=[HatType.DWELLER]),
}
dw_stamp_costs = {
"So You're Back From Outer Space": 2,
"Collect-a-thon": 5,
"She Speedran from Outer Space": 8,
"Encore! Encore!": 10,
"Security Breach": 4,
"The Great Big Hootenanny": 7,
"10 Seconds until Self-Destruct": 15,
"Killing Two Birds": 25,
"Snatcher Coins in Nyakuza Metro": 30,
"Speedrun Well": 10,
"Boss Rush": 15,
"Quality Time with Snatcher": 20,
"Breaching the Contract": 40,
"Bird Sanctuary": 15,
"Wound-Up Windmill": 30,
"The Illness has Speedrun": 35,
"The Mustache Gauntlet": 35,
"No More Bad Guys": 50,
"Seal the Deal": 70,
}
required_snatcher_coins = {
"Snatcher Coins in Mafia Town": ["Snatcher Coin - Top of HQ", "Snatcher Coin - Top of Tower",
"Snatcher Coin - Under Ruined Tower"],
"Snatcher Coins in Battle of the Birds": ["Snatcher Coin - Top of Red House", "Snatcher Coin - Train Rush",
"Snatcher Coin - Picture Perfect"],
"Snatcher Coins in Subcon Forest": ["Snatcher Coin - Swamp Tree", "Snatcher Coin - Manor Roof",
"Snatcher Coin - Giant Time Piece"],
"Snatcher Coins in Alpine Skyline": ["Snatcher Coin - Goat Village Top", "Snatcher Coin - Lava Cake",
"Snatcher Coin - Windmill"],
"Snatcher Coins in Nyakuza Metro": ["Snatcher Coin - Green Clean Tower", "Snatcher Coin - Bluefin Cat Train",
"Snatcher Coin - Pink Paw Fence"],
}
def set_dw_rules(world: World):
if "Snatcher's Hit List" not in world.get_excluded_dws() \
or "Camera Tourist" not in world.get_excluded_dws():
set_enemy_rules(world)
dw_list: List[str] = []
if world.multiworld.DWShuffle[world.player].value > 0:
dw_list = world.get_dw_shuffle()
else:
for name in death_wishes.keys():
dw_list.append(name)
for name in dw_list:
if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2():
continue
dw = world.multiworld.get_region(name, world.player)
temp_list: List[Location] = []
main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player)
full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player)
main_stamp = world.multiworld.get_location(f"Main Stamp - {name}", world.player)
bonus_stamps = world.multiworld.get_location(f"Bonus Stamps - {name}", world.player)
temp_list.append(main_objective)
temp_list.append(full_clear)
if world.multiworld.DWShuffle[world.player].value == 0:
if name in dw_stamp_costs.keys():
for entrance in dw.entrances:
add_rule(entrance, lambda state, n=name: get_total_dw_stamps(state, world) >= dw_stamp_costs[n])
if world.multiworld.DWEnableBonus[world.player].value == 0:
# place nothing, but let the locations exist still, so we can use them for bonus stamp rules
full_clear.address = None
full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, world.player))
full_clear.show_in_spoiler = False
# No need for rules if excluded - stamps will be auto-granted
if world.is_dw_excluded(name):
continue
# Specific Rules
modify_dw_rules(world, name)
main_rule: Callable[[CollectionState], bool]
for i in range(len(temp_list)):
loc = temp_list[i]
data: LocData
if loc.name == main_objective.name:
data = dw_requirements.get(name)
else:
data = dw_bonus_requirements.get(name)
if data is None:
continue
if data.hookshot:
add_rule(loc, lambda state: can_use_hookshot(state, world))
for hat in data.required_hats:
if hat is not HatType.NONE:
add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h))
for misc in data.misc_required:
add_rule(loc, lambda state, item=misc: state.has(item, world.player))
if data.umbrella and world.multiworld.UmbrellaLogic[world.player].value > 0:
add_rule(loc, lambda state: state.has("Umbrella", world.player))
if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0:
add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings))
if data.hit_requirement > 0:
if data.hit_requirement == 1:
add_rule(loc, lambda state: can_hit(state, world))
elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells)
add_rule(loc, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER))
main_rule = main_objective.access_rule
if loc.name == main_objective.name:
add_rule(main_stamp, loc.access_rule)
elif loc.name == full_clear.name:
add_rule(loc, main_rule)
# Only set bonus stamp rules if we don't auto complete bonuses
if world.multiworld.DWAutoCompleteBonuses[world.player].value == 0 \
and not world.is_bonus_excluded(loc.name):
add_rule(bonus_stamps, loc.access_rule)
if world.multiworld.DWShuffle[world.player].value > 0:
dw_shuffle = world.get_dw_shuffle()
for i in range(len(dw_shuffle)):
if i == 0:
continue
name = dw_shuffle[i]
prev_dw = world.multiworld.get_region(dw_shuffle[i-1], world.player)
entrance = world.multiworld.get_entrance(f"{prev_dw.name} -> {name}", world.player)
add_rule(entrance, lambda state, n=prev_dw.name: state.has(f"1 Stamp - {n}", world.player))
else:
for key, reqs in dw_prereqs.items():
if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2():
continue
access_rules: List[Callable[[CollectionState], bool]] = []
entrances: List[Entrance] = []
for parent in reqs:
entrance = world.multiworld.get_entrance(f"{parent} -> {key}", world.player)
entrances.append(entrance)
if not world.is_dw_excluded(parent):
access_rules.append(lambda state, n=parent: state.has(f"1 Stamp - {n}", world.player))
for entrance in entrances:
for rule in access_rules:
add_rule(entrance, rule)
if world.multiworld.EndGoal[world.player].value == 3:
world.multiworld.completion_condition[world.player] = lambda state: state.has("1 Stamp - Seal the Deal",
world.player)
def modify_dw_rules(world: World, name: str):
difficulty: Difficulty = get_difficulty(world)
main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player)
full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player)
if name == "The Illness has Speedrun":
# All stamps with hookshot only in Expert
if difficulty >= Difficulty.EXPERT:
set_rule(full_clear, lambda state: True)
else:
add_rule(main_objective, lambda state: state.has("Umbrella", world.player))
elif name == "The Mustache Gauntlet":
add_rule(main_objective, lambda state: state.has("Umbrella", world.player)
or can_use_hat(state, world, HatType.ICE) or can_use_hat(state, world, HatType.BREWING))
elif name == "Vault Codes in the Wind":
# Sprint is normally expected here
if difficulty >= Difficulty.HARD:
set_rule(main_objective, lambda state: True)
elif name == "Speedrun Well":
# All stamps with nothing :)
if difficulty >= Difficulty.EXPERT:
set_rule(main_objective, lambda state: True)
elif name == "Mafia's Jumps":
if difficulty >= Difficulty.HARD:
set_rule(main_objective, lambda state: True)
set_rule(full_clear, lambda state: True)
elif name == "So You're Back from Outer Space":
# Without Hookshot
if difficulty >= Difficulty.HARD:
set_rule(main_objective, lambda state: True)
elif name == "Wound-Up Windmill":
# No badge pin required. Player can switch to One Hit Hero after the checkpoint and do level without it.
if difficulty >= Difficulty.MODERATE:
set_rule(full_clear, lambda state: can_use_hookshot(state, world)
and state.has("One-Hit Hero Badge", world.player))
if name in dw_candles:
set_candle_dw_rules(name, world)
def get_total_dw_stamps(state: CollectionState, world: World) -> int:
if world.multiworld.DWShuffle[world.player].value > 0:
return 999 # no stamp costs in death wish shuffle
count: int = 0
for name in death_wishes:
if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2():
continue
if state.has(f"1 Stamp - {name}", world.player):
count += 1
else:
continue
if state.has(f"2 Stamps - {name}", world.player):
count += 2
elif name not in dw_candles:
# most non-candle bonus requirements allow the player to get the other stamp (like not having One Hit Hero)
count += 1
return count
def set_candle_dw_rules(name: str, world: World):
main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player)
full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player)
if name == "Zero Jumps":
add_rule(main_objective, lambda state: get_zero_jump_clear_count(state, world) >= 1)
add_rule(full_clear, lambda state: get_zero_jump_clear_count(state, world) >= 4
and state.has("Train Rush (Zero Jumps)", world.player) and can_use_hat(state, world, HatType.ICE))
# No Ice Hat/painting required in Expert for Toilet Zero Jumps
# This painting wall can only be skipped via cherry hover.
if get_difficulty(world) < Difficulty.EXPERT or world.multiworld.NoPaintingSkips[world.player].value == 1:
set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player),
lambda state: can_use_hookshot(state, world) and can_hit(state, world)
and has_paintings(state, world, 1, False))
else:
set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player),
lambda state: can_use_hookshot(state, world) and can_hit(state, world))
set_rule(world.multiworld.get_location("Contractual Obligations (Zero Jumps)", world.player),
lambda state: has_paintings(state, world, 1, False))
elif name == "Snatcher's Hit List":
add_rule(main_objective, lambda state: state.has("Mafia Goon", world.player))
add_rule(full_clear, lambda state: get_reachable_enemy_count(state, world) >= 12)
elif name == "Camera Tourist":
add_rule(main_objective, lambda state: get_reachable_enemy_count(state, world) >= 8)
add_rule(full_clear, lambda state: can_reach_all_bosses(state, world)
and state.has("Triple Enemy Photo", world.player))
elif "Snatcher Coins" in name:
for coin in required_snatcher_coins[name]:
add_rule(main_objective, lambda state: state.has(coin, world.player), "or")
add_rule(full_clear, lambda state: state.has(coin, world.player))
def get_zero_jump_clear_count(state: CollectionState, world: World) -> int:
total: int = 0
for name in act_chapters.keys():
n = f"{name} (Zero Jumps)"
if n not in zero_jumps:
continue
if get_difficulty(world) < Difficulty.HARD and n in zero_jumps_hard:
continue
if get_difficulty(world) < Difficulty.EXPERT and n in zero_jumps_expert:
continue
if not state.has(n, world.player):
continue
total += 1
return total
def get_reachable_enemy_count(state: CollectionState, world: World) -> int:
count: int = 0
for enemy in hit_list.keys():
if enemy in bosses:
continue
if state.has(enemy, world.player):
count += 1
return count
def can_reach_all_bosses(state: CollectionState, world: World) -> bool:
for boss in bosses:
if not state.has(boss, world.player):
return False
return True
def create_enemy_events(world: World):
no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses()
for enemy, regions in hit_list.items():
if no_tourist and enemy in bosses:
continue
for area in regions:
if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1():
continue
if area == "Time Rift - Tour" and (not world.is_dlc1()
or world.multiworld.ExcludeTour[world.player].value > 0):
continue
if area == "Bluefin Tunnel" and not world.is_dlc2():
continue
if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes.keys() \
and area not in world.get_dw_shuffle():
continue
region = world.multiworld.get_region(area, world.player)
event = HatInTimeLocation(world.player, f"{enemy} - {area}", None, region)
event.place_locked_item(HatInTimeItem(enemy, ItemClassification.progression, None, world.player))
region.locations.append(event)
event.show_in_spoiler = False
for name in triple_enemy_locations:
if name == "Time Rift - Tour" and (not world.is_dlc1() or world.multiworld.ExcludeTour[world.player].value > 0):
continue
if world.multiworld.DWShuffle[world.player].value > 0 and name in death_wishes.keys() \
and name not in world.get_dw_shuffle():
continue
region = world.multiworld.get_region(name, world.player)
event = HatInTimeLocation(world.player, f"Triple Enemy Photo - {name}", None, region)
event.place_locked_item(HatInTimeItem("Triple Enemy Photo", ItemClassification.progression, None, world.player))
region.locations.append(event)
event.show_in_spoiler = False
if name == "The Mustache Gauntlet":
add_rule(event, lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER))
def set_enemy_rules(world: World):
no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses()
for enemy, regions in hit_list.items():
if no_tourist and enemy in bosses:
continue
for area in regions:
if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1():
continue
if area == "Time Rift - Tour" and (not world.is_dlc1()
or world.multiworld.ExcludeTour[world.player].value > 0):
continue
if area == "Bluefin Tunnel" and not world.is_dlc2():
continue
if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes \
and area not in world.get_dw_shuffle():
continue
event = world.multiworld.get_location(f"{enemy} - {area}", world.player)
if enemy == "Toxic Flower":
add_rule(event, lambda state: can_use_hookshot(state, world))
if area == "The Illness has Spread":
add_rule(event, lambda state: not zipline_logic(world) or
state.has("Zipline Unlock - The Birdhouse Path", world.player)
or state.has("Zipline Unlock - The Lava Cake Path", world.player)
or state.has("Zipline Unlock - The Windmill Path", world.player))
elif enemy == "Director":
if area == "Dead Bird Studio Basement":
add_rule(event, lambda state: can_use_hookshot(state, world))
elif enemy == "Snatcher" or enemy == "Mustache Girl":
if area == "Boss Rush":
# need to be able to kill toilet and snatcher
add_rule(event, lambda state: can_hit(state, world) and can_use_hookshot(state, world))
if enemy == "Mustache Girl":
add_rule(event, lambda state: can_hit(state, world, True) and can_use_hookshot(state, world))
elif area == "The Finale" and enemy == "Mustache Girl":
add_rule(event, lambda state: can_use_hookshot(state, world)
and can_use_hat(state, world, HatType.DWELLER))
elif enemy == "Shock Squid" or enemy == "Ninja Cat":
if area == "Time Rift - Deep Sea":
add_rule(event, lambda state: can_use_hookshot(state, world))
# Enemies for Snatcher's Hit List/Camera Tourist, and where to find them
hit_list = {
"Mafia Goon": ["Mafia Town Area", "Time Rift - Mafia of Cooks", "Time Rift - Tour",
"Bon Voyage!", "The Mustache Gauntlet", "Rift Collapse: Mafia of Cooks",
"So You're Back From Outer Space"],
"Sleepy Raccoon": ["She Came from Outer Space", "Down with the Mafia!", "The Twilight Bell",
"She Speedran from Outer Space", "Mafia's Jumps", "The Mustache Gauntlet",
"Time Rift - Sleepy Subcon", "Rift Collapse: Sleepy Subcon"],
"UFO": ["Picture Perfect", "So You're Back From Outer Space", "Community Rift: Rhythm Jump Studio"],
"Rat": ["Down with the Mafia!", "Bluefin Tunnel"],
"Shock Squid": ["Bon Voyage!", "Time Rift - Sleepy Subcon", "Time Rift - Deep Sea",
"Rift Collapse: Sleepy Subcon"],
"Shromb Egg": ["The Birdhouse", "Bird Sanctuary"],
"Spider": ["Subcon Forest Area", "The Mustache Gauntlet", "Speedrun Well",
"The Lava Cake", "The Windmill"],
"Crow": ["Mafia Town Area", "The Birdhouse", "Time Rift - Tour", "Bird Sanctuary",
"Time Rift - Alpine Skyline", "Rift Collapse: Alpine Skyline"],
"Pompous Crow": ["The Birdhouse", "Time Rift - The Lab", "Bird Sanctuary", "The Mustache Gauntlet"],
"Fiery Crow": ["The Finale", "The Lava Cake", "The Mustache Gauntlet"],
"Express Owl": ["The Finale", "Time Rift - The Owl Express", "Time Rift - Deep Sea"],
"Ninja Cat": ["The Birdhouse", "The Windmill", "Bluefin Tunnel", "The Mustache Gauntlet",
"Time Rift - Curly Tail Trail", "Time Rift - Alpine Skyline", "Time Rift - Deep Sea",
"Rift Collapse: Alpine Skyline"],
# Bosses
"Mafia Boss": ["Down with the Mafia!", "Encore! Encore!", "Boss Rush"],
"Conductor": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"],
"Toilet": ["Toilet of Doom", "Boss Rush"],
"Snatcher": ["Your Contract has Expired", "Breaching the Contract", "Boss Rush",
"Quality Time with Snatcher"],
"Toxic Flower": ["The Illness has Spread", "The Illness has Speedrun"],
"Mustache Girl": ["The Finale", "Boss Rush", "No More Bad Guys"],
}
# Camera Tourist has a bonus that requires getting three different types of enemies in one picture.
triple_enemy_locations = [
"She Came from Outer Space",
"She Speedran from Outer Space",
"Mafia's Jumps",
"The Mustache Gauntlet",
"The Birdhouse",
"Bird Sanctuary",
"Time Rift - Tour",
]
bosses = [
"Mafia Boss",
"Conductor",
"Toilet",
"Snatcher",
"Toxic Flower",
"Mustache Girl",
]

286
worlds/ahit/Items.py Normal file
View File

@@ -0,0 +1,286 @@
from BaseClasses import Item, ItemClassification
from worlds.AutoWorld import World
from .Types import HatDLC, HatType, hat_type_to_item, Difficulty, ItemData, HatInTimeItem
from .Locations import get_total_locations
from .Rules import get_difficulty
from typing import Optional, List, Dict
def create_itempool(world: World) -> List[Item]:
itempool: List[Item] = []
if not world.is_dw_only() and world.multiworld.HatItems[world.player].value == 0:
calculate_yarn_costs(world)
yarn_pool: List[Item] = create_multiple_items(world, "Yarn",
world.multiworld.YarnAvailable[world.player].value,
ItemClassification.progression_skip_balancing)
for i in range(int(len(yarn_pool) * (0.01 * world.multiworld.YarnBalancePercent[world.player].value))):
yarn_pool[i].classification = ItemClassification.progression
itempool += yarn_pool
for name in item_table.keys():
if name == "Yarn":
continue
if not item_dlc_enabled(world, name):
continue
if world.multiworld.HatItems[world.player].value == 0 and name in hat_type_to_item.values():
continue
item_type: ItemClassification = item_table.get(name).classification
if world.is_dw_only():
if item_type is ItemClassification.progression \
or item_type is ItemClassification.progression_skip_balancing:
continue
else:
if name == "Scooter Badge":
if world.multiworld.CTRLogic[world.player].value >= 1 or get_difficulty(world) >= Difficulty.MODERATE:
item_type = ItemClassification.progression
elif name == "No Bonk Badge":
if get_difficulty(world) >= Difficulty.MODERATE:
item_type = ItemClassification.progression
# some death wish bonuses require one hit hero + hookshot
if world.is_dw() and name == "Badge Pin" and not world.is_dw_only():
item_type = ItemClassification.progression
if item_type is ItemClassification.filler or item_type is ItemClassification.trap:
continue
if name in act_contracts.keys() and world.multiworld.ShuffleActContracts[world.player].value == 0:
continue
if name in alps_hooks.keys() and world.multiworld.ShuffleAlpineZiplines[world.player].value == 0:
continue
if name == "Progressive Painting Unlock" \
and world.multiworld.ShuffleSubconPaintings[world.player].value == 0:
continue
if world.multiworld.StartWithCompassBadge[world.player].value > 0 and name == "Compass Badge":
continue
if name == "Time Piece":
tp_count: int = 40
max_extra: int = 0
if world.is_dlc1():
max_extra += 6
if world.is_dlc2():
max_extra += 10
tp_count += min(max_extra, world.multiworld.MaxExtraTimePieces[world.player].value)
tp_list: List[Item] = create_multiple_items(world, name, tp_count, item_type)
for i in range(int(len(tp_list) * (0.01 * world.multiworld.TimePieceBalancePercent[world.player].value))):
tp_list[i].classification = ItemClassification.progression
itempool += tp_list
continue
itempool += create_multiple_items(world, name, item_frequencies.get(name, 1), item_type)
itempool += create_junk_items(world, get_total_locations(world) - len(itempool))
return itempool
def calculate_yarn_costs(world: World):
mw = world.multiworld
p = world.player
min_yarn_cost = int(min(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value))
max_yarn_cost = int(max(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value))
max_cost: int = 0
for i in range(5):
cost: int = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost))
world.get_hat_yarn_costs()[HatType(i)] = cost
max_cost += cost
available_yarn: int = mw.YarnAvailable[p].value
if max_cost > available_yarn:
mw.YarnAvailable[p].value = max_cost
available_yarn = max_cost
if max_cost + mw.MinExtraYarn[p].value > available_yarn:
mw.YarnAvailable[p].value += (max_cost + mw.MinExtraYarn[p].value) - available_yarn
def item_dlc_enabled(world: World, name: str) -> bool:
data = item_table[name]
if data.dlc_flags == HatDLC.none:
return True
elif data.dlc_flags == HatDLC.dlc1 and world.is_dlc1():
return True
elif data.dlc_flags == HatDLC.dlc2 and world.is_dlc2():
return True
elif data.dlc_flags == HatDLC.death_wish and world.is_dw():
return True
return False
def create_item(world: World, name: str) -> Item:
data = item_table[name]
return HatInTimeItem(name, data.classification, data.code, world.player)
def create_multiple_items(world: World, name: str, count: int = 1,
item_type: Optional[ItemClassification] = ItemClassification.progression) -> List[Item]:
data = item_table[name]
itemlist: List[Item] = []
for i in range(count):
itemlist += [HatInTimeItem(name, item_type, data.code, world.player)]
return itemlist
def create_junk_items(world: World, count: int) -> List[Item]:
trap_chance = world.multiworld.TrapChance[world.player].value
junk_pool: List[Item] = []
junk_list: Dict[str, int] = {}
trap_list: Dict[str, int] = {}
ic: ItemClassification
for name in item_table.keys():
ic = item_table[name].classification
if ic == ItemClassification.filler:
if world.is_dw_only() and "Pons" in name:
continue
junk_list[name] = junk_weights.get(name)
elif trap_chance > 0 and ic == ItemClassification.trap:
if name == "Baby Trap":
trap_list[name] = world.multiworld.BabyTrapWeight[world.player].value
elif name == "Laser Trap":
trap_list[name] = world.multiworld.LaserTrapWeight[world.player].value
elif name == "Parade Trap":
trap_list[name] = world.multiworld.ParadeTrapWeight[world.player].value
for i in range(count):
if trap_chance > 0 and world.random.randint(1, 100) <= trap_chance:
junk_pool += [world.create_item(
world.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])]
else:
junk_pool += [world.create_item(
world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])]
return junk_pool
ahit_items = {
"Yarn": ItemData(2000300001, ItemClassification.progression_skip_balancing),
"Time Piece": ItemData(2000300002, ItemClassification.progression_skip_balancing),
# for HatItems option
"Sprint Hat": ItemData(2000300049, ItemClassification.progression),
"Brewing Hat": ItemData(2000300050, ItemClassification.progression),
"Ice Hat": ItemData(2000300051, ItemClassification.progression),
"Dweller Mask": ItemData(2000300052, ItemClassification.progression),
"Time Stop Hat": ItemData(2000300053, ItemClassification.progression),
# Relics
"Relic (Burger Patty)": ItemData(2000300006, ItemClassification.progression),
"Relic (Burger Cushion)": ItemData(2000300007, ItemClassification.progression),
"Relic (Mountain Set)": ItemData(2000300008, ItemClassification.progression),
"Relic (Train)": ItemData(2000300009, ItemClassification.progression),
"Relic (UFO)": ItemData(2000300010, ItemClassification.progression),
"Relic (Cow)": ItemData(2000300011, ItemClassification.progression),
"Relic (Cool Cow)": ItemData(2000300012, ItemClassification.progression),
"Relic (Tin-foil Hat Cow)": ItemData(2000300013, ItemClassification.progression),
"Relic (Crayon Box)": ItemData(2000300014, ItemClassification.progression),
"Relic (Red Crayon)": ItemData(2000300015, ItemClassification.progression),
"Relic (Blue Crayon)": ItemData(2000300016, ItemClassification.progression),
"Relic (Green Crayon)": ItemData(2000300017, ItemClassification.progression),
# Badges
"Projectile Badge": ItemData(2000300024, ItemClassification.useful),
"Fast Hatter Badge": ItemData(2000300025, ItemClassification.useful),
"Hover Badge": ItemData(2000300026, ItemClassification.useful),
"Hookshot Badge": ItemData(2000300027, ItemClassification.progression),
"Item Magnet Badge": ItemData(2000300028, ItemClassification.useful),
"No Bonk Badge": ItemData(2000300029, ItemClassification.useful),
"Compass Badge": ItemData(2000300030, ItemClassification.useful),
"Scooter Badge": ItemData(2000300031, ItemClassification.useful),
"One-Hit Hero Badge": ItemData(2000300038, ItemClassification.progression, HatDLC.death_wish),
"Camera Badge": ItemData(2000300042, ItemClassification.progression, HatDLC.death_wish),
# Other
"Badge Pin": ItemData(2000300043, ItemClassification.useful),
"Umbrella": ItemData(2000300033, ItemClassification.progression),
"Progressive Painting Unlock": ItemData(2000300003, ItemClassification.progression),
# Garbage items
"25 Pons": ItemData(2000300034, ItemClassification.filler),
"50 Pons": ItemData(2000300035, ItemClassification.filler),
"100 Pons": ItemData(2000300036, ItemClassification.filler),
"Health Pon": ItemData(2000300037, ItemClassification.filler),
"Random Cosmetic": ItemData(2000300044, ItemClassification.filler),
# Traps
"Baby Trap": ItemData(2000300039, ItemClassification.trap),
"Laser Trap": ItemData(2000300040, ItemClassification.trap),
"Parade Trap": ItemData(2000300041, ItemClassification.trap),
# DLC1 items
"Relic (Cake Stand)": ItemData(2000300018, ItemClassification.progression, HatDLC.dlc1),
"Relic (Shortcake)": ItemData(2000300019, ItemClassification.progression, HatDLC.dlc1),
"Relic (Chocolate Cake Slice)": ItemData(2000300020, ItemClassification.progression, HatDLC.dlc1),
"Relic (Chocolate Cake)": ItemData(2000300021, ItemClassification.progression, HatDLC.dlc1),
# DLC2 items
"Relic (Necklace Bust)": ItemData(2000300022, ItemClassification.progression, HatDLC.dlc2),
"Relic (Necklace)": ItemData(2000300023, ItemClassification.progression, HatDLC.dlc2),
"Metro Ticket - Yellow": ItemData(2000300045, ItemClassification.progression, HatDLC.dlc2),
"Metro Ticket - Green": ItemData(2000300046, ItemClassification.progression, HatDLC.dlc2),
"Metro Ticket - Blue": ItemData(2000300047, ItemClassification.progression, HatDLC.dlc2),
"Metro Ticket - Pink": ItemData(2000300048, ItemClassification.progression, HatDLC.dlc2),
}
act_contracts = {
"Snatcher's Contract - The Subcon Well": ItemData(2000300200, ItemClassification.progression),
"Snatcher's Contract - Toilet of Doom": ItemData(2000300201, ItemClassification.progression),
"Snatcher's Contract - Queen Vanessa's Manor": ItemData(2000300202, ItemClassification.progression),
"Snatcher's Contract - Mail Delivery Service": ItemData(2000300203, ItemClassification.progression),
}
alps_hooks = {
"Zipline Unlock - The Birdhouse Path": ItemData(2000300204, ItemClassification.progression),
"Zipline Unlock - The Lava Cake Path": ItemData(2000300205, ItemClassification.progression),
"Zipline Unlock - The Windmill Path": ItemData(2000300206, ItemClassification.progression),
"Zipline Unlock - The Twilight Bell Path": ItemData(2000300207, ItemClassification.progression),
}
relic_groups = {
"Burger": {"Relic (Burger Patty)", "Relic (Burger Cushion)"},
"Train": {"Relic (Mountain Set)", "Relic (Train)"},
"UFO": {"Relic (UFO)", "Relic (Cow)", "Relic (Cool Cow)", "Relic (Tin-foil Hat Cow)"},
"Crayon": {"Relic (Crayon Box)", "Relic (Red Crayon)", "Relic (Blue Crayon)", "Relic (Green Crayon)"},
"Cake": {"Relic (Cake Stand)", "Relic (Chocolate Cake)", "Relic (Chocolate Cake Slice)", "Relic (Shortcake)"},
"Necklace": {"Relic (Necklace Bust)", "Relic (Necklace)"},
}
item_frequencies = {
"Badge Pin": 2,
"Progressive Painting Unlock": 3,
}
junk_weights = {
"25 Pons": 50,
"50 Pons": 25,
"100 Pons": 10,
"Health Pon": 35,
"Random Cosmetic": 35,
}
item_table = {
**ahit_items,
**act_contracts,
**alps_hooks,
}

977
worlds/ahit/Locations.py Normal file
View File

@@ -0,0 +1,977 @@
from worlds.AutoWorld import World
from .Types import HatDLC, HatType, LocData, Difficulty
from typing import Dict
from .Options import TasksanityCheckCount
TASKSANITY_START_ID = 2000300204
def get_total_locations(world: World) -> int:
total: int = 0
if not world.is_dw_only():
for (name) in location_table.keys():
if is_location_valid(world, name):
total += 1
if world.is_dlc1() and world.multiworld.Tasksanity[world.player].value > 0:
total += world.multiworld.TasksanityCheckCount[world.player].value
if world.is_dw():
if world.multiworld.DWShuffle[world.player].value > 0:
total += len(world.get_dw_shuffle())
if world.multiworld.DWEnableBonus[world.player].value > 0:
total += len(world.get_dw_shuffle())
else:
total += 37
if world.is_dlc2():
total += 1
if world.multiworld.DWEnableBonus[world.player].value > 0:
total += 37
if world.is_dlc2():
total += 1
return total
def location_dlc_enabled(world: World, location: str) -> bool:
data = location_table.get(location) or event_locs.get(location)
if data.dlc_flags == HatDLC.none:
return True
elif data.dlc_flags == HatDLC.dlc1 and world.is_dlc1():
return True
elif data.dlc_flags == HatDLC.dlc2 and world.is_dlc2():
return True
elif data.dlc_flags == HatDLC.death_wish and world.is_dw():
return True
elif data.dlc_flags == HatDLC.dlc1_dw and world.is_dlc1() and world.is_dw():
return True
elif data.dlc_flags == HatDLC.dlc2_dw and world.is_dlc2() and world.is_dw():
return True
return False
def is_location_valid(world: World, location: str) -> bool:
if not location_dlc_enabled(world, location):
return False
if world.multiworld.ShuffleStorybookPages[world.player].value == 0 \
and location in storybook_pages.keys():
return False
if world.multiworld.ShuffleActContracts[world.player].value == 0 \
and location in contract_locations.keys():
return False
if location not in world.shop_locs and location in shop_locations:
return False
data = location_table.get(location) or event_locs.get(location)
if world.multiworld.ExcludeTour[world.player].value > 0 and data.region == "Time Rift - Tour":
return False
# No need for all those event items if we're not doing candles
if data.dlc_flags & HatDLC.death_wish:
if world.multiworld.DWExcludeCandles[world.player].value > 0 and location in event_locs.keys():
return False
if world.multiworld.DWShuffle[world.player].value > 0 \
and data.region in death_wishes and data.region not in world.get_dw_shuffle():
return False
if location in zero_jumps:
if world.multiworld.DWShuffle[world.player].value > 0 and "Zero Jumps" not in world.get_dw_shuffle():
return False
difficulty: int = world.multiworld.LogicDifficulty[world.player].value
if location in zero_jumps_hard and difficulty < int(Difficulty.HARD):
return False
if location in zero_jumps_expert and difficulty < int(Difficulty.EXPERT):
return False
return True
def get_location_names() -> Dict[str, int]:
names = {name: data.id for name, data in location_table.items()}
id_start: int = TASKSANITY_START_ID
for i in range(TasksanityCheckCount.range_end):
names.setdefault(f"Tasksanity Check {i+1}", id_start+i)
for (key, loc_id) in death_wishes.items():
names.setdefault(f"{key} - Main Objective", loc_id)
names.setdefault(f"{key} - All Clear", loc_id+1)
return names
ahit_locations = {
"Spaceship - Rumbi Abuse": LocData(2000301000, "Spaceship", hit_requirement=1),
# 300000 range - Mafia Town/Batle of the Birds
"Welcome to Mafia Town - Umbrella": LocData(2000301002, "Welcome to Mafia Town"),
"Mafia Town - Old Man (Seaside Spaghetti)": LocData(2000303833, "Mafia Town Area"),
"Mafia Town - Old Man (Steel Beams)": LocData(2000303832, "Mafia Town Area"),
"Mafia Town - Blue Vault": LocData(2000302850, "Mafia Town Area"),
"Mafia Town - Green Vault": LocData(2000302851, "Mafia Town Area"),
"Mafia Town - Red Vault": LocData(2000302848, "Mafia Town Area"),
"Mafia Town - Blue Vault Brewing Crate": LocData(2000305572, "Mafia Town Area", required_hats=[HatType.BREWING]),
"Mafia Town - Plaza Under Boxes": LocData(2000304458, "Mafia Town Area"),
"Mafia Town - Small Boat": LocData(2000304460, "Mafia Town Area"),
"Mafia Town - Staircase Pon Cluster": LocData(2000304611, "Mafia Town Area"),
"Mafia Town - Palm Tree": LocData(2000304609, "Mafia Town Area"),
"Mafia Town - Port": LocData(2000305219, "Mafia Town Area"),
"Mafia Town - Docks Chest": LocData(2000303534, "Mafia Town Area"),
"Mafia Town - Ice Hat Cage": LocData(2000304831, "Mafia Town Area", required_hats=[HatType.ICE]),
"Mafia Town - Hidden Buttons Chest": LocData(2000303483, "Mafia Town Area"),
# These can be accessed from HUMT, the above locations can't be
"Mafia Town - Dweller Boxes": LocData(2000304462, "Mafia Town Area (HUMT)"),
"Mafia Town - Ledge Chest": LocData(2000303530, "Mafia Town Area (HUMT)"),
"Mafia Town - Yellow Sphere Building Chest": LocData(2000303535, "Mafia Town Area (HUMT)"),
"Mafia Town - Beneath Scaffolding": LocData(2000304456, "Mafia Town Area (HUMT)"),
"Mafia Town - On Scaffolding": LocData(2000304457, "Mafia Town Area (HUMT)"),
"Mafia Town - Cargo Ship": LocData(2000304459, "Mafia Town Area (HUMT)"),
"Mafia Town - Beach Alcove": LocData(2000304463, "Mafia Town Area (HUMT)"),
"Mafia Town - Wood Cage": LocData(2000304606, "Mafia Town Area (HUMT)"),
"Mafia Town - Beach Patio": LocData(2000304610, "Mafia Town Area (HUMT)"),
"Mafia Town - Steel Beam Nest": LocData(2000304608, "Mafia Town Area (HUMT)"),
"Mafia Town - Top of Ruined Tower": LocData(2000304607, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]),
"Mafia Town - Hot Air Balloon": LocData(2000304829, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]),
"Mafia Town - Camera Badge 1": LocData(2000302003, "Mafia Town Area (HUMT)"),
"Mafia Town - Camera Badge 2": LocData(2000302004, "Mafia Town Area (HUMT)"),
"Mafia Town - Chest Beneath Aqueduct": LocData(2000303489, "Mafia Town Area (HUMT)"),
"Mafia Town - Secret Cave": LocData(2000305220, "Mafia Town Area (HUMT)", required_hats=[HatType.BREWING]),
"Mafia Town - Crow Chest": LocData(2000303532, "Mafia Town Area (HUMT)"),
"Mafia Town - Above Boats": LocData(2000305218, "Mafia Town Area (HUMT)", hookshot=True),
"Mafia Town - Slip Slide Chest": LocData(2000303529, "Mafia Town Area (HUMT)"),
"Mafia Town - Behind Faucet": LocData(2000304214, "Mafia Town Area (HUMT)"),
"Mafia Town - Clock Tower Chest": LocData(2000303481, "Mafia Town Area (HUMT)", hookshot=True),
"Mafia Town - Top of Lighthouse": LocData(2000304213, "Mafia Town Area (HUMT)", hookshot=True),
"Mafia Town - Mafia Geek Platform": LocData(2000304212, "Mafia Town Area (HUMT)"),
"Mafia Town - Behind HQ Chest": LocData(2000303486, "Mafia Town Area (HUMT)"),
"Mafia HQ - Hallway Brewing Crate": LocData(2000305387, "Down with the Mafia!", required_hats=[HatType.BREWING]),
"Mafia HQ - Freezer Chest": LocData(2000303241, "Down with the Mafia!"),
"Mafia HQ - Secret Room": LocData(2000304979, "Down with the Mafia!", required_hats=[HatType.ICE]),
"Mafia HQ - Bathroom Stall Chest": LocData(2000303243, "Down with the Mafia!"),
"Dead Bird Studio - Up the Ladder": LocData(2000304874, "Dead Bird Studio - Elevator Area"),
"Dead Bird Studio - Red Building Top": LocData(2000305024, "Dead Bird Studio - Elevator Area"),
"Dead Bird Studio - Behind Water Tower": LocData(2000305248, "Dead Bird Studio - Elevator Area"),
"Dead Bird Studio - Side of House": LocData(2000305247, "Dead Bird Studio - Elevator Area"),
"Dead Bird Studio - DJ Grooves Sign Chest": LocData(2000303901, "Dead Bird Studio - Post Elevator Area",
hit_requirement=1),
"Dead Bird Studio - Tightrope Chest": LocData(2000303898, "Dead Bird Studio - Post Elevator Area",
hit_requirement=1),
"Dead Bird Studio - Tepee Chest": LocData(2000303899, "Dead Bird Studio - Post Elevator Area", hit_requirement=1),
"Dead Bird Studio - Conductor Chest": LocData(2000303900, "Dead Bird Studio - Post Elevator Area",
hit_requirement=1),
"Murder on the Owl Express - Cafeteria": LocData(2000305313, "Murder on the Owl Express"),
"Murder on the Owl Express - Luggage Room Top": LocData(2000305090, "Murder on the Owl Express"),
"Murder on the Owl Express - Luggage Room Bottom": LocData(2000305091, "Murder on the Owl Express"),
"Murder on the Owl Express - Raven Suite Room": LocData(2000305701, "Murder on the Owl Express",
required_hats=[HatType.BREWING]),
"Murder on the Owl Express - Raven Suite Top": LocData(2000305312, "Murder on the Owl Express"),
"Murder on the Owl Express - Lounge Chest": LocData(2000303963, "Murder on the Owl Express"),
"Picture Perfect - Behind Badge Seller": LocData(2000304307, "Picture Perfect"),
"Picture Perfect - Hats Buy Building": LocData(2000304530, "Picture Perfect"),
"Dead Bird Studio Basement - Window Platform": LocData(2000305432, "Dead Bird Studio Basement", hookshot=True),
"Dead Bird Studio Basement - Cardboard Conductor": LocData(2000305059, "Dead Bird Studio Basement", hookshot=True),
"Dead Bird Studio Basement - Above Conductor Sign": LocData(2000305057, "Dead Bird Studio Basement", hookshot=True),
"Dead Bird Studio Basement - Logo Wall": LocData(2000305207, "Dead Bird Studio Basement"),
"Dead Bird Studio Basement - Disco Room": LocData(2000305061, "Dead Bird Studio Basement", hookshot=True),
"Dead Bird Studio Basement - Small Room": LocData(2000304813, "Dead Bird Studio Basement"),
"Dead Bird Studio Basement - Vent Pipe": LocData(2000305430, "Dead Bird Studio Basement"),
"Dead Bird Studio Basement - Tightrope": LocData(2000305058, "Dead Bird Studio Basement", hookshot=True),
"Dead Bird Studio Basement - Cameras": LocData(2000305431, "Dead Bird Studio Basement", hookshot=True),
"Dead Bird Studio Basement - Locked Room": LocData(2000305819, "Dead Bird Studio Basement", hookshot=True),
# Subcon Forest
"Contractual Obligations - Cherry Bomb Bone Cage": LocData(2000324761, "Contractual Obligations"),
"Subcon Village - Tree Top Ice Cube": LocData(2000325078, "Subcon Forest Area"),
"Subcon Village - Graveyard Ice Cube": LocData(2000325077, "Subcon Forest Area"),
"Subcon Village - House Top": LocData(2000325471, "Subcon Forest Area"),
"Subcon Village - Ice Cube House": LocData(2000325469, "Subcon Forest Area"),
"Subcon Village - Snatcher Statue Chest": LocData(2000323730, "Subcon Forest Area", paintings=1),
"Subcon Village - Stump Platform Chest": LocData(2000323729, "Subcon Forest Area"),
"Subcon Forest - Giant Tree Climb": LocData(2000325470, "Subcon Forest Area"),
"Subcon Forest - Ice Cube Shack": LocData(2000324465, "Subcon Forest Area", paintings=1),
"Subcon Forest - Swamp Gravestone": LocData(2000326296, "Subcon Forest Area",
required_hats=[HatType.BREWING], paintings=1),
"Subcon Forest - Swamp Near Well": LocData(2000324762, "Subcon Forest Area", paintings=1),
"Subcon Forest - Swamp Tree A": LocData(2000324763, "Subcon Forest Area", paintings=1),
"Subcon Forest - Swamp Tree B": LocData(2000324764, "Subcon Forest Area", paintings=1),
"Subcon Forest - Swamp Ice Wall": LocData(2000324706, "Subcon Forest Area", paintings=1),
"Subcon Forest - Swamp Treehouse": LocData(2000325468, "Subcon Forest Area", paintings=1),
"Subcon Forest - Swamp Tree Chest": LocData(2000323728, "Subcon Forest Area", paintings=1),
"Subcon Forest - Burning House": LocData(2000324710, "Subcon Forest Area", paintings=2),
"Subcon Forest - Burning Tree Climb": LocData(2000325079, "Subcon Forest Area", paintings=2),
"Subcon Forest - Burning Stump Chest": LocData(2000323731, "Subcon Forest Area", paintings=2),
"Subcon Forest - Burning Forest Treehouse": LocData(2000325467, "Subcon Forest Area", paintings=2),
"Subcon Forest - Spider Bone Cage A": LocData(2000324462, "Subcon Forest Area", paintings=2),
"Subcon Forest - Spider Bone Cage B": LocData(2000325080, "Subcon Forest Area", paintings=2),
"Subcon Forest - Triple Spider Bounce": LocData(2000324765, "Subcon Forest Area", paintings=2),
"Subcon Forest - Noose Treehouse": LocData(2000324856, "Subcon Forest Area", hookshot=True, paintings=2),
"Subcon Forest - Long Tree Climb Chest": LocData(2000323734, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=2),
"Subcon Forest - Boss Arena Chest": LocData(2000323735, "Subcon Forest Area"),
"Subcon Forest - Manor Rooftop": LocData(2000325466, "Subcon Forest Area", hit_requirement=2, paintings=1),
"Subcon Forest - Infinite Yarn Bush": LocData(2000325478, "Subcon Forest Area",
required_hats=[HatType.BREWING], paintings=2),
"Subcon Forest - Magnet Badge Bush": LocData(2000325479, "Subcon Forest Area",
required_hats=[HatType.BREWING], paintings=3),
"Subcon Forest - Dweller Stump": LocData(2000324767, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=3),
"Subcon Forest - Dweller Floating Rocks": LocData(2000324464, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=3),
"Subcon Forest - Dweller Platforming Tree A": LocData(2000324709, "Subcon Forest Area", paintings=3),
"Subcon Forest - Dweller Platforming Tree B": LocData(2000324855, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=3),
"Subcon Forest - Giant Time Piece": LocData(2000325473, "Subcon Forest Area", paintings=3),
"Subcon Forest - Gallows": LocData(2000325472, "Subcon Forest Area", paintings=3),
"Subcon Forest - Green and Purple Dweller Rocks": LocData(2000325082, "Subcon Forest Area", paintings=3),
"Subcon Forest - Dweller Shack": LocData(2000324463, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=3),
"Subcon Forest - Tall Tree Hookshot Swing": LocData(2000324766, "Subcon Forest Area",
required_hats=[HatType.DWELLER],
hookshot=True,
paintings=3),
"Subcon Well - Hookshot Badge Chest": LocData(2000324114, "The Subcon Well", hit_requirement=1, paintings=1),
"Subcon Well - Above Chest": LocData(2000324612, "The Subcon Well", hit_requirement=1, paintings=1),
"Subcon Well - On Pipe": LocData(2000324311, "The Subcon Well", hookshot=True, hit_requirement=1, paintings=1),
"Subcon Well - Mushroom": LocData(2000325318, "The Subcon Well", hit_requirement=1, paintings=1),
"Queen Vanessa's Manor - Cellar": LocData(2000324841, "Queen Vanessa's Manor", hit_requirement=2, paintings=1),
"Queen Vanessa's Manor - Bedroom Chest": LocData(2000323808, "Queen Vanessa's Manor", hit_requirement=2,
paintings=1),
"Queen Vanessa's Manor - Hall Chest": LocData(2000323896, "Queen Vanessa's Manor", hit_requirement=2, paintings=1),
"Queen Vanessa's Manor - Chandelier": LocData(2000325546, "Queen Vanessa's Manor", hit_requirement=2, paintings=1),
# Alpine Skyline
"Alpine Skyline - Goat Village: Below Hookpoint": LocData(2000334856, "Alpine Skyline Area (TIHS)"),
"Alpine Skyline - Goat Village: Hidden Branch": LocData(2000334855, "Alpine Skyline Area (TIHS)"),
"Alpine Skyline - Goat Refinery": LocData(2000333635, "Alpine Skyline Area"),
"Alpine Skyline - Bird Pass Fork": LocData(2000335911, "Alpine Skyline Area (TIHS)"),
"Alpine Skyline - Yellow Band Hills": LocData(2000335756, "Alpine Skyline Area (TIHS)",
required_hats=[HatType.BREWING]),
"Alpine Skyline - The Purrloined Village: Horned Stone": LocData(2000335561, "Alpine Skyline Area"),
"Alpine Skyline - The Purrloined Village: Chest Reward": LocData(2000334831, "Alpine Skyline Area"),
"Alpine Skyline - The Birdhouse: Triple Crow Chest": LocData(2000334758, "The Birdhouse"),
"Alpine Skyline - The Birdhouse: Dweller Platforms Relic": LocData(2000336497, "The Birdhouse",
required_hats=[HatType.DWELLER]),
"Alpine Skyline - The Birdhouse: Brewing Crate House": LocData(2000336496, "The Birdhouse"),
"Alpine Skyline - The Birdhouse: Hay Bale": LocData(2000335885, "The Birdhouse"),
"Alpine Skyline - The Birdhouse: Alpine Crow Mini-Gauntlet": LocData(2000335886, "The Birdhouse"),
"Alpine Skyline - The Birdhouse: Outer Edge": LocData(2000335492, "The Birdhouse"),
"Alpine Skyline - Mystifying Time Mesa: Zipline": LocData(2000337058, "Alpine Skyline Area"),
"Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": LocData(2000336052, "Alpine Skyline Area"),
"Alpine Skyline - Ember Summit": LocData(2000336311, "Alpine Skyline Area (TIHS)"),
"Alpine Skyline - The Lava Cake: Center Fence Cage": LocData(2000335448, "The Lava Cake"),
"Alpine Skyline - The Lava Cake: Outer Island Chest": LocData(2000334291, "The Lava Cake"),
"Alpine Skyline - The Lava Cake: Dweller Pillars": LocData(2000335417, "The Lava Cake"),
"Alpine Skyline - The Lava Cake: Top Cake": LocData(2000335418, "The Lava Cake"),
"Alpine Skyline - The Twilight Path": LocData(2000334434, "Alpine Skyline Area", required_hats=[HatType.DWELLER]),
"Alpine Skyline - The Twilight Bell: Wide Purple Platform": LocData(2000336478, "The Twilight Bell"),
"Alpine Skyline - The Twilight Bell: Ice Platform": LocData(2000335826, "The Twilight Bell"),
"Alpine Skyline - Goat Outpost Horn": LocData(2000334760, "Alpine Skyline Area"),
"Alpine Skyline - Windy Passage": LocData(2000334776, "Alpine Skyline Area (TIHS)"),
"Alpine Skyline - The Windmill: Inside Pon Cluster": LocData(2000336395, "The Windmill"),
"Alpine Skyline - The Windmill: Entrance": LocData(2000335783, "The Windmill"),
"Alpine Skyline - The Windmill: Dropdown": LocData(2000335815, "The Windmill"),
"Alpine Skyline - The Windmill: House Window": LocData(2000335389, "The Windmill"),
"The Finale - Frozen Item": LocData(2000304108, "The Finale"),
"Bon Voyage! - Lamp Post Top": LocData(2000305321, "Bon Voyage!", dlc_flags=HatDLC.dlc1),
"Bon Voyage! - Mafia Cargo Ship": LocData(2000304313, "Bon Voyage!", dlc_flags=HatDLC.dlc1),
"The Arctic Cruise - Toilet": LocData(2000305109, "Cruise Ship", dlc_flags=HatDLC.dlc1),
"The Arctic Cruise - Bar": LocData(2000304251, "Cruise Ship", dlc_flags=HatDLC.dlc1),
"The Arctic Cruise - Dive Board Ledge": LocData(2000304254, "Cruise Ship", dlc_flags=HatDLC.dlc1),
"The Arctic Cruise - Top Balcony": LocData(2000304255, "Cruise Ship", dlc_flags=HatDLC.dlc1),
"The Arctic Cruise - Octopus Room": LocData(2000305253, "Cruise Ship", dlc_flags=HatDLC.dlc1),
"The Arctic Cruise - Octopus Room Top": LocData(2000304249, "Cruise Ship", dlc_flags=HatDLC.dlc1),
"The Arctic Cruise - Laundry Room": LocData(2000304250, "Cruise Ship", dlc_flags=HatDLC.dlc1),
"The Arctic Cruise - Ship Side": LocData(2000304247, "Cruise Ship", dlc_flags=HatDLC.dlc1),
"The Arctic Cruise - Silver Ring": LocData(2000305252, "Cruise Ship", dlc_flags=HatDLC.dlc1),
"Rock the Boat - Reception Room - Suitcase": LocData(2000304045, "Rock the Boat", dlc_flags=HatDLC.dlc1),
"Rock the Boat - Reception Room - Under Desk": LocData(2000304047, "Rock the Boat", dlc_flags=HatDLC.dlc1),
"Rock the Boat - Lamp Post": LocData(2000304048, "Rock the Boat", dlc_flags=HatDLC.dlc1),
"Rock the Boat - Iceberg Top": LocData(2000304046, "Rock the Boat", dlc_flags=HatDLC.dlc1),
"Rock the Boat - Post Captain Rescue": LocData(2000304049, "Rock the Boat", dlc_flags=HatDLC.dlc1,
required_hats=[HatType.ICE]),
"Nyakuza Metro - Main Station Dining Area": LocData(2000304105, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2),
"Nyakuza Metro - Top of Ramen Shop": LocData(2000304104, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2),
"Yellow Overpass Station - Brewing Crate": LocData(2000305413, "Yellow Overpass Station",
dlc_flags=HatDLC.dlc2,
required_hats=[HatType.BREWING]),
"Bluefin Tunnel - Cat Vacuum": LocData(2000305111, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2),
"Pink Paw Station - Cat Vacuum": LocData(2000305110, "Pink Paw Station",
dlc_flags=HatDLC.dlc2,
hookshot=True,
required_hats=[HatType.DWELLER]),
"Pink Paw Station - Behind Fan": LocData(2000304106, "Pink Paw Station",
dlc_flags=HatDLC.dlc2,
hookshot=True,
required_hats=[HatType.TIME_STOP, HatType.DWELLER]),
}
act_completions = {
"Act Completion (Time Rift - Gallery)": LocData(2000312758, "Time Rift - Gallery", required_hats=[HatType.BREWING]),
"Act Completion (Time Rift - The Lab)": LocData(2000312838, "Time Rift - The Lab"),
"Act Completion (Welcome to Mafia Town)": LocData(2000311771, "Welcome to Mafia Town"),
"Act Completion (Barrel Battle)": LocData(2000311958, "Barrel Battle"),
"Act Completion (She Came from Outer Space)": LocData(2000312262, "She Came from Outer Space"),
"Act Completion (Down with the Mafia!)": LocData(2000311326, "Down with the Mafia!"),
"Act Completion (Cheating the Race)": LocData(2000312318, "Cheating the Race", required_hats=[HatType.TIME_STOP]),
"Act Completion (Heating Up Mafia Town)": LocData(2000311481, "Heating Up Mafia Town", umbrella=True),
"Act Completion (The Golden Vault)": LocData(2000312250, "The Golden Vault"),
"Act Completion (Time Rift - Bazaar)": LocData(2000312465, "Time Rift - Bazaar"),
"Act Completion (Time Rift - Sewers)": LocData(2000312484, "Time Rift - Sewers"),
"Act Completion (Time Rift - Mafia of Cooks)": LocData(2000311855, "Time Rift - Mafia of Cooks"),
"Act Completion (Dead Bird Studio)": LocData(2000311383, "Dead Bird Studio", hit_requirement=1),
"Act Completion (Murder on the Owl Express)": LocData(2000311544, "Murder on the Owl Express"),
"Act Completion (Picture Perfect)": LocData(2000311587, "Picture Perfect"),
"Act Completion (Train Rush)": LocData(2000312481, "Train Rush", hookshot=True),
"Act Completion (The Big Parade)": LocData(2000311157, "The Big Parade", umbrella=True),
"Act Completion (Award Ceremony)": LocData(2000311488, "Award Ceremony"),
"Act Completion (Dead Bird Studio Basement)": LocData(2000312253, "Dead Bird Studio Basement", hookshot=True),
"Act Completion (Time Rift - The Owl Express)": LocData(2000312807, "Time Rift - The Owl Express"),
"Act Completion (Time Rift - The Moon)": LocData(2000312785, "Time Rift - The Moon"),
"Act Completion (Time Rift - Dead Bird Studio)": LocData(2000312577, "Time Rift - Dead Bird Studio"),
"Act Completion (Contractual Obligations)": LocData(2000312317, "Contractual Obligations", paintings=1),
"Act Completion (The Subcon Well)": LocData(2000311160, "The Subcon Well", hookshot=True, hit_requirement=1,
paintings=1),
"Act Completion (Toilet of Doom)": LocData(2000311984, "Toilet of Doom", hit_requirement=1, hookshot=True,
paintings=1),
"Act Completion (Queen Vanessa's Manor)": LocData(2000312017, "Queen Vanessa's Manor", umbrella=True, paintings=1),
"Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service",
required_hats=[HatType.SPRINT]),
"Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired", umbrella=True),
"Act Completion (Time Rift - Pipe)": LocData(2000313069, "Time Rift - Pipe", hookshot=True),
"Act Completion (Time Rift - Village)": LocData(2000313056, "Time Rift - Village"),
"Act Completion (Time Rift - Sleepy Subcon)": LocData(2000312086, "Time Rift - Sleepy Subcon"),
"Act Completion (The Birdhouse)": LocData(2000311428, "The Birdhouse"),
"Act Completion (The Lava Cake)": LocData(2000312509, "The Lava Cake"),
"Act Completion (The Twilight Bell)": LocData(2000311540, "The Twilight Bell"),
"Act Completion (The Windmill)": LocData(2000312263, "The Windmill"),
"Act Completion (The Illness has Spread)": LocData(2000312022, "The Illness has Spread", hookshot=True),
"Act Completion (Time Rift - The Twilight Bell)": LocData(2000312399, "Time Rift - The Twilight Bell",
required_hats=[HatType.DWELLER]),
"Act Completion (Time Rift - Curly Tail Trail)": LocData(2000313335, "Time Rift - Curly Tail Trail",
required_hats=[HatType.ICE]),
"Act Completion (Time Rift - Alpine Skyline)": LocData(2000311777, "Time Rift - Alpine Skyline"),
"Act Completion (The Finale)": LocData(2000311872, "The Finale", hookshot=True, required_hats=[HatType.DWELLER]),
"Act Completion (Time Rift - Tour)": LocData(2000311803, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
"Act Completion (Bon Voyage!)": LocData(2000311520, "Bon Voyage!", dlc_flags=HatDLC.dlc1, hookshot=True),
"Act Completion (Ship Shape)": LocData(2000311451, "Ship Shape", dlc_flags=HatDLC.dlc1),
"Act Completion (Rock the Boat)": LocData(2000311437, "Rock the Boat", dlc_flags=HatDLC.dlc1,
required_hats=[HatType.ICE]),
"Act Completion (Time Rift - Balcony)": LocData(2000312226, "Time Rift - Balcony", dlc_flags=HatDLC.dlc1,
hookshot=True),
"Act Completion (Time Rift - Deep Sea)": LocData(2000312434, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1,
hookshot=True, required_hats=[HatType.DWELLER, HatType.ICE]),
"Act Completion (Nyakuza Metro Intro)": LocData(2000311138, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2),
"Act Completion (Yellow Overpass Station)": LocData(2000311206, "Yellow Overpass Station",
dlc_flags=HatDLC.dlc2,
hookshot=True),
"Act Completion (Yellow Overpass Manhole)": LocData(2000311387, "Yellow Overpass Manhole",
dlc_flags=HatDLC.dlc2,
required_hats=[HatType.ICE]),
"Act Completion (Green Clean Station)": LocData(2000311207, "Green Clean Station", dlc_flags=HatDLC.dlc2),
"Act Completion (Green Clean Manhole)": LocData(2000311388, "Green Clean Manhole",
dlc_flags=HatDLC.dlc2,
required_hats=[HatType.ICE, HatType.DWELLER]),
"Act Completion (Bluefin Tunnel)": LocData(2000311208, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2),
"Act Completion (Pink Paw Station)": LocData(2000311209, "Pink Paw Station",
dlc_flags=HatDLC.dlc2,
hookshot=True,
required_hats=[HatType.DWELLER]),
"Act Completion (Pink Paw Manhole)": LocData(2000311389, "Pink Paw Manhole",
dlc_flags=HatDLC.dlc2,
required_hats=[HatType.ICE]),
"Act Completion (Rush Hour)": LocData(2000311210, "Rush Hour",
dlc_flags=HatDLC.dlc2,
hookshot=True,
required_hats=[HatType.ICE, HatType.BREWING]),
"Act Completion (Time Rift - Rumbi Factory)": LocData(2000312736, "Time Rift - Rumbi Factory",
dlc_flags=HatDLC.dlc2),
}
storybook_pages = {
"Mafia of Cooks - Page: Fish Pile": LocData(2000345091, "Time Rift - Mafia of Cooks"),
"Mafia of Cooks - Page: Trash Mound": LocData(2000345090, "Time Rift - Mafia of Cooks"),
"Mafia of Cooks - Page: Beside Red Building": LocData(2000345092, "Time Rift - Mafia of Cooks"),
"Mafia of Cooks - Page: Behind Shipping Containers": LocData(2000345095, "Time Rift - Mafia of Cooks"),
"Mafia of Cooks - Page: Top of Boat": LocData(2000345093, "Time Rift - Mafia of Cooks"),
"Mafia of Cooks - Page: Below Dock": LocData(2000345094, "Time Rift - Mafia of Cooks"),
"Dead Bird Studio (Rift) - Page: Behind Cardboard Planet": LocData(2000345449, "Time Rift - Dead Bird Studio"),
"Dead Bird Studio (Rift) - Page: Near Time Rift Gate": LocData(2000345447, "Time Rift - Dead Bird Studio"),
"Dead Bird Studio (Rift) - Page: Top of Metal Bar": LocData(2000345448, "Time Rift - Dead Bird Studio"),
"Dead Bird Studio (Rift) - Page: Lava Lamp": LocData(2000345450, "Time Rift - Dead Bird Studio"),
"Dead Bird Studio (Rift) - Page: Above Horse Picture": LocData(2000345451, "Time Rift - Dead Bird Studio"),
"Dead Bird Studio (Rift) - Page: Green Screen": LocData(2000345452, "Time Rift - Dead Bird Studio"),
"Dead Bird Studio (Rift) - Page: In The Corner": LocData(2000345453, "Time Rift - Dead Bird Studio"),
"Dead Bird Studio (Rift) - Page: Above TV Room": LocData(2000345445, "Time Rift - Dead Bird Studio"),
"Sleepy Subcon - Page: Behind Entrance Area": LocData(2000345373, "Time Rift - Sleepy Subcon"),
"Sleepy Subcon - Page: Near Wrecking Ball": LocData(2000345327, "Time Rift - Sleepy Subcon"),
"Sleepy Subcon - Page: Behind Crane": LocData(2000345371, "Time Rift - Sleepy Subcon"),
"Sleepy Subcon - Page: Wrecked Treehouse": LocData(2000345326, "Time Rift - Sleepy Subcon"),
"Sleepy Subcon - Page: Behind 2nd Rift Gate": LocData(2000345372, "Time Rift - Sleepy Subcon"),
"Sleepy Subcon - Page: Rotating Platform": LocData(2000345328, "Time Rift - Sleepy Subcon"),
"Sleepy Subcon - Page: Behind 3rd Rift Gate": LocData(2000345329, "Time Rift - Sleepy Subcon"),
"Sleepy Subcon - Page: Frozen Tree": LocData(2000345330, "Time Rift - Sleepy Subcon"),
"Sleepy Subcon - Page: Secret Library": LocData(2000345370, "Time Rift - Sleepy Subcon"),
"Alpine Skyline (Rift) - Page: Entrance Area Hidden Ledge": LocData(2000345016, "Time Rift - Alpine Skyline"),
"Alpine Skyline (Rift) - Page: Windmill Island Ledge": LocData(2000345012, "Time Rift - Alpine Skyline"),
"Alpine Skyline (Rift) - Page: Waterfall Wooden Pillar": LocData(2000345015, "Time Rift - Alpine Skyline"),
"Alpine Skyline (Rift) - Page: Lonely Birdhouse Top": LocData(2000345014, "Time Rift - Alpine Skyline"),
"Alpine Skyline (Rift) - Page: Below Aqueduct": LocData(2000345013, "Time Rift - Alpine Skyline"),
"Deep Sea - Page: Starfish": LocData(2000346454, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1),
"Deep Sea - Page: Mini Castle": LocData(2000346452, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1),
"Deep Sea - Page: Urchins": LocData(2000346449, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1),
"Deep Sea - Page: Big Castle": LocData(2000346450, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1,
hookshot=True),
"Deep Sea - Page: Castle Top Chest": LocData(2000304850, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1,
hookshot=True),
"Deep Sea - Page: Urchin Ledge": LocData(2000346451, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1,
hookshot=True),
"Deep Sea - Page: Hidden Castle Chest": LocData(2000304849, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1,
hookshot=True),
"Deep Sea - Page: Falling Platform": LocData(2000346456, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1,
hookshot=True),
"Deep Sea - Page: Lava Starfish": LocData(2000346453, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1,
hookshot=True),
"Tour - Page: Mafia Town - Ledge": LocData(2000345038, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
"Tour - Page: Mafia Town - Beach": LocData(2000345039, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
"Tour - Page: Dead Bird Studio - C.A.W. Agents": LocData(2000345040, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
"Tour - Page: Dead Bird Studio - Fragile Box": LocData(2000345041, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
"Tour - Page: Subcon Forest - Giant Frozen Tree": LocData(2000345042, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
"Tour - Page: Subcon Forest - Top of Pillar": LocData(2000345043, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
"Tour - Page: Alpine Skyline - Birdhouse": LocData(2000345044, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
"Tour - Page: Alpine Skyline - Behind Lava Isle": LocData(2000345047, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
"Tour - Page: The Finale - Near Entrance": LocData(2000345087, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
"Rumbi Factory - Page: Manhole": LocData(2000345891, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
"Rumbi Factory - Page: Shutter Doors": LocData(2000345888, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
"Rumbi Factory - Page: Toxic Waste Dispenser": LocData(2000345892, "Time Rift - Rumbi Factory",
dlc_flags=HatDLC.dlc2),
"Rumbi Factory - Page: 3rd Area Ledge": LocData(2000345889, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
"Rumbi Factory - Page: Green Box Assembly Line": LocData(2000345884, "Time Rift - Rumbi Factory",
dlc_flags=HatDLC.dlc2),
"Rumbi Factory - Page: Broken Window": LocData(2000345885, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
"Rumbi Factory - Page: Money Vault": LocData(2000345890, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
"Rumbi Factory - Page: Warehouse Boxes": LocData(2000345887, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
"Rumbi Factory - Page: Glass Shelf": LocData(2000345886, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
"Rumbi Factory - Page: Last Area": LocData(2000345883, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
}
shop_locations = {
"Badge Seller - Item 1": LocData(2000301003, "Badge Seller"),
"Badge Seller - Item 2": LocData(2000301004, "Badge Seller"),
"Badge Seller - Item 3": LocData(2000301005, "Badge Seller"),
"Badge Seller - Item 4": LocData(2000301006, "Badge Seller"),
"Badge Seller - Item 5": LocData(2000301007, "Badge Seller"),
"Badge Seller - Item 6": LocData(2000301008, "Badge Seller"),
"Badge Seller - Item 7": LocData(2000301009, "Badge Seller"),
"Badge Seller - Item 8": LocData(2000301010, "Badge Seller"),
"Badge Seller - Item 9": LocData(2000301011, "Badge Seller"),
"Badge Seller - Item 10": LocData(2000301012, "Badge Seller"),
"Mafia Boss Shop Item": LocData(2000301013, "Spaceship"),
"Yellow Overpass Station - Yellow Ticket Booth": LocData(2000301014, "Yellow Overpass Station",
dlc_flags=HatDLC.dlc2),
"Green Clean Station - Green Ticket Booth": LocData(2000301015, "Green Clean Station", dlc_flags=HatDLC.dlc2),
"Bluefin Tunnel - Blue Ticket Booth": LocData(2000301016, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2),
"Pink Paw Station - Pink Ticket Booth": LocData(2000301017, "Pink Paw Station", dlc_flags=HatDLC.dlc2,
hookshot=True, required_hats=[HatType.DWELLER]),
"Main Station Thug A - Item 1": LocData(2000301048, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_0"),
"Main Station Thug A - Item 2": LocData(2000301049, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_0"),
"Main Station Thug A - Item 3": LocData(2000301050, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_0"),
"Main Station Thug A - Item 4": LocData(2000301051, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_0"),
"Main Station Thug A - Item 5": LocData(2000301052, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_0"),
"Main Station Thug B - Item 1": LocData(2000301053, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_1"),
"Main Station Thug B - Item 2": LocData(2000301054, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_1"),
"Main Station Thug B - Item 3": LocData(2000301055, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_1"),
"Main Station Thug B - Item 4": LocData(2000301056, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_1"),
"Main Station Thug B - Item 5": LocData(2000301057, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_1"),
"Main Station Thug C - Item 1": LocData(2000301058, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_2"),
"Main Station Thug C - Item 2": LocData(2000301059, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_2"),
"Main Station Thug C - Item 3": LocData(2000301060, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_2"),
"Main Station Thug C - Item 4": LocData(2000301061, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_2"),
"Main Station Thug C - Item 5": LocData(2000301062, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_2"),
"Yellow Overpass Thug A - Item 1": LocData(2000301018, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_13"),
"Yellow Overpass Thug A - Item 2": LocData(2000301019, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_13"),
"Yellow Overpass Thug A - Item 3": LocData(2000301020, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_13"),
"Yellow Overpass Thug A - Item 4": LocData(2000301021, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_13"),
"Yellow Overpass Thug A - Item 5": LocData(2000301022, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_13"),
"Yellow Overpass Thug B - Item 1": LocData(2000301043, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_5"),
"Yellow Overpass Thug B - Item 2": LocData(2000301044, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_5"),
"Yellow Overpass Thug B - Item 3": LocData(2000301045, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_5"),
"Yellow Overpass Thug B - Item 4": LocData(2000301046, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_5"),
"Yellow Overpass Thug B - Item 5": LocData(2000301047, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_5"),
"Yellow Overpass Thug C - Item 1": LocData(2000301063, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_14"),
"Yellow Overpass Thug C - Item 2": LocData(2000301064, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_14"),
"Yellow Overpass Thug C - Item 3": LocData(2000301065, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_14"),
"Yellow Overpass Thug C - Item 4": LocData(2000301066, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_14"),
"Yellow Overpass Thug C - Item 5": LocData(2000301067, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_14"),
"Green Clean Station Thug A - Item 1": LocData(2000301033, "Green Clean Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_4"),
"Green Clean Station Thug A - Item 2": LocData(2000301034, "Green Clean Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_4"),
"Green Clean Station Thug A - Item 3": LocData(2000301035, "Green Clean Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_4"),
"Green Clean Station Thug A - Item 4": LocData(2000301036, "Green Clean Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_4"),
"Green Clean Station Thug A - Item 5": LocData(2000301037, "Green Clean Station", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_4"),
# This guy requires either the yellow ticket or the Ice Hat
"Green Clean Station Thug B - Item 1": LocData(2000301028, "Green Clean Station", dlc_flags=HatDLC.dlc2,
required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"),
"Green Clean Station Thug B - Item 2": LocData(2000301029, "Green Clean Station", dlc_flags=HatDLC.dlc2,
required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"),
"Green Clean Station Thug B - Item 3": LocData(2000301030, "Green Clean Station", dlc_flags=HatDLC.dlc2,
required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"),
"Green Clean Station Thug B - Item 4": LocData(2000301031, "Green Clean Station", dlc_flags=HatDLC.dlc2,
required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"),
"Green Clean Station Thug B - Item 5": LocData(2000301032, "Green Clean Station", dlc_flags=HatDLC.dlc2,
required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"),
"Bluefin Tunnel Thug - Item 1": LocData(2000301023, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_7"),
"Bluefin Tunnel Thug - Item 2": LocData(2000301024, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_7"),
"Bluefin Tunnel Thug - Item 3": LocData(2000301025, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_7"),
"Bluefin Tunnel Thug - Item 4": LocData(2000301026, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_7"),
"Bluefin Tunnel Thug - Item 5": LocData(2000301027, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2,
nyakuza_thug="Hat_NPC_NyakuzaShop_7"),
"Pink Paw Station Thug - Item 1": LocData(2000301038, "Pink Paw Station", dlc_flags=HatDLC.dlc2,
required_hats=[HatType.DWELLER], hookshot=True,
nyakuza_thug="Hat_NPC_NyakuzaShop_12"),
"Pink Paw Station Thug - Item 2": LocData(2000301039, "Pink Paw Station", dlc_flags=HatDLC.dlc2,
required_hats=[HatType.DWELLER], hookshot=True,
nyakuza_thug="Hat_NPC_NyakuzaShop_12"),
"Pink Paw Station Thug - Item 3": LocData(2000301040, "Pink Paw Station", dlc_flags=HatDLC.dlc2,
required_hats=[HatType.DWELLER], hookshot=True,
nyakuza_thug="Hat_NPC_NyakuzaShop_12"),
"Pink Paw Station Thug - Item 4": LocData(2000301041, "Pink Paw Station", dlc_flags=HatDLC.dlc2,
required_hats=[HatType.DWELLER], hookshot=True,
nyakuza_thug="Hat_NPC_NyakuzaShop_12"),
"Pink Paw Station Thug - Item 5": LocData(2000301042, "Pink Paw Station", dlc_flags=HatDLC.dlc2,
required_hats=[HatType.DWELLER], hookshot=True,
nyakuza_thug="Hat_NPC_NyakuzaShop_12"),
}
contract_locations = {
"Snatcher's Contract - The Subcon Well": LocData(2000300200, "Contractual Obligations"),
"Snatcher's Contract - Toilet of Doom": LocData(2000300201, "Subcon Forest Area"),
"Snatcher's Contract - Queen Vanessa's Manor": LocData(2000300202, "Subcon Forest Area"),
"Snatcher's Contract - Mail Delivery Service": LocData(2000300203, "Subcon Forest Area"),
}
# Don't put any of the locations from peaks here, the rules for their entrances are set already
zipline_unlocks = {
"Alpine Skyline - Bird Pass Fork": "Zipline Unlock - The Birdhouse Path",
"Alpine Skyline - Yellow Band Hills": "Zipline Unlock - The Birdhouse Path",
"Alpine Skyline - The Purrloined Village: Horned Stone": "Zipline Unlock - The Birdhouse Path",
"Alpine Skyline - The Purrloined Village: Chest Reward": "Zipline Unlock - The Birdhouse Path",
"Alpine Skyline - Mystifying Time Mesa: Zipline": "Zipline Unlock - The Lava Cake Path",
"Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": "Zipline Unlock - The Lava Cake Path",
"Alpine Skyline - Ember Summit": "Zipline Unlock - The Lava Cake Path",
"Alpine Skyline - Goat Outpost Horn": "Zipline Unlock - The Windmill Path",
"Alpine Skyline - Windy Passage": "Zipline Unlock - The Windmill Path",
"Alpine Skyline - The Twilight Path": "Zipline Unlock - The Twilight Bell Path",
}
# act completion rules should be set automatically as these are all event items
zero_jumps_hard = {
"Time Rift - Sewers (Zero Jumps)": LocData(0, "Time Rift - Sewers",
required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish),
"Time Rift - Bazaar (Zero Jumps)": LocData(0, "Time Rift - Bazaar",
required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish),
"The Big Parade": LocData(0, "The Big Parade",
umbrella=True,
required_hats=[HatType.ICE],
dlc_flags=HatDLC.death_wish),
"Time Rift - Pipe (Zero Jumps)": LocData(0, "Time Rift - Pipe", hookshot=True, dlc_flags=HatDLC.death_wish),
"Time Rift - Curly Tail Trail (Zero Jumps)": LocData(0, "Time Rift - Curly Tail Trail",
required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish),
"Time Rift - The Twilight Bell (Zero Jumps)": LocData(0, "Time Rift - The Twilight Bell",
required_hats=[HatType.ICE, HatType.DWELLER],
hit_requirement=1,
dlc_flags=HatDLC.death_wish),
"The Illness has Spread (Zero Jumps)": LocData(0, "The Illness has Spread",
required_hats=[HatType.ICE], hookshot=True,
hit_requirement=1, dlc_flags=HatDLC.death_wish),
"The Finale (Zero Jumps)": LocData(0, "The Finale",
required_hats=[HatType.ICE, HatType.DWELLER],
hookshot=True,
dlc_flags=HatDLC.death_wish),
"Pink Paw Station (Zero Jumps)": LocData(0, "Pink Paw Station",
required_hats=[HatType.ICE],
hookshot=True,
dlc_flags=HatDLC.dlc2_dw),
}
zero_jumps_expert = {
"The Birdhouse (Zero Jumps)": LocData(0, "The Birdhouse",
required_hats=[HatType.ICE],
dlc_flags=HatDLC.death_wish),
"The Lava Cake (Zero Jumps)": LocData(0, "The Lava Cake", dlc_flags=HatDLC.death_wish),
"The Windmill (Zero Jumps)": LocData(0, "The Windmill",
required_hats=[HatType.ICE],
misc_required=["No Bonk Badge"],
dlc_flags=HatDLC.death_wish),
"The Twilight Bell (Zero Jumps)": LocData(0, "The Twilight Bell",
required_hats=[HatType.ICE, HatType.DWELLER],
hit_requirement=1,
misc_required=["No Bonk Badge"],
dlc_flags=HatDLC.death_wish),
"Sleepy Subcon (Zero Jumps)": LocData(0, "Time Rift - Sleepy Subcon", required_hats=[HatType.ICE],
dlc_flags=HatDLC.death_wish),
"Ship Shape (Zero Jumps)": LocData(0, "Ship Shape", required_hats=[HatType.ICE], dlc_flags=HatDLC.dlc1_dw),
}
zero_jumps = {
**zero_jumps_hard,
**zero_jumps_expert,
"Welcome to Mafia Town (Zero Jumps)": LocData(0, "Welcome to Mafia Town", dlc_flags=HatDLC.death_wish),
"Down with the Mafia! (Zero Jumps)": LocData(0, "Down with the Mafia!",
required_hats=[HatType.ICE],
dlc_flags=HatDLC.death_wish),
"Cheating the Race (Zero Jumps)": LocData(0, "Cheating the Race",
required_hats=[HatType.TIME_STOP],
dlc_flags=HatDLC.death_wish),
"The Golden Vault (Zero Jumps)": LocData(0, "The Golden Vault",
required_hats=[HatType.ICE],
dlc_flags=HatDLC.death_wish),
"Dead Bird Studio (Zero Jumps)": LocData(0, "Dead Bird Studio",
required_hats=[HatType.ICE],
hit_requirement=1,
dlc_flags=HatDLC.death_wish),
"Murder on the Owl Express (Zero Jumps)": LocData(0, "Murder on the Owl Express",
required_hats=[HatType.ICE],
dlc_flags=HatDLC.death_wish),
"Picture Perfect (Zero Jumps)": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish),
"Train Rush (Zero Jumps)": LocData(0, "Train Rush",
required_hats=[HatType.ICE],
hookshot=True,
dlc_flags=HatDLC.death_wish),
"Contractual Obligations (Zero Jumps)": LocData(0, "Contractual Obligations",
paintings=1,
dlc_flags=HatDLC.death_wish),
"Your Contract has Expired (Zero Jumps)": LocData(0, "Your Contract has Expired",
umbrella=True,
dlc_flags=HatDLC.death_wish),
# No ice hat/painting required in Expert
"Toilet of Doom (Zero Jumps)": LocData(0, "Toilet of Doom",
hookshot=True,
hit_requirement=1,
required_hats=[HatType.ICE],
paintings=1,
dlc_flags=HatDLC.death_wish),
"Mail Delivery Service (Zero Jumps)": LocData(0, "Mail Delivery Service",
required_hats=[HatType.SPRINT],
dlc_flags=HatDLC.death_wish),
"Time Rift - Alpine Skyline (Zero Jumps)": LocData(0, "Time Rift - Alpine Skyline",
required_hats=[HatType.ICE],
hookshot=True,
dlc_flags=HatDLC.death_wish),
"Time Rift - The Lab (Zero Jumps)": LocData(0, "Time Rift - The Lab",
required_hats=[HatType.ICE],
dlc_flags=HatDLC.death_wish),
"Yellow Overpass Station (Zero Jumps)": LocData(0, "Yellow Overpass Station",
required_hats=[HatType.ICE],
hookshot=True,
dlc_flags=HatDLC.dlc2_dw),
"Green Clean Station (Zero Jumps)": LocData(0, "Green Clean Station",
required_hats=[HatType.ICE],
dlc_flags=HatDLC.dlc2_dw),
}
# noinspection PyDictDuplicateKeys
snatcher_coins = {
"Snatcher Coin - Top of HQ": LocData(0, "Down with the Mafia!", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Top of HQ": LocData(0, "Cheating the Race", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Top of HQ": LocData(0, "Heating Up Mafia Town", umbrella=True, dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Top of HQ": LocData(0, "The Golden Vault", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Top of HQ": LocData(0, "Beat the Heat", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Top of Tower": LocData(0, "Mafia Town Area (HUMT)", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Top of Tower": LocData(0, "Beat the Heat", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Top of Tower": LocData(0, "Collect-a-thon", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Top of Tower": LocData(0, "She Speedran from Outer Space", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Top of Tower": LocData(0, "Mafia's Jumps", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Under Ruined Tower": LocData(0, "Mafia Town Area", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Under Ruined Tower": LocData(0, "Collect-a-thon", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Under Ruined Tower": LocData(0, "She Speedran from Outer Space", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Top of Red House": LocData(0, "Dead Bird Studio - Elevator Area", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Top of Red House": LocData(0, "Security Breach", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Train Rush": LocData(0, "Train Rush", hookshot=True, dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Train Rush": LocData(0, "10 Seconds until Self-Destruct", hookshot=True,
dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Picture Perfect": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Swamp Tree": LocData(0, "Subcon Forest Area", hookshot=True, paintings=1,
dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Swamp Tree": LocData(0, "Speedrun Well", hookshot=True, dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Manor Roof": LocData(0, "Subcon Forest Area", hit_requirement=2, paintings=1,
dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Giant Time Piece": LocData(0, "Subcon Forest Area", paintings=3, dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Goat Village Top": LocData(0, "Alpine Skyline Area (TIHS)", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Goat Village Top": LocData(0, "The Illness has Speedrun", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Lava Cake": LocData(0, "The Lava Cake", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Windmill": LocData(0, "The Windmill", dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Windmill": LocData(0, "Wound-Up Windmill", hookshot=True, dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Green Clean Tower": LocData(0, "Green Clean Station", dlc_flags=HatDLC.dlc2_dw),
"Snatcher Coin - Bluefin Cat Train": LocData(0, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2_dw),
"Snatcher Coin - Pink Paw Fence": LocData(0, "Pink Paw Station", dlc_flags=HatDLC.dlc2_dw),
}
event_locs = {
**zero_jumps,
**snatcher_coins,
"HUMT Access": LocData(0, "Heating Up Mafia Town"),
"TOD Access": LocData(0, "Toilet of Doom"),
"YCHE Access": LocData(0, "Your Contract has Expired"),
"Birdhouse Cleared": LocData(0, "The Birdhouse", act_event=True),
"Lava Cake Cleared": LocData(0, "The Lava Cake", act_event=True),
"Windmill Cleared": LocData(0, "The Windmill", act_event=True),
"Twilight Bell Cleared": LocData(0, "The Twilight Bell", act_event=True),
"Time Piece Cluster": LocData(0, "The Finale", act_event=True),
# not really an act
"Nyakuza Intro Cleared": LocData(0, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2),
"Yellow Overpass Station Cleared": LocData(0, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, act_event=True),
"Green Clean Station Cleared": LocData(0, "Green Clean Station", dlc_flags=HatDLC.dlc2, act_event=True),
"Bluefin Tunnel Cleared": LocData(0, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, act_event=True),
"Pink Paw Station Cleared": LocData(0, "Pink Paw Station", dlc_flags=HatDLC.dlc2, act_event=True),
"Yellow Overpass Manhole Cleared": LocData(0, "Yellow Overpass Manhole", dlc_flags=HatDLC.dlc2, act_event=True),
"Green Clean Manhole Cleared": LocData(0, "Green Clean Manhole", dlc_flags=HatDLC.dlc2, act_event=True),
"Pink Paw Manhole Cleared": LocData(0, "Pink Paw Manhole", dlc_flags=HatDLC.dlc2, act_event=True),
"Rush Hour Cleared": LocData(0, "Rush Hour", dlc_flags=HatDLC.dlc2, act_event=True),
}
# DO NOT ALTER THE ORDER OF THIS LIST
death_wishes = {
"Beat the Heat": 2000350000,
"Snatcher's Hit List": 2000350002,
"So You're Back From Outer Space": 2000350004,
"Collect-a-thon": 2000350006,
"Rift Collapse: Mafia of Cooks": 2000350008,
"She Speedran from Outer Space": 2000350010,
"Mafia's Jumps": 2000350012,
"Vault Codes in the Wind": 2000350014,
"Encore! Encore!": 2000350016,
"Snatcher Coins in Mafia Town": 2000350018,
"Security Breach": 2000350020,
"The Great Big Hootenanny": 2000350022,
"Rift Collapse: Dead Bird Studio": 2000350024,
"10 Seconds until Self-Destruct": 2000350026,
"Killing Two Birds": 2000350028,
"Snatcher Coins in Battle of the Birds": 2000350030,
"Zero Jumps": 2000350032,
"Speedrun Well": 2000350034,
"Rift Collapse: Sleepy Subcon": 2000350036,
"Boss Rush": 2000350038,
"Quality Time with Snatcher": 2000350040,
"Breaching the Contract": 2000350042,
"Snatcher Coins in Subcon Forest": 2000350044,
"Bird Sanctuary": 2000350046,
"Rift Collapse: Alpine Skyline": 2000350048,
"Wound-Up Windmill": 2000350050,
"The Illness has Speedrun": 2000350052,
"Snatcher Coins in Alpine Skyline": 2000350054,
"Camera Tourist": 2000350056,
"The Mustache Gauntlet": 2000350058,
"No More Bad Guys": 2000350060,
"Seal the Deal": 2000350062,
"Rift Collapse: Deep Sea": 2000350064,
"Cruisin' for a Bruisin'": 2000350066,
"Community Rift: Rhythm Jump Studio": 2000350068,
"Community Rift: Twilight Travels": 2000350070,
"Community Rift: The Mountain Rift": 2000350072,
"Snatcher Coins in Nyakuza Metro": 2000350074,
}
location_table = {
**ahit_locations,
**act_completions,
**storybook_pages,
**contract_locations,
**shop_locations,
}

728
worlds/ahit/Options.py Normal file
View File

@@ -0,0 +1,728 @@
import typing
from worlds.AutoWorld import World
from Options import Option, Range, Toggle, DeathLink, Choice, OptionDict
def adjust_options(world: World):
world.multiworld.HighestChapterCost[world.player].value = max(
world.multiworld.HighestChapterCost[world.player].value,
world.multiworld.LowestChapterCost[world.player].value)
world.multiworld.LowestChapterCost[world.player].value = min(
world.multiworld.LowestChapterCost[world.player].value,
world.multiworld.HighestChapterCost[world.player].value)
world.multiworld.FinalChapterMinCost[world.player].value = min(
world.multiworld.FinalChapterMinCost[world.player].value,
world.multiworld.FinalChapterMaxCost[world.player].value)
world.multiworld.FinalChapterMaxCost[world.player].value = max(
world.multiworld.FinalChapterMaxCost[world.player].value,
world.multiworld.FinalChapterMinCost[world.player].value)
world.multiworld.BadgeSellerMinItems[world.player].value = min(
world.multiworld.BadgeSellerMinItems[world.player].value,
world.multiworld.BadgeSellerMaxItems[world.player].value)
world.multiworld.BadgeSellerMaxItems[world.player].value = max(
world.multiworld.BadgeSellerMinItems[world.player].value,
world.multiworld.BadgeSellerMaxItems[world.player].value)
world.multiworld.NyakuzaThugMinShopItems[world.player].value = min(
world.multiworld.NyakuzaThugMinShopItems[world.player].value,
world.multiworld.NyakuzaThugMaxShopItems[world.player].value)
world.multiworld.NyakuzaThugMaxShopItems[world.player].value = max(
world.multiworld.NyakuzaThugMinShopItems[world.player].value,
world.multiworld.NyakuzaThugMaxShopItems[world.player].value)
world.multiworld.DWShuffleCountMin[world.player].value = min(
world.multiworld.DWShuffleCountMin[world.player].value,
world.multiworld.DWShuffleCountMax[world.player].value)
world.multiworld.DWShuffleCountMax[world.player].value = max(
world.multiworld.DWShuffleCountMin[world.player].value,
world.multiworld.DWShuffleCountMax[world.player].value)
total_tps: int = get_total_time_pieces(world)
if world.multiworld.HighestChapterCost[world.player].value > total_tps-5:
world.multiworld.HighestChapterCost[world.player].value = min(45, total_tps-5)
if world.multiworld.LowestChapterCost[world.player].value > total_tps-5:
world.multiworld.LowestChapterCost[world.player].value = min(45, total_tps-5)
if world.multiworld.FinalChapterMaxCost[world.player].value > total_tps:
world.multiworld.FinalChapterMaxCost[world.player].value = min(50, total_tps)
if world.multiworld.FinalChapterMinCost[world.player].value > total_tps:
world.multiworld.FinalChapterMinCost[world.player].value = min(50, total_tps-5)
# Don't allow Rush Hour goal if DLC2 content is disabled
if world.multiworld.EndGoal[world.player].value == 2 and world.multiworld.EnableDLC2[world.player].value == 0:
world.multiworld.EndGoal[world.player].value = 1
# Don't allow Seal the Deal goal if Death Wish content is disabled
if world.multiworld.EndGoal[world.player].value == 3 and not world.is_dw():
world.multiworld.EndGoal[world.player].value = 1
if world.multiworld.DWEnableBonus[world.player].value > 0:
world.multiworld.DWAutoCompleteBonuses[world.player].value = 0
if world.is_dw_only():
world.multiworld.EndGoal[world.player].value = 3
world.multiworld.ActRandomizer[world.player].value = 0
world.multiworld.ShuffleAlpineZiplines[world.player].value = 0
world.multiworld.ShuffleSubconPaintings[world.player].value = 0
world.multiworld.ShuffleStorybookPages[world.player].value = 0
world.multiworld.ShuffleActContracts[world.player].value = 0
world.multiworld.EnableDLC1[world.player].value = 0
world.multiworld.LogicDifficulty[world.player].value = -1
world.multiworld.DWTimePieceRequirement[world.player].value = 0
def get_total_time_pieces(world: World) -> int:
count: int = 40
if world.is_dlc1():
count += 6
if world.is_dlc2():
count += 10
return min(40+world.multiworld.MaxExtraTimePieces[world.player].value, count)
class EndGoal(Choice):
"""The end goal required to beat the game.
Finale: Reach Time's End and beat Mustache Girl. The Finale will be in its vanilla location.
Rush Hour: Reach and complete Rush Hour. The level will be in its vanilla location and Chapter 7
will be the final chapter. You also must find Nyakuza Metro itself and complete all of its levels.
Requires DLC2 content to be enabled.
Seal the Deal: Reach and complete the Seal the Deal death wish main objective.
Requires Death Wish content to be enabled."""
display_name = "End Goal"
option_finale = 1
option_rush_hour = 2
option_seal_the_deal = 3
default = 1
class ActRandomizer(Choice):
"""If enabled, shuffle the game's Acts between each other.
Light will cause Time Rifts to only be shuffled amongst each other,
and Blue Time Rifts and Purple Time Rifts to be shuffled separately."""
display_name = "Shuffle Acts"
option_false = 0
option_light = 1
option_insanity = 2
default = 1
class ActPlando(OptionDict):
"""Plando acts onto other acts. For example, \"Train Rush\": \"Alpine Free Roam\""""
display_name = "Act Plando"
class FinaleShuffle(Toggle):
"""If enabled, chapter finales will only be shuffled amongst each other in act shuffle."""
display_name = "Finale Shuffle"
default = 0
class LogicDifficulty(Choice):
"""Choose the difficulty setting for logic."""
display_name = "Logic Difficulty"
option_normal = -1
option_moderate = 0
option_hard = 1
option_expert = 2
default = -1
class CTRLogic(Choice):
"""Choose how you want to logically clear Cheating the Race."""
display_name = "Cheating the Race Logic"
option_time_stop_only = 0
option_scooter = 1
option_sprint = 2
option_nothing = 3
default = 0
class RandomizeHatOrder(Choice):
"""Randomize the order that hats are stitched in.
Time Stop Last will force Time Stop to be the last hat in the sequence."""
display_name = "Randomize Hat Order"
option_false = 0
option_true = 1
option_time_stop_last = 2
default = 1
class YarnBalancePercent(Range):
"""How much (in percentage) of the yarn in the pool that will be progression balanced."""
display_name = "Yarn Balance Percentage"
default = 20
range_start = 0
range_end = 100
class TimePieceBalancePercent(Range):
"""How much (in percentage) of time pieces in the pool that will be progression balanced."""
display_name = "Time Piece Balance Percentage"
default = 35
range_start = 0
range_end = 100
class StartWithCompassBadge(Toggle):
"""If enabled, start with the Compass Badge. In Archipelago, the Compass Badge will track all items in the world
(instead of just Relics). Recommended if you're not familiar with where item locations are."""
display_name = "Start with Compass Badge"
default = 1
class CompassBadgeMode(Choice):
"""closest - Compass Badge points to the closest item regardless of classification
important_only - Compass Badge points to progression/useful items only
important_first - Compass Badge points to progression/useful items first, then it will point to junk items"""
display_name = "Compass Badge Mode"
option_closest = 1
option_important_only = 2
option_important_first = 3
default = 1
class UmbrellaLogic(Toggle):
"""Makes Hat Kid's default punch attack do absolutely nothing, making the Umbrella much more relevant and useful"""
display_name = "Umbrella Logic"
default = 0
class ShuffleStorybookPages(Toggle):
"""If enabled, each storybook page in the purple Time Rifts is an item check.
The Compass Badge can track these down for you."""
display_name = "Shuffle Storybook Pages"
default = 1
class ShuffleActContracts(Toggle):
"""If enabled, shuffle Snatcher's act contracts into the pool as items"""
display_name = "Shuffle Contracts"
default = 1
class ShuffleAlpineZiplines(Toggle):
"""If enabled, Alpine's zipline paths leading to the peaks will be locked behind items."""
display_name = "Shuffle Alpine Ziplines"
default = 0
class ShuffleSubconPaintings(Toggle):
"""If enabled, shuffle items into the pool that unlock Subcon Forest fire spirit paintings.
These items are progressive, with the order of Village-Swamp-Courtyard."""
display_name = "Shuffle Subcon Paintings"
default = 0
class NoPaintingSkips(Toggle):
"""If enabled, prevent Subcon fire wall skips from being in logic on higher difficulty settings."""
display_name = "No Subcon Fire Wall Skips"
default = 0
class StartingChapter(Choice):
"""Determines which chapter you will be guaranteed to be able to enter at the beginning of the game."""
display_name = "Starting Chapter"
option_1 = 1
option_2 = 2
option_3 = 3
option_4 = 4
default = 1
class ChapterCostIncrement(Range):
"""Lower values mean chapter costs increase slower. Higher values make the cost differences more steep."""
display_name = "Chapter Cost Increment"
range_start = 1
range_end = 8
default = 4
class ChapterCostMinDifference(Range):
"""The minimum difference between chapter costs."""
display_name = "Minimum Chapter Cost Difference"
range_start = 1
range_end = 8
default = 4
class LowestChapterCost(Range):
"""Value determining the lowest possible cost for a chapter.
Chapter costs will, progressively, be calculated based on this value (except for the final chapter)."""
display_name = "Lowest Possible Chapter Cost"
range_start = 0
range_end = 10
default = 5
class HighestChapterCost(Range):
"""Value determining the highest possible cost for a chapter.
Chapter costs will, progressively, be calculated based on this value (except for the final chapter)."""
display_name = "Highest Possible Chapter Cost"
range_start = 15
range_end = 45
default = 25
class FinalChapterMinCost(Range):
"""Minimum Time Pieces required to enter the final chapter. This is part of your goal."""
display_name = "Final Chapter Minimum Time Piece Cost"
range_start = 0
range_end = 50
default = 30
class FinalChapterMaxCost(Range):
"""Maximum Time Pieces required to enter the final chapter. This is part of your goal."""
display_name = "Final Chapter Maximum Time Piece Cost"
range_start = 0
range_end = 50
default = 35
class MaxExtraTimePieces(Range):
"""Maximum amount of extra Time Pieces from the DLCs.
Arctic Cruise will add up to 6. Nyakuza Metro will add up to 10. The absolute maximum is 56."""
display_name = "Max Extra Time Pieces"
range_start = 0
range_end = 16
default = 16
class YarnCostMin(Range):
"""The minimum possible yarn needed to stitch a hat."""
display_name = "Minimum Yarn Cost"
range_start = 1
range_end = 12
default = 4
class YarnCostMax(Range):
"""The maximum possible yarn needed to stitch a hat."""
display_name = "Maximum Yarn Cost"
range_start = 1
range_end = 12
default = 8
class YarnAvailable(Range):
"""How much yarn is available to collect in the item pool."""
display_name = "Yarn Available"
range_start = 30
range_end = 80
default = 50
class MinExtraYarn(Range):
"""The minimum amount of extra yarn in the item pool.
There must be at least this much more yarn over the total amount of yarn needed to craft all hats.
For example, if this option's value is 10, and the total yarn needed to craft all hats is 40,
there must be at least 50 yarn in the pool."""
display_name = "Max Extra Yarn"
range_start = 5
range_end = 15
default = 10
class HatItems(Toggle):
"""Removes all yarn from the pool and turns the hats into individual items instead."""
display_name = "Hat Items"
default = 0
class MinPonCost(Range):
"""The minimum amount of Pons that any shop item can cost."""
display_name = "Minimum Shop Pon Cost"
range_start = 10
range_end = 800
default = 75
class MaxPonCost(Range):
"""The maximum amount of Pons that any shop item can cost."""
display_name = "Maximum Shop Pon Cost"
range_start = 10
range_end = 800
default = 300
class BadgeSellerMinItems(Range):
"""The smallest amount of items that the Badge Seller can have for sale."""
display_name = "Badge Seller Minimum Items"
range_start = 0
range_end = 10
default = 4
class BadgeSellerMaxItems(Range):
"""The largest amount of items that the Badge Seller can have for sale."""
display_name = "Badge Seller Maximum Items"
range_start = 0
range_end = 10
default = 8
class EnableDLC1(Toggle):
"""Shuffle content from The Arctic Cruise (Chapter 6) into the game. This also includes the Tour time rift.
DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!"""
display_name = "Shuffle Chapter 6"
default = 0
class Tasksanity(Toggle):
"""If enabled, Ship Shape tasks will become checks. Requires DLC1 content to be enabled."""
display_name = "Tasksanity"
default = 0
class TasksanityTaskStep(Range):
"""How many tasks the player must complete in Tasksanity to send a check."""
display_name = "Tasksanity Task Step"
range_start = 1
range_end = 3
default = 1
class TasksanityCheckCount(Range):
"""How many Tasksanity checks there will be in total."""
display_name = "Tasksanity Check Count"
range_start = 5
range_end = 30
default = 18
class ExcludeTour(Toggle):
"""Removes the Tour time rift from the game. This option is recommended if you don't want to deal with
important levels being shuffled onto the Tour time rift, or important items being shuffled onto Tour pages
when your goal is Time's End."""
display_name = "Exclude Tour Time Rift"
default = 0
class ShipShapeCustomTaskGoal(Range):
"""Change the amount of tasks required to complete Ship Shape. This will not affect Cruisin' for a Bruisin'."""
display_name = "Ship Shape Custom Task Goal"
range_start = 1
range_end = 30
default = 18
class EnableDLC2(Toggle):
"""Shuffle content from Nyakuza Metro (Chapter 7) into the game.
DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE NYAKUZA METRO DLC INSTALLED!!!"""
display_name = "Shuffle Chapter 7"
default = 0
class MetroMinPonCost(Range):
"""The cheapest an item can be in any Nyakuza Metro shop. Includes ticket booths."""
display_name = "Metro Shops Minimum Pon Cost"
range_start = 10
range_end = 800
default = 50
class MetroMaxPonCost(Range):
"""The most expensive an item can be in any Nyakuza Metro shop. Includes ticket booths."""
display_name = "Metro Shops Maximum Pon Cost"
range_start = 10
range_end = 800
default = 200
class NyakuzaThugMinShopItems(Range):
"""The smallest amount of items that the thugs in Nyakuza Metro can have for sale."""
display_name = "Nyakuza Thug Minimum Shop Items"
range_start = 0
range_end = 5
default = 2
class NyakuzaThugMaxShopItems(Range):
"""The largest amount of items that the thugs in Nyakuza Metro can have for sale."""
display_name = "Nyakuza Thug Maximum Shop Items"
range_start = 0
range_end = 5
default = 4
class BaseballBat(Toggle):
"""Replace the Umbrella with the baseball bat from Nyakuza Metro.
DLC2 content does not have to be shuffled for this option but Nyakuza Metro still needs to be installed."""
display_name = "Baseball Bat"
default = 0
class EnableDeathWish(Toggle):
"""Shuffle Death Wish contracts into the game. Each contract by default will have 1 check granted upon completion.
DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!"""
display_name = "Enable Death Wish"
default = 0
class DeathWishOnly(Toggle):
"""An alternative gameplay mode that allows you to exclusively play Death Wish in a seed.
This has the following effects:
- Death Wish is instantly unlocked from the start
- All hats and other progression items are instantly given to you
- Useful items such as Fast Hatter Badge will still be in the item pool instead of in your inventory at the start
- All chapters and their levels are unlocked, act shuffle is forced off
- Any checks other than Death Wish contracts are completely removed
- All Pons in the item pool are replaced with Health Pons or random cosmetics
- The EndGoal option is forced to complete Seal the Deal"""
display_name = "Death Wish Only"
default = 0
class DWShuffle(Toggle):
"""An alternative mode for Death Wish where each contract is unlocked one by one, in a random order.
Stamp requirements to unlock contracts is removed. Any excluded contracts will not be shuffled into the sequence.
If Seal the Deal is the end goal, it will always be the last Death Wish in the sequence.
Disabling candles is highly recommended."""
display_name = "Death Wish Shuffle"
default = 0
class DWShuffleCountMin(Range):
"""The minimum number of Death Wishes that can be in the Death Wish shuffle sequence.
The final result is clamped at the number of non-excluded Death Wishes."""
display_name = "Death Wish Shuffle Minimum Count"
range_start = 5
range_end = 38
default = 18
class DWShuffleCountMax(Range):
"""The maximum number of Death Wishes that can be in the Death Wish shuffle sequence.
The final result is clamped at the number of non-excluded Death Wishes."""
display_name = "Death Wish Shuffle Maximum Count"
range_start = 5
range_end = 38
default = 25
class DWEnableBonus(Toggle):
"""In Death Wish, allow the full completion of contracts to reward items.
WARNING!! Only for the brave! This option can create VERY DIFFICULT SEEDS!
ONLY turn this on if you know what you are doing to yourself and everyone else in the multiworld!
Using Peace and Tranquility to auto-complete the bonuses will NOT count!"""
display_name = "Shuffle Death Wish Full Completions"
default = 0
class DWAutoCompleteBonuses(Toggle):
"""If enabled, auto complete all bonus stamps after completing the main objective in a Death Wish.
This option will have no effect if bonus checks (DWEnableBonus) are turned on."""
display_name = "Auto Complete Bonus Stamps"
default = 1
class DWExcludeAnnoyingContracts(Toggle):
"""Exclude Death Wish contracts from the pool that are particularly tedious or take a long time to reach/clear.
Excluded Death Wishes are automatically completed as soon as they are unlocked.
This option currently excludes the following contracts:
- Vault Codes in the Wind
- Boss Rush
- Camera Tourist
- The Mustache Gauntlet
- Rift Collapse: Deep Sea
- Cruisin' for a Bruisin'
- Seal the Deal (non-excluded if goal, but the checks are still excluded)"""
display_name = "Exclude Annoying Death Wish Contracts"
default = 1
class DWExcludeAnnoyingBonuses(Toggle):
"""If Death Wish full completions are shuffled in, exclude tedious Death Wish full completions from the pool.
Excluded bonus Death Wishes automatically reward their bonus stamps upon completion of the main objective.
This option currently excludes the following bonuses:
- So You're Back From Outer Space
- Encore! Encore!
- Snatcher's Hit List
- 10 Seconds until Self-Destruct
- Killing Two Birds
- Zero Jumps
- Bird Sanctuary
- Wound-Up Windmill
- Seal the Deal"""
display_name = "Exclude Annoying Death Wish Full Completions"
default = 1
class DWExcludeCandles(Toggle):
"""If enabled, exclude all candle Death Wishes."""
display_name = "Exclude Candle Death Wishes"
default = 1
class DWTimePieceRequirement(Range):
"""How many Time Pieces that will be required to unlock Death Wish."""
display_name = "Death Wish Time Piece Requirement"
range_start = 0
range_end = 35
default = 15
class TrapChance(Range):
"""The chance for any junk item in the pool to be replaced by a trap."""
display_name = "Trap Chance"
range_start = 0
range_end = 100
default = 0
class BabyTrapWeight(Range):
"""The weight of Baby Traps in the trap pool.
Baby Traps place a multitude of the Conductor's grandkids into Hat Kid's hands, causing her to lose her balance."""
display_name = "Baby Trap Weight"
range_start = 0
range_end = 100
default = 40
class LaserTrapWeight(Range):
"""The weight of Laser Traps in the trap pool.
Laser Traps will spawn multiple giant lasers (from Snatcher's boss fight) at Hat Kid's location."""
display_name = "Laser Trap Weight"
range_start = 0
range_end = 100
default = 40
class ParadeTrapWeight(Range):
"""The weight of Parade Traps in the trap pool.
Parade Traps will summon multiple Express Band owls with knives that chase Hat Kid by mimicking her movement."""
display_name = "Parade Trap Weight"
range_start = 0
range_end = 100
default = 20
ahit_options: typing.Dict[str, type(Option)] = {
"EndGoal": EndGoal,
"ActRandomizer": ActRandomizer,
"ActPlando": ActPlando,
"ShuffleAlpineZiplines": ShuffleAlpineZiplines,
"FinaleShuffle": FinaleShuffle,
"LogicDifficulty": LogicDifficulty,
"YarnBalancePercent": YarnBalancePercent,
"TimePieceBalancePercent": TimePieceBalancePercent,
"RandomizeHatOrder": RandomizeHatOrder,
"UmbrellaLogic": UmbrellaLogic,
"StartWithCompassBadge": StartWithCompassBadge,
"CompassBadgeMode": CompassBadgeMode,
"ShuffleStorybookPages": ShuffleStorybookPages,
"ShuffleActContracts": ShuffleActContracts,
"ShuffleSubconPaintings": ShuffleSubconPaintings,
"NoPaintingSkips": NoPaintingSkips,
"StartingChapter": StartingChapter,
"CTRLogic": CTRLogic,
"EnableDLC1": EnableDLC1,
"Tasksanity": Tasksanity,
"TasksanityTaskStep": TasksanityTaskStep,
"TasksanityCheckCount": TasksanityCheckCount,
"ExcludeTour": ExcludeTour,
"ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal,
"EnableDeathWish": EnableDeathWish,
"DWShuffle": DWShuffle,
"DWShuffleCountMin": DWShuffleCountMin,
"DWShuffleCountMax": DWShuffleCountMax,
"DeathWishOnly": DeathWishOnly,
"DWEnableBonus": DWEnableBonus,
"DWAutoCompleteBonuses": DWAutoCompleteBonuses,
"DWExcludeAnnoyingContracts": DWExcludeAnnoyingContracts,
"DWExcludeAnnoyingBonuses": DWExcludeAnnoyingBonuses,
"DWExcludeCandles": DWExcludeCandles,
"DWTimePieceRequirement": DWTimePieceRequirement,
"EnableDLC2": EnableDLC2,
"BaseballBat": BaseballBat,
"MetroMinPonCost": MetroMinPonCost,
"MetroMaxPonCost": MetroMaxPonCost,
"NyakuzaThugMinShopItems": NyakuzaThugMinShopItems,
"NyakuzaThugMaxShopItems": NyakuzaThugMaxShopItems,
"LowestChapterCost": LowestChapterCost,
"HighestChapterCost": HighestChapterCost,
"ChapterCostIncrement": ChapterCostIncrement,
"ChapterCostMinDifference": ChapterCostMinDifference,
"MaxExtraTimePieces": MaxExtraTimePieces,
"FinalChapterMinCost": FinalChapterMinCost,
"FinalChapterMaxCost": FinalChapterMaxCost,
"YarnCostMin": YarnCostMin,
"YarnCostMax": YarnCostMax,
"YarnAvailable": YarnAvailable,
"MinExtraYarn": MinExtraYarn,
"HatItems": HatItems,
"MinPonCost": MinPonCost,
"MaxPonCost": MaxPonCost,
"BadgeSellerMinItems": BadgeSellerMinItems,
"BadgeSellerMaxItems": BadgeSellerMaxItems,
"TrapChance": TrapChance,
"BabyTrapWeight": BabyTrapWeight,
"LaserTrapWeight": LaserTrapWeight,
"ParadeTrapWeight": ParadeTrapWeight,
"death_link": DeathLink,
}
slot_data_options: typing.Dict[str, type(Option)] = {
"EndGoal": EndGoal,
"ActRandomizer": ActRandomizer,
"ShuffleAlpineZiplines": ShuffleAlpineZiplines,
"LogicDifficulty": LogicDifficulty,
"CTRLogic": CTRLogic,
"RandomizeHatOrder": RandomizeHatOrder,
"UmbrellaLogic": UmbrellaLogic,
"StartWithCompassBadge": StartWithCompassBadge,
"CompassBadgeMode": CompassBadgeMode,
"ShuffleStorybookPages": ShuffleStorybookPages,
"ShuffleActContracts": ShuffleActContracts,
"ShuffleSubconPaintings": ShuffleSubconPaintings,
"NoPaintingSkips": NoPaintingSkips,
"HatItems": HatItems,
"EnableDLC1": EnableDLC1,
"Tasksanity": Tasksanity,
"TasksanityTaskStep": TasksanityTaskStep,
"TasksanityCheckCount": TasksanityCheckCount,
"ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal,
"ExcludeTour": ExcludeTour,
"EnableDeathWish": EnableDeathWish,
"DWShuffle": DWShuffle,
"DeathWishOnly": DeathWishOnly,
"DWEnableBonus": DWEnableBonus,
"DWAutoCompleteBonuses": DWAutoCompleteBonuses,
"DWTimePieceRequirement": DWTimePieceRequirement,
"EnableDLC2": EnableDLC2,
"MetroMinPonCost": MetroMinPonCost,
"MetroMaxPonCost": MetroMaxPonCost,
"BaseballBat": BaseballBat,
"MinPonCost": MinPonCost,
"MaxPonCost": MaxPonCost,
"death_link": DeathLink,
}

900
worlds/ahit/Regions.py Normal file
View File

@@ -0,0 +1,900 @@
from worlds.AutoWorld import World
from BaseClasses import Region, Entrance, ItemClassification, Location
from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem
from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \
shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard
import typing
from .Rules import set_rift_rules, get_difficulty
# ChapterIndex: region
chapter_regions = {
ChapterIndex.SPACESHIP: "Spaceship",
ChapterIndex.MAFIA: "Mafia Town",
ChapterIndex.BIRDS: "Battle of the Birds",
ChapterIndex.SUBCON: "Subcon Forest",
ChapterIndex.ALPINE: "Alpine Skyline",
ChapterIndex.FINALE: "Time's End",
ChapterIndex.CRUISE: "The Arctic Cruise",
ChapterIndex.METRO: "Nyakuza Metro",
}
# entrance: region
act_entrances = {
"Welcome to Mafia Town": "Mafia Town - Act 1",
"Barrel Battle": "Mafia Town - Act 2",
"She Came from Outer Space": "Mafia Town - Act 3",
"Down with the Mafia!": "Mafia Town - Act 4",
"Cheating the Race": "Mafia Town - Act 5",
"Heating Up Mafia Town": "Mafia Town - Act 6",
"The Golden Vault": "Mafia Town - Act 7",
"Dead Bird Studio": "Battle of the Birds - Act 1",
"Murder on the Owl Express": "Battle of the Birds - Act 2",
"Picture Perfect": "Battle of the Birds - Act 3",
"Train Rush": "Battle of the Birds - Act 4",
"The Big Parade": "Battle of the Birds - Act 5",
"Award Ceremony": "Battle of the Birds - Finale A",
"Dead Bird Studio Basement": "Battle of the Birds - Finale B",
"Contractual Obligations": "Subcon Forest - Act 1",
"The Subcon Well": "Subcon Forest - Act 2",
"Toilet of Doom": "Subcon Forest - Act 3",
"Queen Vanessa's Manor": "Subcon Forest - Act 4",
"Mail Delivery Service": "Subcon Forest - Act 5",
"Your Contract has Expired": "Subcon Forest - Finale",
"Alpine Free Roam": "Alpine Skyline - Free Roam",
"The Illness has Spread": "Alpine Skyline - Finale",
"The Finale": "Time's End - Act 1",
"Bon Voyage!": "The Arctic Cruise - Act 1",
"Ship Shape": "The Arctic Cruise - Act 2",
"Rock the Boat": "The Arctic Cruise - Finale",
"Nyakuza Free Roam": "Nyakuza Metro - Free Roam",
"Rush Hour": "Nyakuza Metro - Finale",
}
act_chapters = {
"Time Rift - Gallery": "Spaceship",
"Time Rift - The Lab": "Spaceship",
"Welcome to Mafia Town": "Mafia Town",
"Barrel Battle": "Mafia Town",
"She Came from Outer Space": "Mafia Town",
"Down with the Mafia!": "Mafia Town",
"Cheating the Race": "Mafia Town",
"Heating Up Mafia Town": "Mafia Town",
"The Golden Vault": "Mafia Town",
"Time Rift - Mafia of Cooks": "Mafia Town",
"Time Rift - Sewers": "Mafia Town",
"Time Rift - Bazaar": "Mafia Town",
"Dead Bird Studio": "Battle of the Birds",
"Murder on the Owl Express": "Battle of the Birds",
"Picture Perfect": "Battle of the Birds",
"Train Rush": "Battle of the Birds",
"The Big Parade": "Battle of the Birds",
"Award Ceremony": "Battle of the Birds",
"Dead Bird Studio Basement": "Battle of the Birds",
"Time Rift - Dead Bird Studio": "Battle of the Birds",
"Time Rift - The Owl Express": "Battle of the Birds",
"Time Rift - The Moon": "Battle of the Birds",
"Contractual Obligations": "Subcon Forest",
"The Subcon Well": "Subcon Forest",
"Toilet of Doom": "Subcon Forest",
"Queen Vanessa's Manor": "Subcon Forest",
"Mail Delivery Service": "Subcon Forest",
"Your Contract has Expired": "Subcon Forest",
"Time Rift - Sleepy Subcon": "Subcon Forest",
"Time Rift - Pipe": "Subcon Forest",
"Time Rift - Village": "Subcon Forest",
"Alpine Free Roam": "Alpine Skyline",
"The Illness has Spread": "Alpine Skyline",
"Time Rift - Alpine Skyline": "Alpine Skyline",
"Time Rift - The Twilight Bell": "Alpine Skyline",
"Time Rift - Curly Tail Trail": "Alpine Skyline",
"The Finale": "Time's End",
"Time Rift - Tour": "Time's End",
"Bon Voyage!": "The Arctic Cruise",
"Ship Shape": "The Arctic Cruise",
"Rock the Boat": "The Arctic Cruise",
"Time Rift - Balcony": "The Arctic Cruise",
"Time Rift - Deep Sea": "The Arctic Cruise",
"Nyakuza Free Roam": "Nyakuza Metro",
"Rush Hour": "Nyakuza Metro",
"Time Rift - Rumbi Factory": "Nyakuza Metro",
}
# region: list[Region]
rift_access_regions = {
"Time Rift - Gallery": ["Spaceship"],
"Time Rift - The Lab": ["Spaceship"],
"Time Rift - Sewers": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space",
"Down with the Mafia!", "Cheating the Race", "Heating Up Mafia Town",
"The Golden Vault"],
"Time Rift - Bazaar": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space",
"Down with the Mafia!", "Cheating the Race", "Heating Up Mafia Town",
"The Golden Vault"],
"Time Rift - Mafia of Cooks": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space",
"Down with the Mafia!", "Cheating the Race", "The Golden Vault"],
"Time Rift - The Owl Express": ["Murder on the Owl Express"],
"Time Rift - The Moon": ["Picture Perfect", "The Big Parade"],
"Time Rift - Dead Bird Studio": ["Dead Bird Studio", "Dead Bird Studio Basement"],
"Time Rift - Pipe": ["Contractual Obligations", "The Subcon Well",
"Toilet of Doom", "Queen Vanessa's Manor",
"Mail Delivery Service"],
"Time Rift - Village": ["Contractual Obligations", "The Subcon Well",
"Toilet of Doom", "Queen Vanessa's Manor",
"Mail Delivery Service"],
"Time Rift - Sleepy Subcon": ["Contractual Obligations", "The Subcon Well",
"Toilet of Doom", "Queen Vanessa's Manor",
"Mail Delivery Service"],
"Time Rift - The Twilight Bell": ["Alpine Free Roam"],
"Time Rift - Curly Tail Trail": ["Alpine Free Roam"],
"Time Rift - Alpine Skyline": ["Alpine Free Roam", "The Illness has Spread"],
"Time Rift - Tour": ["Time's End"],
"Time Rift - Balcony": ["Cruise Ship"],
"Time Rift - Deep Sea": ["Bon Voyage!"],
"Time Rift - Rumbi Factory": ["Nyakuza Free Roam"],
}
# Time piece identifiers to be used in act shuffle
chapter_act_info = {
"Time Rift - Gallery": "Spaceship_WaterRift_Gallery",
"Time Rift - The Lab": "Spaceship_WaterRift_MailRoom",
"Welcome to Mafia Town": "chapter1_tutorial",
"Barrel Battle": "chapter1_barrelboss",
"She Came from Outer Space": "chapter1_cannon_repair",
"Down with the Mafia!": "chapter1_boss",
"Cheating the Race": "harbor_impossible_race",
"Heating Up Mafia Town": "mafiatown_lava",
"The Golden Vault": "mafiatown_goldenvault",
"Time Rift - Mafia of Cooks": "TimeRift_Cave_Mafia",
"Time Rift - Sewers": "TimeRift_Water_Mafia_Easy",
"Time Rift - Bazaar": "TimeRift_Water_Mafia_Hard",
"Dead Bird Studio": "DeadBirdStudio",
"Murder on the Owl Express": "chapter3_murder",
"Picture Perfect": "moon_camerasnap",
"Train Rush": "trainwreck_selfdestruct",
"The Big Parade": "moon_parade",
"Award Ceremony": "award_ceremony",
"Dead Bird Studio Basement": "chapter3_secret_finale",
"Time Rift - Dead Bird Studio": "TimeRift_Cave_BirdBasement",
"Time Rift - The Owl Express": "TimeRift_Water_TWreck_Panels",
"Time Rift - The Moon": "TimeRift_Water_TWreck_Parade",
"Contractual Obligations": "subcon_village_icewall",
"The Subcon Well": "subcon_cave",
"Toilet of Doom": "chapter2_toiletboss",
"Queen Vanessa's Manor": "vanessa_manor_attic",
"Mail Delivery Service": "subcon_maildelivery",
"Your Contract has Expired": "snatcher_boss",
"Time Rift - Sleepy Subcon": "TimeRift_Cave_Raccoon",
"Time Rift - Pipe": "TimeRift_Water_Subcon_Hookshot",
"Time Rift - Village": "TimeRift_Water_Subcon_Dwellers",
"Alpine Free Roam": "AlpineFreeRoam", # not an actual Time Piece
"The Illness has Spread": "AlpineSkyline_Finale",
"Time Rift - Alpine Skyline": "TimeRift_Cave_Alps",
"Time Rift - The Twilight Bell": "TimeRift_Water_Alp_Goats",
"Time Rift - Curly Tail Trail": "TimeRift_Water_AlpineSkyline_Cats",
"The Finale": "TheFinale_FinalBoss",
"Time Rift - Tour": "TimeRift_Cave_Tour",
"Bon Voyage!": "Cruise_Boarding",
"Ship Shape": "Cruise_Working",
"Rock the Boat": "Cruise_Sinking",
"Time Rift - Balcony": "Cruise_WaterRift_Slide",
"Time Rift - Deep Sea": "Cruise_CaveRift_Aquarium",
"Nyakuza Free Roam": "MetroFreeRoam", # not an actual Time Piece
"Rush Hour": "Metro_Escape",
"Time Rift - Rumbi Factory": "Metro_CaveRift_RumbiFactory"
}
# Guarantee that the first level a player can access is a location dense area beatable with no items
guaranteed_first_acts = [
"Welcome to Mafia Town",
"Barrel Battle",
"She Came from Outer Space",
"Down with the Mafia!",
"Heating Up Mafia Town", # Removed in umbrella logic
"The Golden Vault",
"Contractual Obligations", # Removed in painting logic
"Queen Vanessa's Manor", # Removed in umbrella/painting logic
]
purple_time_rifts = [
"Time Rift - Mafia of Cooks",
"Time Rift - Dead Bird Studio",
"Time Rift - Sleepy Subcon",
"Time Rift - Alpine Skyline",
"Time Rift - Deep Sea",
"Time Rift - Tour",
"Time Rift - Rumbi Factory",
]
chapter_finales = [
"Dead Bird Studio Basement",
"Your Contract has Expired",
"The Illness has Spread",
"Rock the Boat",
"Rush Hour",
]
# Acts blacklisted in act shuffle
# entrance: region
blacklisted_acts = {
"Battle of the Birds - Finale A": "Award Ceremony",
}
# Blacklisted act shuffle combinations to help prevent impossible layouts. Mostly for free roam acts.
blacklisted_combos = {
"The Illness has Spread": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"],
"Rush Hour": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"],
"Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam", "Bon Voyage!",
"Contractual Obligations"],
"Time Rift - The Moon": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"],
"Time Rift - Dead Bird Studio": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"],
"Time Rift - Curly Tail Trail": ["Nyakuza Free Roam", "Contractual Obligations"],
"Time Rift - The Twilight Bell": ["Nyakuza Free Roam", "Contractual Obligations"],
"Time Rift - Alpine Skyline": ["Nyakuza Free Roam", "Contractual Obligations"],
"Time Rift - Rumbi Factory": ["Alpine Free Roam", "Contractual Obligations"],
"Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"],
}
def create_regions(world: World):
w = world
mw = world.multiworld
p = world.player
# ------------------------------------------- HUB -------------------------------------------------- #
menu = create_region(w, "Menu")
spaceship = create_region_and_connect(w, "Spaceship", "Save File -> Spaceship", menu)
# we only need the menu and the spaceship regions
if world.is_dw_only():
return
create_rift_connections(w, create_region(w, "Time Rift - Gallery"))
create_rift_connections(w, create_region(w, "Time Rift - The Lab"))
# ------------------------------------------- MAFIA TOWN ------------------------------------------- #
mafia_town = create_region_and_connect(w, "Mafia Town", "Telescope -> Mafia Town", spaceship)
mt_act1 = create_region_and_connect(w, "Welcome to Mafia Town", "Mafia Town - Act 1", mafia_town)
mt_act2 = create_region_and_connect(w, "Barrel Battle", "Mafia Town - Act 2", mafia_town)
mt_act3 = create_region_and_connect(w, "She Came from Outer Space", "Mafia Town - Act 3", mafia_town)
mt_act4 = create_region_and_connect(w, "Down with the Mafia!", "Mafia Town - Act 4", mafia_town)
mt_act6 = create_region_and_connect(w, "Heating Up Mafia Town", "Mafia Town - Act 6", mafia_town)
mt_act5 = create_region_and_connect(w, "Cheating the Race", "Mafia Town - Act 5", mafia_town)
mt_act7 = create_region_and_connect(w, "The Golden Vault", "Mafia Town - Act 7", mafia_town)
# ------------------------------------------- BOTB ------------------------------------------------- #
botb = create_region_and_connect(w, "Battle of the Birds", "Telescope -> Battle of the Birds", spaceship)
dbs = create_region_and_connect(w, "Dead Bird Studio", "Battle of the Birds - Act 1", botb)
create_region_and_connect(w, "Murder on the Owl Express", "Battle of the Birds - Act 2", botb)
pp = create_region_and_connect(w, "Picture Perfect", "Battle of the Birds - Act 3", botb)
tr = create_region_and_connect(w, "Train Rush", "Battle of the Birds - Act 4", botb)
create_region_and_connect(w, "The Big Parade", "Battle of the Birds - Act 5", botb)
create_region_and_connect(w, "Award Ceremony", "Battle of the Birds - Finale A", botb)
basement = create_region_and_connect(w, "Dead Bird Studio Basement", "Battle of the Birds - Finale B", botb)
create_rift_connections(w, create_region(w, "Time Rift - Dead Bird Studio"))
create_rift_connections(w, create_region(w, "Time Rift - The Owl Express"))
create_rift_connections(w, create_region(w, "Time Rift - The Moon"))
# Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert
ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs)
post_ev_area = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs)
connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p)
if world.multiworld.LogicDifficulty[world.player].value >= int(Difficulty.EXPERT):
connect_regions(basement, post_ev_area, "DBS Basement -> Post Elevator Area", p)
# ------------------------------------------- SUBCON FOREST --------------------------------------- #
subcon_forest = create_region_and_connect(w, "Subcon Forest", "Telescope -> Subcon Forest", spaceship)
sf_act1 = create_region_and_connect(w, "Contractual Obligations", "Subcon Forest - Act 1", subcon_forest)
sf_act2 = create_region_and_connect(w, "The Subcon Well", "Subcon Forest - Act 2", subcon_forest)
sf_act3 = create_region_and_connect(w, "Toilet of Doom", "Subcon Forest - Act 3", subcon_forest)
sf_act4 = create_region_and_connect(w, "Queen Vanessa's Manor", "Subcon Forest - Act 4", subcon_forest)
sf_act5 = create_region_and_connect(w, "Mail Delivery Service", "Subcon Forest - Act 5", subcon_forest)
create_region_and_connect(w, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest)
# ------------------------------------------- ALPINE SKYLINE ------------------------------------------ #
alpine_skyline = create_region_and_connect(w, "Alpine Skyline", "Telescope -> Alpine Skyline", spaceship)
alpine_freeroam = create_region_and_connect(w, "Alpine Free Roam", "Alpine Skyline - Free Roam", alpine_skyline)
alpine_area = create_region_and_connect(w, "Alpine Skyline Area", "AFR -> Alpine Skyline Area", alpine_freeroam)
# Needs to be separate because there are a lot of locations in Alpine that can't be accessed from Illness
alpine_area_tihs = create_region_and_connect(w, "Alpine Skyline Area (TIHS)", "-> Alpine Skyline Area (TIHS)",
alpine_area)
create_region_and_connect(w, "The Birdhouse", "-> The Birdhouse", alpine_area)
create_region_and_connect(w, "The Lava Cake", "-> The Lava Cake", alpine_area)
create_region_and_connect(w, "The Windmill", "-> The Windmill", alpine_area)
create_region_and_connect(w, "The Twilight Bell", "-> The Twilight Bell", alpine_area)
illness = create_region_and_connect(w, "The Illness has Spread", "Alpine Skyline - Finale", alpine_skyline)
connect_regions(illness, alpine_area_tihs, "TIHS -> Alpine Skyline Area (TIHS)", p)
create_rift_connections(w, create_region(w, "Time Rift - Alpine Skyline"))
create_rift_connections(w, create_region(w, "Time Rift - The Twilight Bell"))
create_rift_connections(w, create_region(w, "Time Rift - Curly Tail Trail"))
# ------------------------------------------- OTHER -------------------------------------------------- #
mt_area: Region = create_region(w, "Mafia Town Area")
mt_area_humt: Region = create_region(w, "Mafia Town Area (HUMT)")
connect_regions(mt_area, mt_area_humt, "MT Area -> MT Area (HUMT)", p)
connect_regions(mt_act1, mt_area, "Mafia Town Entrance WTMT", p)
connect_regions(mt_act2, mt_area, "Mafia Town Entrance BB", p)
connect_regions(mt_act3, mt_area, "Mafia Town Entrance SCFOS", p)
connect_regions(mt_act4, mt_area, "Mafia Town Entrance DWTM", p)
connect_regions(mt_act5, mt_area, "Mafia Town Entrance CTR", p)
connect_regions(mt_act6, mt_area_humt, "Mafia Town Entrance HUMT", p)
connect_regions(mt_act7, mt_area, "Mafia Town Entrance TGV", p)
create_rift_connections(w, create_region(w, "Time Rift - Mafia of Cooks"))
create_rift_connections(w, create_region(w, "Time Rift - Sewers"))
create_rift_connections(w, create_region(w, "Time Rift - Bazaar"))
sf_area: Region = create_region(w, "Subcon Forest Area")
connect_regions(sf_act1, sf_area, "Subcon Forest Entrance CO", p)
connect_regions(sf_act2, sf_area, "Subcon Forest Entrance SW", p)
connect_regions(sf_act3, sf_area, "Subcon Forest Entrance TOD", p)
connect_regions(sf_act4, sf_area, "Subcon Forest Entrance QVM", p)
connect_regions(sf_act5, sf_area, "Subcon Forest Entrance MDS", p)
create_rift_connections(w, create_region(w, "Time Rift - Sleepy Subcon"))
create_rift_connections(w, create_region(w, "Time Rift - Pipe"))
create_rift_connections(w, create_region(w, "Time Rift - Village"))
badge_seller = create_badge_seller(w)
connect_regions(mt_area, badge_seller, "MT Area -> Badge Seller", p)
connect_regions(mt_area_humt, badge_seller, "MT Area (HUMT) -> Badge Seller", p)
connect_regions(sf_area, badge_seller, "SF Area -> Badge Seller", p)
connect_regions(dbs, badge_seller, "DBS -> Badge Seller", p)
connect_regions(pp, badge_seller, "PP -> Badge Seller", p)
connect_regions(tr, badge_seller, "TR -> Badge Seller", p)
connect_regions(alpine_area_tihs, badge_seller, "ASA -> Badge Seller", p)
times_end = create_region_and_connect(w, "Time's End", "Telescope -> Time's End", spaceship)
create_region_and_connect(w, "The Finale", "Time's End - Act 1", times_end)
# ------------------------------------------- DLC1 ------------------------------------------------- #
if w.is_dlc1():
arctic_cruise = create_region_and_connect(w, "The Arctic Cruise", "Telescope -> The Arctic Cruise", spaceship)
cruise_ship = create_region(w, "Cruise Ship")
ac_act1 = create_region_and_connect(w, "Bon Voyage!", "The Arctic Cruise - Act 1", arctic_cruise)
ac_act2 = create_region_and_connect(w, "Ship Shape", "The Arctic Cruise - Act 2", arctic_cruise)
ac_act3 = create_region_and_connect(w, "Rock the Boat", "The Arctic Cruise - Finale", arctic_cruise)
connect_regions(ac_act1, cruise_ship, "Cruise Ship Entrance BV", p)
connect_regions(ac_act2, cruise_ship, "Cruise Ship Entrance SS", p)
connect_regions(ac_act3, cruise_ship, "Cruise Ship Entrance RTB", p)
create_rift_connections(w, create_region(w, "Time Rift - Balcony"))
create_rift_connections(w, create_region(w, "Time Rift - Deep Sea"))
if mw.ExcludeTour[world.player].value == 0:
create_rift_connections(w, create_region(w, "Time Rift - Tour"))
if mw.Tasksanity[p].value > 0:
create_tasksanity_locations(w)
connect_regions(cruise_ship, badge_seller, "CS -> Badge Seller", p)
if w.is_dlc2():
nyakuza_metro = create_region_and_connect(w, "Nyakuza Metro", "Telescope -> Nyakuza Metro", spaceship)
metro_freeroam = create_region_and_connect(w, "Nyakuza Free Roam", "Nyakuza Metro - Free Roam", nyakuza_metro)
create_region_and_connect(w, "Rush Hour", "Nyakuza Metro - Finale", nyakuza_metro)
yellow = create_region_and_connect(w, "Yellow Overpass Station", "-> Yellow Overpass Station", metro_freeroam)
green = create_region_and_connect(w, "Green Clean Station", "-> Green Clean Station", metro_freeroam)
pink = create_region_and_connect(w, "Pink Paw Station", "-> Pink Paw Station", metro_freeroam)
create_region_and_connect(w, "Bluefin Tunnel", "-> Bluefin Tunnel", metro_freeroam) # No manhole
create_region_and_connect(w, "Yellow Overpass Manhole", "-> Yellow Overpass Manhole", yellow)
create_region_and_connect(w, "Green Clean Manhole", "-> Green Clean Manhole", green)
create_region_and_connect(w, "Pink Paw Manhole", "-> Pink Paw Manhole", pink)
create_rift_connections(w, create_region(w, "Time Rift - Rumbi Factory"))
create_thug_shops(w)
def create_rift_connections(world: World, region: Region):
i = 1
for name in rift_access_regions[region.name]:
act_region = world.multiworld.get_region(name, world.player)
entrance_name = f"{region.name} Portal - Entrance {i}"
connect_regions(act_region, region, entrance_name, world.player)
i += 1
# fix for some weird keyerror from tests
if region.name == "Time Rift - Rumbi Factory":
for entrance in region.entrances:
world.multiworld.get_entrance(entrance.name, world.player)
def create_tasksanity_locations(world: World):
ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player)
id_start: int = TASKSANITY_START_ID
for i in range(world.multiworld.TasksanityCheckCount[world.player].value):
location = HatInTimeLocation(world.player, f"Tasksanity Check {i+1}", id_start+i, ship_shape)
ship_shape.locations.append(location)
def is_valid_plando(world: World, region: str) -> bool:
if region in blacklisted_acts.values():
return False
if region not in world.multiworld.ActPlando[world.player].keys():
return False
act = world.multiworld.ActPlando[world.player].get(region)
if act in blacklisted_acts.values():
return False
# Don't allow plando-ing things onto the first act that aren't completable with nothing
is_first_act: bool = act_chapters[region] == get_first_chapter_region(world).name \
and region in act_entrances.keys() and ("Act 1" in act_entrances[region] or "Free Roam" in act_entrances[region])
if is_first_act:
if act_chapters[act] == "Subcon Forest" and world.multiworld.ShuffleSubconPaintings[world.player].value > 0:
return False
if world.multiworld.UmbrellaLogic[world.player].value > 0 \
and (act == "Heating Up Mafia Town" or act == "Queen Vanessa's Manor"):
return False
if act not in guaranteed_first_acts:
return False
# Don't allow straight up impossible mappings
if region == "The Illness has Spread" and act == "Alpine Free Roam":
return False
if region == "Rush Hour" and act == "Nyakuza Free Roam":
return False
if region == "Time Rift - Rumbi Factory" and act == "Nyakuza Free Roam":
return False
if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express":
return False
return any(a.name == world.multiworld.ActPlando[world.player].get(region) for a in
world.multiworld.get_regions(world.player))
def randomize_act_entrances(world: World):
region_list: typing.List[Region] = get_act_regions(world)
world.random.shuffle(region_list)
separate_rifts: bool = bool(world.multiworld.ActRandomizer[world.player].value == 1)
for region in region_list.copy():
if (act_chapters[region.name] == "Alpine Skyline" or act_chapters[region.name] == "Nyakuza Metro") \
and "Time Rift" not in region.name:
region_list.remove(region)
region_list.append(region)
for region in region_list.copy():
if "Time Rift" in region.name:
region_list.remove(region)
region_list.append(region)
for region in region_list.copy():
if region.name in chapter_finales:
region_list.remove(region)
region_list.append(region)
for region in region_list.copy():
if region.name in world.multiworld.ActPlando[world.player].keys():
if is_valid_plando(world, region.name):
region_list.remove(region)
region_list.append(region)
else:
print("Disallowing act plando for",
world.multiworld.player_name[world.player],
"-", region.name, ":", world.multiworld.ActPlando[world.player].get(region.name))
# Reverse the list, so we can do what we want to do first
region_list.reverse()
shuffled_list: typing.List[Region] = []
mapped_list: typing.List[Region] = []
rift_dict: typing.Dict[str, Region] = {}
first_chapter: Region = get_first_chapter_region(world)
has_guaranteed: bool = False
i: int = 0
while i < len(region_list):
region = region_list[i]
i += 1
# Get the first accessible act, so we can map that to something first
if not has_guaranteed:
if act_chapters[region.name] != first_chapter.name:
continue
if region.name not in act_entrances.keys() or "Act 1" not in act_entrances[region.name] \
and "Free Roam" not in act_entrances[region.name]:
continue
if region.name in world.multiworld.ActPlando[world.player].keys() and is_valid_plando(world, region.name):
has_guaranteed = True
i = 0
# Already mapped to something else
if region in mapped_list:
continue
mapped_list.append(region)
# Look for candidates to map this act to
candidate_list: typing.List[Region] = []
for candidate in region_list:
# We're mapping something to the first act, make sure it is valid
if not has_guaranteed:
if candidate.name not in guaranteed_first_acts:
continue
if candidate.name in world.multiworld.ActPlando[world.player].values():
continue
# Not completable without Umbrella
if world.multiworld.UmbrellaLogic[world.player].value > 0 \
and (candidate.name == "Heating Up Mafia Town" or candidate.name == "Queen Vanessa's Manor"):
continue
# Subcon sphere 1 is too small without painting unlocks, and no acts are completable either
if world.multiworld.ShuffleSubconPaintings[world.player].value > 0 \
and "Subcon Forest" in act_entrances[candidate.name]:
continue
candidate_list.append(candidate)
has_guaranteed = True
break
if region.name in world.multiworld.ActPlando[world.player].keys() and is_valid_plando(world, region.name):
candidate_list.clear()
candidate_list.append(
world.multiworld.get_region(world.multiworld.ActPlando[world.player].get(region.name), world.player))
break
# Already mapped onto something else
if candidate in shuffled_list:
continue
if separate_rifts:
# Don't map Time Rifts to normal acts
if "Time Rift" in region.name and "Time Rift" not in candidate.name:
continue
# Don't map normal acts to Time Rifts
if "Time Rift" not in region.name and "Time Rift" in candidate.name:
continue
# Separate purple rifts
if region.name in purple_time_rifts and candidate.name not in purple_time_rifts \
or region.name not in purple_time_rifts and candidate.name in purple_time_rifts:
continue
if region.name in blacklisted_combos.keys() and candidate.name in blacklisted_combos[region.name]:
continue
# Prevent Contractual Obligations from being inaccessible if contracts are not shuffled
if world.multiworld.ShuffleActContracts[world.player].value == 0:
if (region.name == "Your Contract has Expired" or region.name == "The Subcon Well") \
and candidate.name == "Contractual Obligations":
continue
if world.multiworld.FinaleShuffle[world.player].value > 0 and region.name in chapter_finales:
if candidate.name not in chapter_finales:
continue
if region.name in rift_access_regions and candidate.name in rift_access_regions[region.name]:
continue
candidate_list.append(candidate)
candidate: Region
if len(candidate_list) > 0:
candidate = candidate_list[world.random.randint(0, len(candidate_list)-1)]
else:
# plando can still break certain rules, so acts may not always end up shuffled.
for c in region_list:
if c not in shuffled_list:
candidate = c
break
shuffled_list.append(candidate)
# print(region, candidate)
# Vanilla
if candidate.name == region.name:
if region.name in rift_access_regions.keys():
rift_dict.setdefault(region.name, candidate)
update_chapter_act_info(world, region, candidate)
continue
if region.name in rift_access_regions.keys():
connect_time_rift(world, region, candidate)
rift_dict.setdefault(region.name, candidate)
else:
if candidate.name in rift_access_regions.keys():
for e in candidate.entrances.copy():
e.parent_region.exits.remove(e)
e.connected_region.entrances.remove(e)
entrance = world.multiworld.get_entrance(act_entrances[region.name], world.player)
reconnect_regions(entrance, world.multiworld.get_region(act_chapters[region.name], world.player), candidate)
update_chapter_act_info(world, region, candidate)
for name in blacklisted_acts.values():
if not is_act_blacklisted(world, name):
continue
region: Region = world.multiworld.get_region(name, world.player)
update_chapter_act_info(world, region, region)
set_rift_rules(world, rift_dict)
def connect_time_rift(world: World, time_rift: Region, exit_region: Region):
count: int = len(rift_access_regions[time_rift.name])
i: int = 1
while i <= count:
name = f"{time_rift.name} Portal - Entrance {i}"
entrance: Entrance = world.multiworld.get_entrance(name, world.player)
reconnect_regions(entrance, entrance.parent_region, exit_region)
i += 1
def get_act_regions(world: World) -> typing.List[Region]:
act_list: typing.List[Region] = []
for region in world.multiworld.get_regions(world.player):
if region.name in chapter_act_info.keys():
if not is_act_blacklisted(world, region.name):
act_list.append(region)
return act_list
def is_act_blacklisted(world: World, name: str) -> bool:
plando: bool = name in world.multiworld.ActPlando[world.player].keys() \
or name in world.multiworld.ActPlando[world.player].values()
if name == "The Finale":
return not plando and world.multiworld.EndGoal[world.player].value == 1
if name == "Rush Hour":
return not plando and world.multiworld.EndGoal[world.player].value == 2
if name == "Time Rift - Tour":
return world.multiworld.ExcludeTour[world.player].value > 0
return name in blacklisted_acts.values()
def create_region(world: World, name: str) -> Region:
reg = Region(name, world.player, world.multiworld)
for (key, data) in location_table.items():
if world.is_dw_only():
break
if data.nyakuza_thug != "":
continue
if data.region == name:
if key in storybook_pages.keys() \
and world.multiworld.ShuffleStorybookPages[world.player].value == 0:
continue
location = HatInTimeLocation(world.player, key, data.id, reg)
reg.locations.append(location)
if location.name in shop_locations:
world.shop_locs.append(location.name)
world.multiworld.regions.append(reg)
return reg
def create_badge_seller(world: World) -> Region:
badge_seller = Region("Badge Seller", world.player, world.multiworld)
world.multiworld.regions.append(badge_seller)
count: int = 0
max_items: int = 0
if world.multiworld.BadgeSellerMaxItems[world.player].value > 0:
max_items = world.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value,
world.multiworld.BadgeSellerMaxItems[world.player].value)
if max_items <= 0:
world.set_badge_seller_count(0)
return badge_seller
for (key, data) in shop_locations.items():
if "Badge Seller" not in key:
continue
location = HatInTimeLocation(world.player, key, data.id, badge_seller)
badge_seller.locations.append(location)
world.shop_locs.append(location.name)
count += 1
if count >= max_items:
break
world.set_badge_seller_count(max_items)
return badge_seller
def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance:
entrance = Entrance(player, entrancename, start_region)
start_region.exits.append(entrance)
entrance.connect(exit_region)
return entrance
# Takes an entrance, removes its old connections, and reconnects it between the two regions specified.
def reconnect_regions(entrance: Entrance, start_region: Region, exit_region: Region):
if entrance in entrance.connected_region.entrances:
entrance.connected_region.entrances.remove(entrance)
if entrance in entrance.parent_region.exits:
entrance.parent_region.exits.remove(entrance)
if entrance in start_region.exits:
start_region.exits.remove(entrance)
if entrance in exit_region.entrances:
exit_region.entrances.remove(entrance)
entrance.parent_region = start_region
start_region.exits.append(entrance)
entrance.connect(exit_region)
def create_region_and_connect(world: World,
name: str, entrancename: str, connected_region: Region, is_exit: bool = True) -> Region:
reg: Region = create_region(world, name)
entrance_region: Region
exit_region: Region
if is_exit:
entrance_region = connected_region
exit_region = reg
else:
entrance_region = reg
exit_region = connected_region
connect_regions(entrance_region, exit_region, entrancename, world.player)
return reg
def get_first_chapter_region(world: World) -> Region:
start_chapter: ChapterIndex = world.multiworld.StartingChapter[world.player]
return world.multiworld.get_region(chapter_regions.get(start_chapter), world.player)
def get_act_original_chapter(world: World, act_name: str) -> Region:
return world.multiworld.get_region(act_chapters[act_name], world.player)
# Sets an act entrance in slot data by specifying the Hat_ChapterActInfo, to be used in-game
def update_chapter_act_info(world: World, original_region: Region, new_region: Region):
original_act_info = chapter_act_info[original_region.name]
new_act_info = chapter_act_info[new_region.name]
world.act_connections[original_act_info] = new_act_info
def get_shuffled_region(self, region: str) -> str:
ci: str = chapter_act_info[region]
for key, val in self.act_connections.items():
if val == ci:
for name in chapter_act_info.keys():
if chapter_act_info[name] == key:
return name
def create_thug_shops(world: World):
min_items: int = world.multiworld.NyakuzaThugMinShopItems[world.player].value
max_items: int = world.multiworld.NyakuzaThugMaxShopItems[world.player].value
count: int = -1
step: int = 0
old_name: str = ""
thug_items = world.get_nyakuza_thug_items()
for key, data in shop_locations.items():
if data.nyakuza_thug == "":
continue
if old_name != "" and old_name == data.nyakuza_thug:
continue
try:
if thug_items[data.nyakuza_thug] <= 0:
continue
except KeyError:
pass
if count == -1:
count = world.random.randint(min_items, max_items)
thug_items.setdefault(data.nyakuza_thug, count)
if count <= 0:
continue
if count >= 1:
region = world.multiworld.get_region(data.region, world.player)
loc = HatInTimeLocation(world.player, key, data.id, region)
region.locations.append(loc)
world.shop_locs.append(loc.name)
step += 1
if step >= count:
old_name = data.nyakuza_thug
step = 0
count = -1
world.set_nyakuza_thug_items(thug_items)
def create_events(world: World) -> int:
count: int = 0
for (name, data) in event_locs.items():
if not is_location_valid(world, name):
continue
item_name: str = name
if world.is_dw():
if name in snatcher_coins.keys():
name = f"{name} ({data.region})"
elif name in zero_jumps:
if get_difficulty(world) < Difficulty.HARD and name in zero_jumps_hard:
continue
if get_difficulty(world) < Difficulty.EXPERT and name in zero_jumps_expert:
continue
event: Location = create_event(name, item_name, world.multiworld.get_region(data.region, world.player), world)
event.show_in_spoiler = False
count += 1
return count
def create_event(name: str, item_name: str, region: Region, world: World) -> Location:
event = HatInTimeLocation(world.player, name, None, region)
region.locations.append(event)
event.place_locked_item(HatInTimeItem(item_name, ItemClassification.progression, None, world.player))
return event

944
worlds/ahit/Rules.py Normal file
View File

@@ -0,0 +1,944 @@
from worlds.AutoWorld import World, CollectionState
from worlds.generic.Rules import add_rule, set_rule
from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \
shop_locations, event_locs, snatcher_coins
from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HatDLC
from BaseClasses import Location, Entrance, Region
import typing
act_connections = {
"Mafia Town - Act 2": ["Mafia Town - Act 1"],
"Mafia Town - Act 3": ["Mafia Town - Act 1"],
"Mafia Town - Act 4": ["Mafia Town - Act 2", "Mafia Town - Act 3"],
"Mafia Town - Act 6": ["Mafia Town - Act 4"],
"Mafia Town - Act 7": ["Mafia Town - Act 4"],
"Mafia Town - Act 5": ["Mafia Town - Act 6", "Mafia Town - Act 7"],
"Battle of the Birds - Act 2": ["Battle of the Birds - Act 1"],
"Battle of the Birds - Act 3": ["Battle of the Birds - Act 1"],
"Battle of the Birds - Act 4": ["Battle of the Birds - Act 2", "Battle of the Birds - Act 3"],
"Battle of the Birds - Act 5": ["Battle of the Birds - Act 2", "Battle of the Birds - Act 3"],
"Battle of the Birds - Finale A": ["Battle of the Birds - Act 4", "Battle of the Birds - Act 5"],
"Battle of the Birds - Finale B": ["Battle of the Birds - Finale A"],
"Subcon Forest - Finale": ["Subcon Forest - Act 1", "Subcon Forest - Act 2",
"Subcon Forest - Act 3", "Subcon Forest - Act 4",
"Subcon Forest - Act 5"],
"The Arctic Cruise - Act 2": ["The Arctic Cruise - Act 1"],
"The Arctic Cruise - Finale": ["The Arctic Cruise - Act 2"],
}
def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool:
if world.multiworld.HatItems[world.player].value > 0:
return state.has(hat_type_to_item[hat], world.player)
return state.count("Yarn", world.player) >= get_hat_cost(world, hat)
def get_hat_cost(world: World, hat: HatType) -> int:
cost: int = 0
costs = world.get_hat_yarn_costs()
for h in world.get_hat_craft_order():
cost += costs[h]
if h == hat:
break
return cost
def can_sdj(state: CollectionState, world: World):
return can_use_hat(state, world, HatType.SPRINT)
def painting_logic(world: World) -> bool:
return world.multiworld.ShuffleSubconPaintings[world.player].value > 0
# -1 = Normal, 0 = Moderate, 1 = Hard, 2 = Expert
def get_difficulty(world: World) -> Difficulty:
return Difficulty(world.multiworld.LogicDifficulty[world.player].value)
def has_paintings(state: CollectionState, world: World, count: int, allow_skip: bool = True) -> bool:
if not painting_logic(world):
return True
if world.multiworld.NoPaintingSkips[world.player].value == 0 and allow_skip:
# In Moderate there is a very easy trick to skip all the walls, except for the one guarding the boss arena
if get_difficulty(world) >= Difficulty.MODERATE:
return True
return state.count("Progressive Painting Unlock", world.player) >= count
def zipline_logic(world: World) -> bool:
return world.multiworld.ShuffleAlpineZiplines[world.player].value > 0
def can_use_hookshot(state: CollectionState, world: World):
return state.has("Hookshot Badge", world.player)
def can_hit(state: CollectionState, world: World, umbrella_only: bool = False):
if world.multiworld.UmbrellaLogic[world.player].value == 0:
return True
return state.has("Umbrella", world.player) or not umbrella_only and can_use_hat(state, world, HatType.BREWING)
def can_surf(state: CollectionState, world: World):
return state.has("No Bonk Badge", world.player)
def has_relic_combo(state: CollectionState, world: World, relic: str) -> bool:
return state.has_group(relic, world.player, len(world.item_name_groups[relic]))
def get_relic_count(state: CollectionState, world: World, relic: str) -> int:
return state.count_group(relic, world.player)
# Only use for rifts
def can_clear_act(state: CollectionState, world: World, act_entrance: str) -> bool:
entrance: Entrance = world.multiworld.get_entrance(act_entrance, world.player)
if not state.can_reach(entrance.connected_region, "Region", world.player):
return False
if "Free Roam" in entrance.connected_region.name:
return True
name: str = f"Act Completion ({entrance.connected_region.name})"
return world.multiworld.get_location(name, world.player).access_rule(state)
def can_clear_alpine(state: CollectionState, world: World) -> bool:
return state.has("Birdhouse Cleared", world.player) and state.has("Lava Cake Cleared", world.player) \
and state.has("Windmill Cleared", world.player) and state.has("Twilight Bell Cleared", world.player)
def can_clear_metro(state: CollectionState, world: World) -> bool:
return state.has("Nyakuza Intro Cleared", world.player) \
and state.has("Yellow Overpass Station Cleared", world.player) \
and state.has("Yellow Overpass Manhole Cleared", world.player) \
and state.has("Green Clean Station Cleared", world.player) \
and state.has("Green Clean Manhole Cleared", world.player) \
and state.has("Bluefin Tunnel Cleared", world.player) \
and state.has("Pink Paw Station Cleared", world.player) \
and state.has("Pink Paw Manhole Cleared", world.player)
def set_rules(world: World):
# First, chapter access
starting_chapter = ChapterIndex(world.multiworld.StartingChapter[world.player].value)
world.set_chapter_cost(starting_chapter, 0)
# Chapter costs increase progressively. Randomly decide the chapter order, except for Finale
chapter_list: typing.List[ChapterIndex] = [ChapterIndex.MAFIA, ChapterIndex.BIRDS,
ChapterIndex.SUBCON, ChapterIndex.ALPINE]
final_chapter = ChapterIndex.FINALE
if world.multiworld.EndGoal[world.player].value == 2:
final_chapter = ChapterIndex.METRO
chapter_list.append(ChapterIndex.FINALE)
elif world.multiworld.EndGoal[world.player].value == 3:
final_chapter = None
chapter_list.append(ChapterIndex.FINALE)
if world.is_dlc1():
chapter_list.append(ChapterIndex.CRUISE)
if world.is_dlc2() and final_chapter is not ChapterIndex.METRO:
chapter_list.append(ChapterIndex.METRO)
chapter_list.remove(starting_chapter)
world.random.shuffle(chapter_list)
if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()):
index1: int = 69
index2: int = 69
pos: int
lowest_index: int
chapter_list.remove(ChapterIndex.ALPINE)
if world.is_dlc1():
index1 = chapter_list.index(ChapterIndex.CRUISE)
if world.is_dlc2() and final_chapter is not ChapterIndex.METRO:
index2 = chapter_list.index(ChapterIndex.METRO)
lowest_index = min(index1, index2)
if lowest_index == 0:
pos = 0
else:
pos = world.random.randint(0, lowest_index)
chapter_list.insert(pos, ChapterIndex.ALPINE)
if world.is_dlc1() and world.is_dlc2() and final_chapter is not ChapterIndex.METRO:
chapter_list.remove(ChapterIndex.METRO)
index = chapter_list.index(ChapterIndex.CRUISE)
if index >= len(chapter_list):
chapter_list.append(ChapterIndex.METRO)
else:
chapter_list.insert(world.random.randint(index+1, len(chapter_list)), ChapterIndex.METRO)
lowest_cost: int = world.multiworld.LowestChapterCost[world.player].value
highest_cost: int = world.multiworld.HighestChapterCost[world.player].value
cost_increment: int = world.multiworld.ChapterCostIncrement[world.player].value
min_difference: int = world.multiworld.ChapterCostMinDifference[world.player].value
last_cost: int = 0
cost: int
loop_count: int = 0
for chapter in chapter_list:
min_range: int = lowest_cost + (cost_increment * loop_count)
if min_range >= highest_cost:
min_range = highest_cost-1
value: int = world.random.randint(min_range, min(highest_cost, max(lowest_cost, last_cost + cost_increment)))
cost = world.random.randint(value, min(value + cost_increment, highest_cost))
if loop_count >= 1:
if last_cost + min_difference > cost:
cost = last_cost + min_difference
cost = min(cost, highest_cost)
world.set_chapter_cost(chapter, cost)
last_cost = cost
loop_count += 1
if final_chapter is not None:
world.set_chapter_cost(final_chapter, world.random.randint(
world.multiworld.FinalChapterMinCost[world.player].value,
world.multiworld.FinalChapterMaxCost[world.player].value))
add_rule(world.multiworld.get_entrance("Telescope -> Mafia Town", world.player),
lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.MAFIA)))
add_rule(world.multiworld.get_entrance("Telescope -> Battle of the Birds", world.player),
lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS)))
add_rule(world.multiworld.get_entrance("Telescope -> Subcon Forest", world.player),
lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.SUBCON)))
add_rule(world.multiworld.get_entrance("Telescope -> Alpine Skyline", world.player),
lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE)))
add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player),
lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE))
and can_use_hat(state, world, HatType.BREWING) and can_use_hat(state, world, HatType.DWELLER))
if world.is_dlc1():
add_rule(world.multiworld.get_entrance("Telescope -> The Arctic Cruise", world.player),
lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))
and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.CRUISE)))
if world.is_dlc2():
add_rule(world.multiworld.get_entrance("Telescope -> Nyakuza Metro", world.player),
lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))
and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.METRO))
and can_use_hat(state, world, HatType.DWELLER) and can_use_hat(state, world, HatType.ICE))
if world.multiworld.ActRandomizer[world.player].value == 0:
set_default_rift_rules(world)
table = location_table | event_locs
location: Location
for (key, data) in table.items():
if not is_location_valid(world, key):
continue
if key in contract_locations.keys():
continue
if data.dlc_flags & HatDLC.death_wish and key in snatcher_coins.keys():
key = f"{key} ({data.region})"
location = world.multiworld.get_location(key, world.player)
for hat in data.required_hats:
if hat is not HatType.NONE:
add_rule(location, lambda state, h=hat: can_use_hat(state, world, h))
if data.hookshot:
add_rule(location, lambda state: can_use_hookshot(state, world))
if data.umbrella and world.multiworld.UmbrellaLogic[world.player].value > 0:
add_rule(location, lambda state: state.has("Umbrella", world.player))
if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0:
add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings))
if data.hit_requirement > 0:
if data.hit_requirement == 1:
add_rule(location, lambda state: can_hit(state, world))
elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells)
add_rule(location, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER))
for misc in data.misc_required:
add_rule(location, lambda state, item=misc: state.has(item, world.player))
set_specific_rules(world)
# Putting all of this here, so it doesn't get overridden by anything
# Illness starts the player past the intro
alpine_entrance = world.multiworld.get_entrance("AFR -> Alpine Skyline Area", world.player)
add_rule(alpine_entrance, lambda state: can_use_hookshot(state, world))
if world.multiworld.UmbrellaLogic[world.player].value > 0:
add_rule(alpine_entrance, lambda state: state.has("Umbrella", world.player))
if zipline_logic(world):
add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player),
lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player))
add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player),
lambda state: state.has("Zipline Unlock - The Lava Cake Path", world.player))
add_rule(world.multiworld.get_entrance("-> The Windmill", world.player),
lambda state: state.has("Zipline Unlock - The Windmill Path", world.player))
add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player),
lambda state: state.has("Zipline Unlock - The Twilight Bell Path", world.player))
add_rule(world.multiworld.get_location("Act Completion (The Illness has Spread)", world.player),
lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player)
and state.has("Zipline Unlock - The Lava Cake Path", world.player)
and state.has("Zipline Unlock - The Windmill Path", world.player))
if zipline_logic(world):
for (loc, zipline) in zipline_unlocks.items():
add_rule(world.multiworld.get_location(loc, world.player),
lambda state, z=zipline: state.has(z, world.player))
for loc in world.multiworld.get_region("Alpine Skyline Area (TIHS)", world.player).locations:
if "Goat Village" in loc.name:
continue
add_rule(loc, lambda state: can_use_hookshot(state, world))
for (key, acts) in act_connections.items():
if "Arctic Cruise" in key and not world.is_dlc1():
continue
i: int = 1
entrance: Entrance = world.multiworld.get_entrance(key, world.player)
region: Region = entrance.connected_region
access_rules: typing.List[typing.Callable[[CollectionState], bool]] = []
entrance.parent_region.exits.remove(entrance)
# Entrances to this act that we have to set access_rules on
entrances: typing.List[Entrance] = []
for act in acts:
act_entrance: Entrance = world.multiworld.get_entrance(act, world.player)
access_rules.append(act_entrance.access_rule)
required_region = act_entrance.connected_region
name: str = f"{key}: Connection {i}"
new_entrance: Entrance = connect_regions(required_region, region, name, world.player)
entrances.append(new_entrance)
# Copy access rules from act completions
if "Free Roam" not in required_region.name:
rule: typing.Callable[[CollectionState], bool]
name = f"Act Completion ({required_region.name})"
rule = world.multiworld.get_location(name, world.player).access_rule
access_rules.append(rule)
i += 1
for e in entrances:
for rules in access_rules:
add_rule(e, rules)
set_event_rules(world)
if world.multiworld.EndGoal[world.player].value == 1:
world.multiworld.completion_condition[world.player] = lambda state: state.has("Time Piece Cluster", world.player)
elif world.multiworld.EndGoal[world.player].value == 2:
world.multiworld.completion_condition[world.player] = lambda state: state.has("Rush Hour Cleared", world.player)
def set_specific_rules(world: World):
add_rule(world.multiworld.get_location("Mafia Boss Shop Item", world.player),
lambda state: state.has("Time Piece", world.player, 12)
and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS)))
add_rule(world.multiworld.get_location("Spaceship - Rumbi Abuse", world.player),
lambda state: state.has("Time Piece", world.player, 4))
set_mafia_town_rules(world)
set_botb_rules(world)
set_subcon_rules(world)
set_alps_rules(world)
if world.is_dlc1():
set_dlc1_rules(world)
if world.is_dlc2():
set_dlc2_rules(world)
difficulty: Difficulty = get_difficulty(world)
if difficulty >= Difficulty.MODERATE:
set_moderate_rules(world)
if difficulty >= Difficulty.HARD:
set_hard_rules(world)
if difficulty >= 2:
set_expert_rules(world)
def set_moderate_rules(world: World):
# Moderate: Gallery without Brewing Hat
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Gallery)", world.player), lambda state: True)
# Moderate: Above Boats via Ice Hat Sliding
add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player),
lambda state: can_use_hat(state, world, HatType.ICE), "or")
# Moderate: Clock Tower Chest + Ruined Tower with nothing
add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
# Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell
for loc in world.multiworld.get_region("The Subcon Well", world.player).locations:
set_rule(loc, lambda state: has_paintings(state, world, 1))
# Moderate: Vanessa Manor with nothing
for loc in world.multiworld.get_region("Queen Vanessa's Manor", world.player).locations:
set_rule(loc, lambda state: True)
set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), lambda state: True)
# Moderate: get to Birdhouse/Yellow Band Hills without Brewing Hat
set_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player),
lambda state: can_use_hookshot(state, world))
set_rule(world.multiworld.get_location("Alpine Skyline - Yellow Band Hills", world.player),
lambda state: can_use_hookshot(state, world))
# Moderate: The Birdhouse - Dweller Platforms Relic with only Birdhouse access
set_rule(world.multiworld.get_location("Alpine Skyline - The Birdhouse: Dweller Platforms Relic", world.player),
lambda state: True)
# Moderate: Twilight Path without Dweller Mask
set_rule(world.multiworld.get_location("Alpine Skyline - The Twilight Path", world.player), lambda state: True)
# Moderate: Mystifying Time Mesa time trial without hats
set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player),
lambda state: can_use_hookshot(state, world))
# Moderate: Finale without Hookshot
set_rule(world.multiworld.get_location("Act Completion (The Finale)", world.player),
lambda state: can_use_hat(state, world, HatType.DWELLER))
if world.is_dlc1():
# Moderate: clear Rock the Boat without Ice Hat
add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
# Moderate: clear Deep Sea without Ice Hat
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player),
lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER))
# There is a glitched fall damage volume near the Yellow Overpass time piece that warps the player to Pink Paw.
# Yellow Overpass time piece can also be reached without Hookshot quite easily.
if world.is_dlc2():
set_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), lambda state: True)
set_rule(world.multiworld.get_location("Act Completion (Yellow Overpass Station)", world.player),
lambda state: True)
set_rule(world.multiworld.get_location("Pink Paw Station - Cat Vacuum", world.player), lambda state: True)
# The player can quite literally walk past the fan from the side without Time Stop.
set_rule(world.multiworld.get_location("Pink Paw Station - Behind Fan", world.player), lambda state: True)
# Moderate: clear Rush Hour without Hookshot
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
lambda state: state.has("Metro Ticket - Pink", world.player)
and state.has("Metro Ticket - Yellow", world.player)
and state.has("Metro Ticket - Blue", world.player)
and can_use_hat(state, world, HatType.ICE)
and can_use_hat(state, world, HatType.BREWING))
# Moderate: Bluefin Tunnel without tickets
set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True)
def set_hard_rules(world: World):
# Hard: clear Time Rift - The Twilight Bell with Sprint+Scooter only
add_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player),
lambda state: can_use_hat(state, world, HatType.SPRINT)
and state.has("Scooter Badge", world.player), "or")
# No Dweller Mask required
set_rule(world.multiworld.get_location("Subcon Forest - Dweller Floating Rocks", world.player),
lambda state: has_paintings(state, world, 3))
# Cherry bridge over boss arena gap (painting still expected)
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
lambda state: has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player))
# SDJ
add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player),
lambda state: can_sdj(state, world) and has_paintings(state, world, 2), "or")
add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player),
lambda state: has_paintings(state, world, 3) and can_sdj(state, world), "or")
add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player),
lambda state: can_sdj(state, world), "or")
# Finale Telescope with only Ice Hat
add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player),
lambda state: can_use_hat(state, world, HatType.ICE), "or")
if world.is_dlc1():
# Hard: clear Deep Sea without Dweller Mask
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player),
lambda state: can_use_hookshot(state, world))
if world.is_dlc2():
# Hard: clear Green Clean Manhole without Dweller Mask
set_rule(world.multiworld.get_location("Act Completion (Green Clean Manhole)", world.player),
lambda state: can_use_hat(state, world, HatType.ICE))
# Hard: clear Rush Hour with Brewing Hat only
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
lambda state: can_use_hat(state, world, HatType.BREWING))
def set_expert_rules(world: World):
# Finale Telescope with no hats
set_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player),
lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE)))
# Expert: Mafia Town - Above Boats with nothing
set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True)
# Expert: Clear Dead Bird Studio with nothing
for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations:
set_rule(loc, lambda state: True)
set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player), lambda state: True)
# Expert: get to and clear Twilight Bell without Dweller Mask.
# Dweller Mask OR Sprint Hat OR Brewing Hat OR Time Stop + Umbrella required to complete act.
add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player),
lambda state: can_use_hookshot(state, world), "or")
add_rule(world.multiworld.get_location("Act Completion (The Twilight Bell)", world.player),
lambda state: can_use_hat(state, world, HatType.BREWING)
or can_use_hat(state, world, HatType.DWELLER)
or can_use_hat(state, world, HatType.SPRINT)
or (can_use_hat(state, world, HatType.TIME_STOP) and state.has("Umbrella", world.player)))
# Expert: Time Rift - Curly Tail Trail with nothing
# Time Rift - Twilight Bell and Time Rift - Village with nothing
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player),
lambda state: True)
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: True)
set_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player),
lambda state: True)
# Expert: Cherry Hovering
entrance = connect_regions(world.multiworld.get_region("Your Contract has Expired", world.player),
world.multiworld.get_region("Subcon Forest Area", world.player),
"Subcon Forest Entrance YCHE", world.player)
if world.multiworld.NoPaintingSkips[world.player].value > 0:
add_rule(entrance, lambda state: has_paintings(state, world, 1))
set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player),
lambda state: can_use_hookshot(state, world) and can_hit(state, world)
and has_paintings(state, world, 1, True))
# Set painting rules only. Skipping paintings is determined in has_paintings
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
lambda state: has_paintings(state, world, 1, True))
set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player),
lambda state: has_paintings(state, world, 2, True))
set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player),
lambda state: has_paintings(state, world, 2, True))
set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player),
lambda state: has_paintings(state, world, 3, True))
set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player),
lambda state: has_paintings(state, world, 3, True))
# You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him
connect_regions(world.multiworld.get_region("Subcon Forest Area", world.player),
world.multiworld.get_region("Your Contract has Expired", world.player),
"Snatcher Hover", world.player)
set_rule(world.multiworld.get_location("Act Completion (Your Contract has Expired)", world.player),
lambda state: True)
if world.is_dlc2():
# Expert: clear Rush Hour with nothing
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True)
def set_mafia_town_rules(world: World):
add_rule(world.multiworld.get_location("Mafia Town - Behind HQ Chest", world.player),
lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", world.player)
or state.can_reach("Down with the Mafia!", "Region", world.player)
or state.can_reach("Cheating the Race", "Region", world.player)
or state.can_reach("The Golden Vault", "Region", world.player))
# Old guys don't appear in SCFOS
add_rule(world.multiworld.get_location("Mafia Town - Old Man (Steel Beams)", world.player),
lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player)
or state.can_reach("Barrel Battle", "Region", world.player)
or state.can_reach("Cheating the Race", "Region", world.player)
or state.can_reach("The Golden Vault", "Region", world.player)
or state.can_reach("Down with the Mafia!", "Region", world.player))
add_rule(world.multiworld.get_location("Mafia Town - Old Man (Seaside Spaghetti)", world.player),
lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player)
or state.can_reach("Barrel Battle", "Region", world.player)
or state.can_reach("Cheating the Race", "Region", world.player)
or state.can_reach("The Golden Vault", "Region", world.player)
or state.can_reach("Down with the Mafia!", "Region", world.player))
# Only available outside She Came from Outer Space
add_rule(world.multiworld.get_location("Mafia Town - Mafia Geek Platform", world.player),
lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player)
or state.can_reach("Barrel Battle", "Region", world.player)
or state.can_reach("Down with the Mafia!", "Region", world.player)
or state.can_reach("Cheating the Race", "Region", world.player)
or state.can_reach("Heating Up Mafia Town", "Region", world.player)
or state.can_reach("The Golden Vault", "Region", world.player))
# Only available outside Down with the Mafia! (for some reason)
add_rule(world.multiworld.get_location("Mafia Town - On Scaffolding", world.player),
lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player)
or state.can_reach("Barrel Battle", "Region", world.player)
or state.can_reach("She Came from Outer Space", "Region", world.player)
or state.can_reach("Cheating the Race", "Region", world.player)
or state.can_reach("Heating Up Mafia Town", "Region", world.player)
or state.can_reach("The Golden Vault", "Region", world.player))
# For some reason, the brewing crate is removed in HUMT
add_rule(world.multiworld.get_location("Mafia Town - Secret Cave", world.player),
lambda state: state.has("HUMT Access", world.player), "or")
# Can bounce across the lava to get this without Hookshot (need to die though)
add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player),
lambda state: state.has("HUMT Access", world.player), "or")
ctr_logic: int = world.multiworld.CTRLogic[world.player].value
if ctr_logic == 3:
set_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), lambda state: True)
elif ctr_logic == 2:
add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player),
lambda state: can_use_hat(state, world, HatType.SPRINT), "or")
elif ctr_logic == 1:
add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player),
lambda state: can_use_hat(state, world, HatType.SPRINT)
and state.has("Scooter Badge", world.player), "or")
def set_botb_rules(world: World):
if world.multiworld.UmbrellaLogic[world.player].value == 0 and get_difficulty(world) < Difficulty.MODERATE:
set_rule(world.multiworld.get_location("Dead Bird Studio - DJ Grooves Sign Chest", world.player),
lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING))
set_rule(world.multiworld.get_location("Dead Bird Studio - Tepee Chest", world.player),
lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING))
set_rule(world.multiworld.get_location("Dead Bird Studio - Conductor Chest", world.player),
lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING))
set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player),
lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING))
def set_subcon_rules(world: World):
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player),
lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player)
or can_use_hat(state, world, HatType.DWELLER))
# You can't skip over the boss arena wall without cherry hover, so these two need to be set this way
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world)
and has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player))
# The painting wall can't be skipped without cherry hover, which is Expert
set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player),
lambda state: can_use_hookshot(state, world) and can_hit(state, world)
and has_paintings(state, world, 1, False))
add_rule(world.multiworld.get_entrance("Subcon Forest - Act 2", world.player),
lambda state: state.has("Snatcher's Contract - The Subcon Well", world.player))
add_rule(world.multiworld.get_entrance("Subcon Forest - Act 3", world.player),
lambda state: state.has("Snatcher's Contract - Toilet of Doom", world.player))
add_rule(world.multiworld.get_entrance("Subcon Forest - Act 4", world.player),
lambda state: state.has("Snatcher's Contract - Queen Vanessa's Manor", world.player))
add_rule(world.multiworld.get_entrance("Subcon Forest - Act 5", world.player),
lambda state: state.has("Snatcher's Contract - Mail Delivery Service", world.player))
if painting_logic(world):
add_rule(world.multiworld.get_location("Act Completion (Contractual Obligations)", world.player),
lambda state: has_paintings(state, world, 1, False))
for key in contract_locations:
if key == "Snatcher's Contract - The Subcon Well":
continue
add_rule(world.multiworld.get_location(key, world.player), lambda state: has_paintings(state, world, 1))
def set_alps_rules(world: World):
add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player),
lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.BREWING))
add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player),
lambda state: can_use_hookshot(state, world))
add_rule(world.multiworld.get_entrance("-> The Windmill", world.player),
lambda state: can_use_hookshot(state, world))
add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player),
lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER))
add_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player),
lambda state: can_use_hat(state, world, HatType.SPRINT) or can_use_hat(state, world, HatType.TIME_STOP))
add_rule(world.multiworld.get_entrance("Alpine Skyline - Finale", world.player),
lambda state: can_clear_alpine(state, world))
def set_dlc1_rules(world: World):
add_rule(world.multiworld.get_entrance("Cruise Ship Entrance BV", world.player),
lambda state: can_use_hookshot(state, world))
# This particular item isn't present in Act 3 for some reason, yes in vanilla too
add_rule(world.multiworld.get_location("The Arctic Cruise - Toilet", world.player),
lambda state: state.can_reach("Bon Voyage!", "Region", world.player)
or state.can_reach("Ship Shape", "Region", world.player))
def set_dlc2_rules(world: World):
add_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player),
lambda state: state.has("Metro Ticket - Green", world.player)
or state.has("Metro Ticket - Blue", world.player))
add_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player),
lambda state: state.has("Metro Ticket - Pink", world.player)
or state.has("Metro Ticket - Yellow", world.player) and state.has("Metro Ticket - Blue", world.player))
add_rule(world.multiworld.get_entrance("Nyakuza Metro - Finale", world.player),
lambda state: can_clear_metro(state, world))
add_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
lambda state: state.has("Metro Ticket - Yellow", world.player)
and state.has("Metro Ticket - Blue", world.player)
and state.has("Metro Ticket - Pink", world.player))
for key in shop_locations.keys():
if "Green Clean Station Thug B" in key and is_location_valid(world, key):
add_rule(world.multiworld.get_location(key, world.player),
lambda state: state.has("Metro Ticket - Yellow", world.player), "or")
def reg_act_connection(world: World, region: typing.Union[str, Region], unlocked_entrance: typing.Union[str, Entrance]):
reg: Region
entrance: Entrance
if isinstance(region, str):
reg = world.multiworld.get_region(region, world.player)
else:
reg = region
if isinstance(unlocked_entrance, str):
entrance = world.multiworld.get_entrance(unlocked_entrance, world.player)
else:
entrance = unlocked_entrance
world.multiworld.register_indirect_condition(reg, entrance)
# See randomize_act_entrances in Regions.py
# Called before set_rules
def set_rift_rules(world: World, regions: typing.Dict[str, Region]):
# This is accessing the regions in place of these time rifts, so we can set the rules on all the entrances.
for entrance in regions["Time Rift - Gallery"].entrances:
add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING)
and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS)))
for entrance in regions["Time Rift - The Lab"].entrances:
add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER)
and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE)))
for entrance in regions["Time Rift - Sewers"].entrances:
add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 4"))
reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 4",
world.player).connected_region, entrance)
for entrance in regions["Time Rift - Bazaar"].entrances:
add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 6"))
reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 6",
world.player).connected_region, entrance)
for entrance in regions["Time Rift - Mafia of Cooks"].entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger"))
for entrance in regions["Time Rift - The Owl Express"].entrances:
add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 2"))
add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 3"))
reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 2",
world.player).connected_region, entrance)
reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 3",
world.player).connected_region, entrance)
for entrance in regions["Time Rift - The Moon"].entrances:
add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 4"))
add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 5"))
reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 4",
world.player).connected_region, entrance)
reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 5",
world.player).connected_region, entrance)
for entrance in regions["Time Rift - Dead Bird Studio"].entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "Train"))
for entrance in regions["Time Rift - Pipe"].entrances:
add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 2"))
reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 2",
world.player).connected_region, entrance)
if painting_logic(world):
add_rule(entrance, lambda state: has_paintings(state, world, 2))
for entrance in regions["Time Rift - Village"].entrances:
add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 4"))
reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 4",
world.player).connected_region, entrance)
if painting_logic(world):
add_rule(entrance, lambda state: has_paintings(state, world, 2))
for entrance in regions["Time Rift - Sleepy Subcon"].entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO"))
if painting_logic(world):
add_rule(entrance, lambda state: has_paintings(state, world, 3))
for entrance in regions["Time Rift - Curly Tail Trail"].entrances:
add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player))
for entrance in regions["Time Rift - The Twilight Bell"].entrances:
add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", world.player))
for entrance in regions["Time Rift - Alpine Skyline"].entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
if world.is_dlc1() > 0:
for entrance in regions["Time Rift - Balcony"].entrances:
add_rule(entrance, lambda state: can_clear_act(state, world, "The Arctic Cruise - Finale"))
for entrance in regions["Time Rift - Deep Sea"].entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake"))
if world.is_dlc2() > 0:
for entrance in regions["Time Rift - Rumbi Factory"].entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace"))
# Basically the same as above, but without the need of the dict since we are just setting defaults
# Called if Act Rando is disabled
def set_default_rift_rules(world: World):
for entrance in world.multiworld.get_region("Time Rift - Gallery", world.player).entrances:
add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING)
and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS)))
for entrance in world.multiworld.get_region("Time Rift - The Lab", world.player).entrances:
add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER)
and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE)))
for entrance in world.multiworld.get_region("Time Rift - Sewers", world.player).entrances:
add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 4"))
reg_act_connection(world, "Down with the Mafia!", entrance.name)
for entrance in world.multiworld.get_region("Time Rift - Bazaar", world.player).entrances:
add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 6"))
reg_act_connection(world, "Heating Up Mafia Town", entrance.name)
for entrance in world.multiworld.get_region("Time Rift - Mafia of Cooks", world.player).entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger"))
for entrance in world.multiworld.get_region("Time Rift - The Owl Express", world.player).entrances:
add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 2"))
add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 3"))
reg_act_connection(world, "Murder on the Owl Express", entrance.name)
reg_act_connection(world, "Picture Perfect", entrance.name)
for entrance in world.multiworld.get_region("Time Rift - The Moon", world.player).entrances:
add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 4"))
add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 5"))
reg_act_connection(world, "Train Rush", entrance.name)
reg_act_connection(world, "The Big Parade", entrance.name)
for entrance in world.multiworld.get_region("Time Rift - Dead Bird Studio", world.player).entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "Train"))
for entrance in world.multiworld.get_region("Time Rift - Pipe", world.player).entrances:
add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 2"))
reg_act_connection(world, "The Subcon Well", entrance.name)
if painting_logic(world):
add_rule(entrance, lambda state: has_paintings(state, world, 2))
for entrance in world.multiworld.get_region("Time Rift - Village", world.player).entrances:
add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 4"))
reg_act_connection(world, "Queen Vanessa's Manor", entrance.name)
if painting_logic(world):
add_rule(entrance, lambda state: has_paintings(state, world, 2))
for entrance in world.multiworld.get_region("Time Rift - Sleepy Subcon", world.player).entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO"))
if painting_logic(world):
add_rule(entrance, lambda state: has_paintings(state, world, 3))
for entrance in world.multiworld.get_region("Time Rift - Curly Tail Trail", world.player).entrances:
add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player))
for entrance in world.multiworld.get_region("Time Rift - The Twilight Bell", world.player).entrances:
add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", world.player))
for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
if world.is_dlc1():
for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances:
add_rule(entrance, lambda state: can_clear_act(state, world, "The Arctic Cruise - Finale"))
for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake"))
if world.is_dlc2():
for entrance in world.multiworld.get_region("Time Rift - Rumbi Factory", world.player).entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace"))
def set_event_rules(world: World):
for (name, data) in event_locs.items():
if not is_location_valid(world, name):
continue
if data.dlc_flags & HatDLC.death_wish and name in snatcher_coins.keys():
name = f"{name} ({data.region})"
event: Location = world.multiworld.get_location(name, world.player)
if data.act_event:
add_rule(event, world.multiworld.get_location(f"Act Completion ({data.region})", world.player).access_rule)
def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance:
entrance = Entrance(player, entrancename, start_region)
start_region.exits.append(entrance)
entrance.connect(exit_region)
return entrance

80
worlds/ahit/Types.py Normal file
View File

@@ -0,0 +1,80 @@
from enum import IntEnum, IntFlag
from typing import NamedTuple, Optional, List
from BaseClasses import Location, Item, ItemClassification
class HatInTimeLocation(Location):
game: str = "A Hat in Time"
class HatInTimeItem(Item):
game: str = "A Hat in Time"
class HatType(IntEnum):
NONE = -1
SPRINT = 0
BREWING = 1
ICE = 2
DWELLER = 3
TIME_STOP = 4
class HatDLC(IntFlag):
none = 0b000
dlc1 = 0b001
dlc2 = 0b010
death_wish = 0b100
dlc1_dw = 0b101
dlc2_dw = 0b110
class ChapterIndex(IntEnum):
SPACESHIP = 0
MAFIA = 1
BIRDS = 2
SUBCON = 3
ALPINE = 4
FINALE = 5
CRUISE = 6
METRO = 7
class Difficulty(IntEnum):
NORMAL = -1
MODERATE = 0
HARD = 1
EXPERT = 2
class LocData(NamedTuple):
id: Optional[int] = 0
region: Optional[str] = ""
required_hats: Optional[List[HatType]] = [HatType.NONE]
hookshot: Optional[bool] = False
dlc_flags: Optional[HatDLC] = HatDLC.none
paintings: Optional[int] = 0 # Paintings required for Subcon painting shuffle
misc_required: Optional[List[str]] = []
# For UmbrellaLogic setting
umbrella: Optional[bool] = False # Umbrella required for this check
hit_requirement: Optional[int] = 0 # Hit required. 1 = Umbrella/Brewing only, 2 = bypass w/Dweller Mask (bells)
# Other
act_event: Optional[bool] = False # Only used for event locations. Copy access rule from act completion
nyakuza_thug: Optional[str] = "" # Name of Nyakuza thug NPC (for metro shops)
class ItemData(NamedTuple):
code: Optional[int]
classification: ItemClassification
dlc_flags: Optional[HatDLC] = HatDLC.none
hat_type_to_item = {
HatType.SPRINT: "Sprint Hat",
HatType.BREWING: "Brewing Hat",
HatType.ICE: "Ice Hat",
HatType.DWELLER: "Dweller Mask",
HatType.TIME_STOP: "Time Stop Hat",
}

334
worlds/ahit/__init__.py Normal file
View File

@@ -0,0 +1,334 @@
from BaseClasses import Item, ItemClassification, Tutorial
from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool
from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID
from .Rules import set_rules
from .Options import ahit_options, slot_data_options, adjust_options
from .Types import HatType, ChapterIndex, HatInTimeItem
from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes
from .DeathWishRules import set_dw_rules, create_enemy_events
from worlds.AutoWorld import World, WebWorld
from typing import List, Dict, TextIO
from worlds.LauncherComponents import Component, components, icon_paths
from Utils import local_path
hat_craft_order: Dict[int, List[HatType]] = {}
hat_yarn_costs: Dict[int, Dict[HatType, int]] = {}
chapter_timepiece_costs: Dict[int, Dict[ChapterIndex, int]] = {}
excluded_dws: Dict[int, List[str]] = {}
excluded_bonuses: Dict[int, List[str]] = {}
dw_shuffle: Dict[int, List[str]] = {}
nyakuza_thug_items: Dict[int, Dict[str, int]] = {}
badge_seller_count: Dict[int, int] = {}
components.append(Component("A Hat in Time Client", "AHITClient", icon='yatta'))
icon_paths['yatta'] = local_path('data', 'yatta.png')
class AWebInTime(WebWorld):
theme = "partyTime"
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide for setting up A Hat in Time to be played in Archipelago.",
"English",
"ahit_en.md",
"setup/en",
["CookieCat"]
)]
class HatInTimeWorld(World):
"""
A Hat in Time is a cute-as-peck 3D platformer featuring a little girl who stitches hats for wicked powers!
Freely explore giant worlds and recover Time Pieces to travel to new heights!
"""
game = "A Hat in Time"
data_version = 1
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = get_location_names()
option_definitions = ahit_options
act_connections: Dict[str, str] = {}
shop_locs: List[str] = []
item_name_groups = relic_groups
web = AWebInTime()
def generate_early(self):
adjust_options(self)
if self.multiworld.StartWithCompassBadge[self.player].value > 0:
self.multiworld.push_precollected(self.create_item("Compass Badge"))
if self.is_dw_only():
return
# If our starting chapter is 4 and act rando isn't on, force hookshot into inventory
# If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock
start_chapter: int = self.multiworld.StartingChapter[self.player].value
if start_chapter == 4 or start_chapter == 3:
if self.multiworld.ActRandomizer[self.player].value == 0:
if start_chapter == 4:
self.multiworld.push_precollected(self.create_item("Hookshot Badge"))
if start_chapter == 3 and self.multiworld.ShuffleSubconPaintings[self.player].value > 0:
self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock"))
def create_regions(self):
excluded_dws[self.player] = []
excluded_bonuses[self.player] = []
dw_shuffle[self.player] = []
nyakuza_thug_items[self.player] = {}
badge_seller_count[self.player] = 0
self.shop_locs = []
self.topology_present = self.multiworld.ActRandomizer[self.player].value
create_regions(self)
if self.multiworld.EnableDeathWish[self.player].value > 0:
create_dw_regions(self)
if self.is_dw_only():
return
create_events(self)
if self.is_dw():
if "Snatcher's Hit List" not in self.get_excluded_dws() \
or "Camera Tourist" not in self.get_excluded_dws():
create_enemy_events(self)
# place vanilla contract locations if contract shuffle is off
if self.multiworld.ShuffleActContracts[self.player].value == 0:
for name in contract_locations.keys():
self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name))
def create_items(self):
hat_yarn_costs[self.player] = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1,
HatType.DWELLER: -1, HatType.TIME_STOP: -1}
hat_craft_order[self.player] = [HatType.SPRINT, HatType.BREWING, HatType.ICE,
HatType.DWELLER, HatType.TIME_STOP]
if self.multiworld.HatItems[self.player].value == 0 and self.multiworld.RandomizeHatOrder[self.player].value > 0:
self.random.shuffle(hat_craft_order[self.player])
if self.multiworld.RandomizeHatOrder[self.player].value == 2:
hat_craft_order[self.player].remove(HatType.TIME_STOP)
hat_craft_order[self.player].append(HatType.TIME_STOP)
self.multiworld.itempool += create_itempool(self)
def set_rules(self):
self.act_connections = {}
chapter_timepiece_costs[self.player] = {ChapterIndex.MAFIA: -1,
ChapterIndex.BIRDS: -1,
ChapterIndex.SUBCON: -1,
ChapterIndex.ALPINE: -1,
ChapterIndex.FINALE: -1,
ChapterIndex.CRUISE: -1,
ChapterIndex.METRO: -1}
if self.is_dw_only():
# we already have all items if this is the case, no need for rules
self.multiworld.push_precollected(HatInTimeItem("Death Wish Only Mode", ItemClassification.progression,
None, self.player))
self.multiworld.completion_condition[self.player] = lambda state: state.has("Death Wish Only Mode",
self.player)
if self.multiworld.DWEnableBonus[self.player].value == 0:
for name in death_wishes:
if name == "Snatcher Coins in Nyakuza Metro" and not self.is_dlc2():
continue
if self.multiworld.DWShuffle[self.player].value > 0 and name not in self.get_dw_shuffle():
continue
full_clear = self.multiworld.get_location(f"{name} - All Clear", self.player)
full_clear.address = None
full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, self.player))
full_clear.show_in_spoiler = False
return
if self.multiworld.ActRandomizer[self.player].value > 0:
randomize_act_entrances(self)
set_rules(self)
if self.is_dw():
set_dw_rules(self)
def create_item(self, name: str) -> Item:
return create_item(self, name)
def fill_slot_data(self) -> dict:
slot_data: dict = {"Chapter1Cost": chapter_timepiece_costs[self.player][ChapterIndex.MAFIA],
"Chapter2Cost": chapter_timepiece_costs[self.player][ChapterIndex.BIRDS],
"Chapter3Cost": chapter_timepiece_costs[self.player][ChapterIndex.SUBCON],
"Chapter4Cost": chapter_timepiece_costs[self.player][ChapterIndex.ALPINE],
"Chapter5Cost": chapter_timepiece_costs[self.player][ChapterIndex.FINALE],
"Chapter6Cost": chapter_timepiece_costs[self.player][ChapterIndex.CRUISE],
"Chapter7Cost": chapter_timepiece_costs[self.player][ChapterIndex.METRO],
"BadgeSellerItemCount": badge_seller_count[self.player],
"SeedNumber": str(self.multiworld.seed), # For shop prices
"SeedName": self.multiworld.seed_name}
if self.multiworld.HatItems[self.player].value == 0:
slot_data.setdefault("SprintYarnCost", hat_yarn_costs[self.player][HatType.SPRINT])
slot_data.setdefault("BrewingYarnCost", hat_yarn_costs[self.player][HatType.BREWING])
slot_data.setdefault("IceYarnCost", hat_yarn_costs[self.player][HatType.ICE])
slot_data.setdefault("DwellerYarnCost", hat_yarn_costs[self.player][HatType.DWELLER])
slot_data.setdefault("TimeStopYarnCost", hat_yarn_costs[self.player][HatType.TIME_STOP])
slot_data.setdefault("Hat1", int(hat_craft_order[self.player][0]))
slot_data.setdefault("Hat2", int(hat_craft_order[self.player][1]))
slot_data.setdefault("Hat3", int(hat_craft_order[self.player][2]))
slot_data.setdefault("Hat4", int(hat_craft_order[self.player][3]))
slot_data.setdefault("Hat5", int(hat_craft_order[self.player][4]))
if self.multiworld.ActRandomizer[self.player].value > 0:
for name in self.act_connections.keys():
slot_data[name] = self.act_connections[name]
if self.is_dlc2() and not self.is_dw_only():
for name in nyakuza_thug_items[self.player].keys():
slot_data[name] = nyakuza_thug_items[self.player][name]
if self.is_dw():
i: int = 0
for name in excluded_dws[self.player]:
if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal":
continue
slot_data[f"excluded_dw{i}"] = dw_classes[name]
i += 1
i = 0
if self.multiworld.DWAutoCompleteBonuses[self.player].value == 0:
for name in excluded_bonuses[self.player]:
if name in excluded_dws[self.player]:
continue
slot_data[f"excluded_bonus{i}"] = dw_classes[name]
i += 1
if self.multiworld.DWShuffle[self.player].value > 0:
shuffled_dws = self.get_dw_shuffle()
for i in range(len(shuffled_dws)):
slot_data[f"dw_{i}"] = dw_classes[shuffled_dws[i]]
for option_name in slot_data_options:
option = getattr(self.multiworld, option_name)[self.player]
slot_data[option_name] = option.value
return slot_data
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
if self.is_dw_only() or self.multiworld.ActRandomizer[self.player].value == 0:
return
new_hint_data = {}
alpine_regions = ["The Birdhouse", "The Lava Cake", "The Windmill", "The Twilight Bell", "Alpine Skyline Area"]
metro_regions = ["Yellow Overpass Station", "Green Clean Station", "Bluefin Tunnel", "Pink Paw Station"]
for key, data in location_table.items():
if not is_location_valid(self, key):
continue
location = self.multiworld.get_location(key, self.player)
region_name: str
if data.region in alpine_regions:
region_name = "Alpine Free Roam"
elif data.region in metro_regions:
region_name = "Nyakuza Free Roam"
elif data.region in chapter_act_info.keys():
region_name = location.parent_region.name
else:
continue
new_hint_data[location.address] = get_shuffled_region(self, region_name)
if self.is_dlc1() and self.multiworld.Tasksanity[self.player].value > 0:
ship_shape_region = get_shuffled_region(self, "Ship Shape")
id_start: int = TASKSANITY_START_ID
for i in range(self.multiworld.TasksanityCheckCount[self.player].value):
new_hint_data[id_start+i] = ship_shape_region
hint_data[self.player] = new_hint_data
def write_spoiler_header(self, spoiler_handle: TextIO):
for i in self.get_chapter_costs():
spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.get_chapter_costs()[ChapterIndex(i)]))
for hat in hat_craft_order[self.player]:
spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, hat_yarn_costs[self.player][hat]))
def set_chapter_cost(self, chapter: ChapterIndex, cost: int):
chapter_timepiece_costs[self.player][chapter] = cost
def get_chapter_cost(self, chapter: ChapterIndex) -> int:
return chapter_timepiece_costs[self.player][chapter]
def get_hat_craft_order(self):
return hat_craft_order[self.player]
def get_hat_yarn_costs(self):
return hat_yarn_costs[self.player]
def get_chapter_costs(self):
return chapter_timepiece_costs[self.player]
def is_dlc1(self) -> bool:
return self.multiworld.EnableDLC1[self.player].value > 0
def is_dlc2(self) -> bool:
return self.multiworld.EnableDLC2[self.player].value > 0
def is_dw(self) -> bool:
return self.multiworld.EnableDeathWish[self.player].value > 0
def is_dw_only(self) -> bool:
return self.is_dw() and self.multiworld.DeathWishOnly[self.player].value > 0
def get_excluded_dws(self):
return excluded_dws[self.player]
def get_excluded_bonuses(self):
return excluded_bonuses[self.player]
def is_dw_excluded(self, name: str) -> bool:
# don't exclude Seal the Deal if it's our goal
if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal" \
and f"{name} - Main Objective" not in self.multiworld.exclude_locations[self.player]:
return False
if name in excluded_dws[self.player]:
return True
return f"{name} - Main Objective" in self.multiworld.exclude_locations[self.player]
def is_bonus_excluded(self, name: str) -> bool:
if self.is_dw_excluded(name) or name in excluded_bonuses[self.player]:
return True
return f"{name} - All Clear" in self.multiworld.exclude_locations[self.player]
def get_dw_shuffle(self):
return dw_shuffle[self.player]
def set_dw_shuffle(self, shuffle: List[str]):
dw_shuffle[self.player] = shuffle
def get_badge_seller_count(self) -> int:
return badge_seller_count[self.player]
def set_badge_seller_count(self, value: int):
badge_seller_count[self.player] = value
def get_nyakuza_thug_items(self):
return nyakuza_thug_items[self.player]
def set_nyakuza_thug_items(self, items: Dict[str, int]):
nyakuza_thug_items[self.player] = items

View File

@@ -0,0 +1,31 @@
# A Hat in Time
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Items which the player would normally acquire throughout the game have been moved around. Chapter costs are randomized in a progressive order based on your settings, so for example you could go to Subcon Forest -> Battle of the Birds -> Alpine Skyline, etc. in that order. If act shuffle is turned on, the levels and Time Rifts in these chapters will be randomized as well.
To unlock and access a chapter's Time Rift in act shuffle, the levels in place of the original acts required to unlock the Time Rift in the vanilla game must be completed, and then you must enter a level that allows you to enter that Time Rift. For example, Time Rift: Bazaar requires Heating Up Mafia Town to be completed in the vanilla game. To unlock this Time Rift in act shuffle (and therefore the level it contains) you must complete the level that was shuffled in place of Heating Up Mafia Town and then enter the Time Rift through a Mafia Town level.
## What items and locations get shuffled?
Time Pieces, Relics, Yarn, Badges, and most other items are shuffled. Unlike in the vanilla game, yarn is typeless, and will be automatically crafted in a set order once you gather enough yarn for each hat. Any items in the world, shops, act completions, and optionally storybook pages or Death Wish contracts are the locations.
Any freestanding items that are considered to be progression or useful will have a rainbow streak particle attached to them. Filler items will have a white glow attached to them instead.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit
certain items to your own world.
## What does another world's item look like in A Hat in Time?
Items belonging to other worlds are represented by a badge with the Archipelago logo on it.
## When the player receives an item, what happens?
When the player receives an item, it will play the item collect effect and information about the item will be printed on the screen and in the in-game developer console.

View File

@@ -0,0 +1,43 @@
# Setup Guide for A Hat in Time in Archipelago
## Required Software
- [Steam release of A Hat in Time](https://store.steampowered.com/app/253230/A_Hat_in_Time/)
- [Archipelago Workshop Mod for A Hat in Time](https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601)
## Instructions
1. Have Steam running. Open the Steam console with [this link.](steam://open/console)
2. In the Steam console, enter the following command:
`download_depot 253230 253232 7770543545116491859`. Wait for the console to say the download is finished.
3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder.
4. There should be a folder named `depot_253232`. Rename it to HatinTime_AP and move it to your `steamapps/common` folder.
5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`. In this new text file, input the number **253230** on the first line.
6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like. You will use this shortcut to open the Archipelago-compatible version of A Hat in Time.
7. Start up the game using your new shortcut. To confirm if you are on the correct version, go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked.
## Connecting to the Archipelago server
When you create a new save file, you should be prompted to enter your slot name, password, and Archipelago server address:port after loading into the Spaceship. Once that's done, the game will automatically connect to the multiserver using the info you entered whenever that save file is loaded. If you must change the IP or port for the save file, use the `ap_set_connection_info` console command.
## Console Commands
Commands will not work on the title screen, you must be in-game to use them. To use console commands, make sure ***Enable Developer Console*** is checked in Game Settings and press the tilde key or TAB while in-game.
`ap_say <message>` - Send a chat message to the server. Supports commands, such as !hint or !release.
`ap_deathlink` - Toggle Death Link.
`ap_set_connection_info <ip> <port>` - Set the connection info for the save file. The IP address MUST BE IN QUOTES!
`ap_show_connection_info` - Show the connection info for the save file.

View File

@@ -0,0 +1,31 @@
from worlds.ahit.Regions import act_chapters
from worlds.ahit.test.TestBase import HatInTimeTestBase
class TestActs(HatInTimeTestBase):
def run_default_tests(self) -> bool:
return False
def testAllStateCanReachEverything(self):
pass
options = {
"ActRandomizer": 2,
"EnableDLC1": 1,
"EnableDLC2": 1,
"ShuffleActContracts": 0,
}
def test_act_shuffle(self):
for i in range(1000):
self.world_setup()
self.collect_all_but([""])
for name in act_chapters.keys():
region = self.multiworld.get_region(name, 1)
for entrance in region.entrances:
self.assertTrue(self.can_reach_entrance(entrance.name),
f"Can't reach {name} from {entrance}\n"
f"{entrance.parent_region.entrances[0]} -> {entrance.parent_region} "
f"-> {entrance} -> {name}"
f" (expected method of access)")

View File

@@ -0,0 +1,5 @@
from test.TestBase import WorldTestBase
class HatInTimeTestBase(WorldTestBase):
game = "A Hat in Time"

View File