mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-07 15:13:52 -08:00
Compare commits
50 Commits
plando-cou
...
0.6.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9b35de7ee | ||
|
|
125d053b61 | ||
|
|
585cbf95a6 | ||
|
|
909565e5d9 | ||
|
|
a79423534c | ||
|
|
7a6fb5e35b | ||
|
|
6af34b66fb | ||
|
|
2974f7d11f | ||
|
|
edc0c89753 | ||
|
|
b1ff55dd06 | ||
|
|
f4b5422f66 | ||
|
|
d4ebace99f | ||
|
|
95e09c8e2a | ||
|
|
4623d59206 | ||
|
|
e68b1ad428 | ||
|
|
072e2ece15 | ||
|
|
11130037fe | ||
|
|
ba66ef14cc | ||
|
|
8aacc23882 | ||
|
|
03e5fd3dae | ||
|
|
da52598c08 | ||
|
|
52389731eb | ||
|
|
21864f6f95 | ||
|
|
00f8625280 | ||
|
|
c34e29c712 | ||
|
|
e0ae3359f1 | ||
|
|
c2666bacd7 | ||
|
|
4eefd9c3ce | ||
|
|
211456242e | ||
|
|
6f244c4661 | ||
|
|
47bf6d724b | ||
|
|
5c710ad032 | ||
|
|
dda5a05cbb | ||
|
|
e0a63e0290 | ||
|
|
9246659589 | ||
|
|
377cdb84b4 | ||
|
|
0e759f25fd | ||
|
|
b408bb4f6e | ||
|
|
1356479415 | ||
|
|
ec5b4e704f | ||
|
|
aa9e617510 | ||
|
|
ecb739ce96 | ||
|
|
3b72140435 | ||
|
|
27a6770569 | ||
|
|
2ff611167a | ||
|
|
e83e178b63 | ||
|
|
068a757373 | ||
|
|
0ad4527719 | ||
|
|
8c6327d024 | ||
|
|
aecbb2ab02 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -98,7 +98,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cd build/exe*
|
||||
cp Players/Templates/Clique.yaml Players/
|
||||
cp Players/Templates/VVVVVV.yaml Players/
|
||||
timeout 30 ./ArchipelagoGenerate
|
||||
- name: Store 7z
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cd build/exe*
|
||||
cp Players/Templates/Clique.yaml Players/
|
||||
cp Players/Templates/VVVVVV.yaml Players/
|
||||
timeout 30 ./ArchipelagoGenerate
|
||||
- name: Store AppImage
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -56,7 +56,6 @@ success.txt
|
||||
output/
|
||||
Output Logs/
|
||||
/factorio/
|
||||
/Minecraft Forge Server/
|
||||
/WebHostLib/static/generated
|
||||
/freeze_requirements.txt
|
||||
/Archipelago.zip
|
||||
@@ -184,12 +183,6 @@ _speedups.c
|
||||
_speedups.cpp
|
||||
_speedups.html
|
||||
|
||||
# minecraft server stuff
|
||||
jdk*/
|
||||
minecraft*/
|
||||
minecraft_versions.json
|
||||
!worlds/minecraft/
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import List
|
||||
|
||||
|
||||
import Utils
|
||||
from settings import get_settings
|
||||
from NetUtils import ClientStatus
|
||||
from Utils import async_start
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
|
||||
@@ -80,8 +81,8 @@ class AdventureContext(CommonContext):
|
||||
self.local_item_locations = {}
|
||||
self.dragon_speed_info = {}
|
||||
|
||||
options = Utils.get_settings()
|
||||
self.display_msgs = options["adventure_options"]["display_msgs"]
|
||||
options = get_settings().adventure_options
|
||||
self.display_msgs = options.display_msgs
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
@@ -102,7 +103,7 @@ class AdventureContext(CommonContext):
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == 'Connected':
|
||||
self.locations_array = None
|
||||
if Utils.get_settings()["adventure_options"].get("death_link", False):
|
||||
if get_settings().adventure_options.as_dict().get("death_link", False):
|
||||
self.set_deathlink = True
|
||||
async_start(self.get_freeincarnates_used())
|
||||
elif cmd == "RoomInfo":
|
||||
@@ -415,8 +416,9 @@ async def atari_sync_task(ctx: AdventureContext):
|
||||
|
||||
|
||||
async def run_game(romfile):
|
||||
auto_start = Utils.get_settings()["adventure_options"].get("rom_start", True)
|
||||
rom_args = Utils.get_settings()["adventure_options"].get("rom_args")
|
||||
options = get_settings().adventure_options
|
||||
auto_start = options.rom_start
|
||||
rom_args = options.rom_args
|
||||
if auto_start is True:
|
||||
import webbrowser
|
||||
webbrowser.open(romfile)
|
||||
|
||||
@@ -1337,8 +1337,8 @@ class Region:
|
||||
Connects current region to regions in exit dictionary. Passed region names must exist first.
|
||||
|
||||
:param exits: exits from the region. format is {"connecting_region": "exit_name"}. if a non dict is provided,
|
||||
created entrances will be named "self.name -> connecting_region"
|
||||
:param rules: rules for the exits from this region. format is {"connecting_region", rule}
|
||||
created entrances will be named "self.name -> connecting_region"
|
||||
:param rules: rules for the exits from this region. format is {"connecting_region": rule}
|
||||
"""
|
||||
if not isinstance(exits, Dict):
|
||||
exits = dict.fromkeys(exits)
|
||||
|
||||
11
Fill.py
11
Fill.py
@@ -890,7 +890,7 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
|
||||
worlds = set()
|
||||
for listed_world in target_world:
|
||||
if listed_world not in world_name_lookup:
|
||||
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
|
||||
failed(f"Cannot place item to {listed_world}'s world as that world does not exist.",
|
||||
block.force)
|
||||
continue
|
||||
worlds.add(world_name_lookup[listed_world])
|
||||
@@ -923,9 +923,9 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
|
||||
if isinstance(locations, str):
|
||||
locations = [locations]
|
||||
|
||||
locations_from_groups: list[str] = []
|
||||
resolved_locations: list[Location] = []
|
||||
for target_player in worlds:
|
||||
locations_from_groups: list[str] = []
|
||||
world_locations = multiworld.get_unfilled_locations(target_player)
|
||||
for group in multiworld.worlds[target_player].location_name_groups:
|
||||
if group in locations:
|
||||
@@ -937,13 +937,16 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
|
||||
|
||||
count = block.count
|
||||
if not count:
|
||||
count = len(new_block.items)
|
||||
count = (min(len(new_block.items), len(new_block.resolved_locations))
|
||||
if new_block.resolved_locations else len(new_block.items))
|
||||
if isinstance(count, int):
|
||||
count = {"min": count, "max": count}
|
||||
if "min" not in count:
|
||||
count["min"] = 0
|
||||
if "max" not in count:
|
||||
count["max"] = len(new_block.items)
|
||||
count["max"] = (min(len(new_block.items), len(new_block.resolved_locations))
|
||||
if new_block.resolved_locations else len(new_block.items))
|
||||
|
||||
|
||||
new_block.count = count
|
||||
plando_blocks[player].append(new_block)
|
||||
|
||||
@@ -290,12 +290,9 @@ async def gba_sync_task(ctx: MMBN3Context):
|
||||
|
||||
|
||||
async def run_game(romfile):
|
||||
options = Utils.get_options().get("mmbn3_options", None)
|
||||
if options is None:
|
||||
auto_start = True
|
||||
else:
|
||||
auto_start = options.get("rom_start", True)
|
||||
if auto_start:
|
||||
from worlds.mmbn3 import MMBN3World
|
||||
auto_start = MMBN3World.settings.rom_start
|
||||
if auto_start is True:
|
||||
import webbrowser
|
||||
webbrowser.open(romfile)
|
||||
elif os.path.isfile(auto_start):
|
||||
|
||||
4
Main.py
4
Main.py
@@ -12,6 +12,7 @@ import worlds
|
||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
|
||||
from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, flood_items, \
|
||||
parse_planned_blocks, distribute_planned_blocks, resolve_early_locations_for_planned
|
||||
from NetUtils import convert_to_base_types
|
||||
from Options import StartInventoryPool
|
||||
from Utils import __version__, output_path, version_tuple
|
||||
from settings import get_settings
|
||||
@@ -334,6 +335,9 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
|
||||
}
|
||||
AutoWorld.call_all(multiworld, "modify_multidata", multidata)
|
||||
|
||||
for key in ("slot_data", "er_hint_data"):
|
||||
multidata[key] = convert_to_base_types(multidata[key])
|
||||
|
||||
multidata = zlib.compress(pickle.dumps(multidata), 9)
|
||||
|
||||
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
|
||||
|
||||
@@ -1,347 +0,0 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import atexit
|
||||
import shutil
|
||||
from subprocess import Popen
|
||||
from shutil import copyfile
|
||||
from time import strftime
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
import Utils
|
||||
from Utils import is_windows
|
||||
from settings import get_settings
|
||||
|
||||
atexit.register(input, "Press enter to exit.")
|
||||
|
||||
# 1 or more digits followed by m or g, then optional b
|
||||
max_heap_re = re.compile(r"^\d+[mMgG][bB]?$")
|
||||
|
||||
|
||||
def prompt_yes_no(prompt):
|
||||
yes_inputs = {'yes', 'ye', 'y'}
|
||||
no_inputs = {'no', 'n'}
|
||||
while True:
|
||||
choice = input(prompt + " [y/n] ").lower()
|
||||
if choice in yes_inputs:
|
||||
return True
|
||||
elif choice in no_inputs:
|
||||
return False
|
||||
else:
|
||||
print('Please respond with "y" or "n".')
|
||||
|
||||
|
||||
def find_ap_randomizer_jar(forge_dir):
|
||||
"""Create mods folder if needed; find AP randomizer jar; return None if not found."""
|
||||
mods_dir = os.path.join(forge_dir, 'mods')
|
||||
if os.path.isdir(mods_dir):
|
||||
for entry in os.scandir(mods_dir):
|
||||
if entry.name.startswith("aprandomizer") and entry.name.endswith(".jar"):
|
||||
logging.info(f"Found AP randomizer mod: {entry.name}")
|
||||
return entry.name
|
||||
return None
|
||||
else:
|
||||
os.mkdir(mods_dir)
|
||||
logging.info(f"Created mods folder in {forge_dir}")
|
||||
return None
|
||||
|
||||
|
||||
def replace_apmc_files(forge_dir, apmc_file):
|
||||
"""Create APData folder if needed; clean .apmc files from APData; copy given .apmc into directory."""
|
||||
if apmc_file is None:
|
||||
return
|
||||
apdata_dir = os.path.join(forge_dir, 'APData')
|
||||
copy_apmc = True
|
||||
if not os.path.isdir(apdata_dir):
|
||||
os.mkdir(apdata_dir)
|
||||
logging.info(f"Created APData folder in {forge_dir}")
|
||||
for entry in os.scandir(apdata_dir):
|
||||
if entry.name.endswith(".apmc") and entry.is_file():
|
||||
if not os.path.samefile(apmc_file, entry.path):
|
||||
os.remove(entry.path)
|
||||
logging.info(f"Removed {entry.name} in {apdata_dir}")
|
||||
else: # apmc already in apdata
|
||||
copy_apmc = False
|
||||
if copy_apmc:
|
||||
copyfile(apmc_file, os.path.join(apdata_dir, os.path.basename(apmc_file)))
|
||||
logging.info(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
|
||||
|
||||
|
||||
def read_apmc_file(apmc_file):
|
||||
from base64 import b64decode
|
||||
|
||||
with open(apmc_file, 'r') as f:
|
||||
return json.loads(b64decode(f.read()))
|
||||
|
||||
|
||||
def update_mod(forge_dir, url: str):
|
||||
"""Check mod version, download new mod from GitHub releases page if needed. """
|
||||
ap_randomizer = find_ap_randomizer_jar(forge_dir)
|
||||
os.path.basename(url)
|
||||
if ap_randomizer is not None:
|
||||
logging.info(f"Your current mod is {ap_randomizer}.")
|
||||
else:
|
||||
logging.info(f"You do not have the AP randomizer mod installed.")
|
||||
|
||||
if ap_randomizer != os.path.basename(url):
|
||||
logging.info(f"A new release of the Minecraft AP randomizer mod was found: "
|
||||
f"{os.path.basename(url)}")
|
||||
if prompt_yes_no("Would you like to update?"):
|
||||
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
|
||||
new_ap_mod = os.path.join(forge_dir, 'mods', os.path.basename(url))
|
||||
logging.info("Downloading AP randomizer mod. This may take a moment...")
|
||||
apmod_resp = requests.get(url)
|
||||
if apmod_resp.status_code == 200:
|
||||
with open(new_ap_mod, 'wb') as f:
|
||||
f.write(apmod_resp.content)
|
||||
logging.info(f"Wrote new mod file to {new_ap_mod}")
|
||||
if old_ap_mod is not None:
|
||||
os.remove(old_ap_mod)
|
||||
logging.info(f"Removed old mod file from {old_ap_mod}")
|
||||
else:
|
||||
logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
|
||||
logging.error(f"Please report this issue on the Archipelago Discord server.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def check_eula(forge_dir):
|
||||
"""Check if the EULA is agreed to, and prompt the user to read and agree if necessary."""
|
||||
eula_path = os.path.join(forge_dir, "eula.txt")
|
||||
if not os.path.isfile(eula_path):
|
||||
# Create eula.txt
|
||||
with open(eula_path, 'w') as f:
|
||||
f.write("#By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula).\n")
|
||||
f.write(f"#{strftime('%a %b %d %X %Z %Y')}\n")
|
||||
f.write("eula=false\n")
|
||||
with open(eula_path, 'r+') as f:
|
||||
text = f.read()
|
||||
if 'false' in text:
|
||||
# Prompt user to agree to the EULA
|
||||
logging.info("You need to agree to the Minecraft EULA in order to run the server.")
|
||||
logging.info("The EULA can be found at https://account.mojang.com/documents/minecraft_eula")
|
||||
if prompt_yes_no("Do you agree to the EULA?"):
|
||||
f.seek(0)
|
||||
f.write(text.replace('false', 'true'))
|
||||
f.truncate()
|
||||
logging.info(f"Set {eula_path} to true")
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def find_jdk_dir(version: str) -> str:
|
||||
"""get the specified versions jdk directory"""
|
||||
for entry in os.listdir():
|
||||
if os.path.isdir(entry) and entry.startswith(f"jdk{version}"):
|
||||
return os.path.abspath(entry)
|
||||
|
||||
|
||||
def find_jdk(version: str) -> str:
|
||||
"""get the java exe location"""
|
||||
|
||||
if is_windows:
|
||||
jdk = find_jdk_dir(version)
|
||||
jdk_exe = os.path.join(jdk, "bin", "java.exe")
|
||||
if os.path.isfile(jdk_exe):
|
||||
return jdk_exe
|
||||
else:
|
||||
jdk_exe = shutil.which(options.java)
|
||||
if not jdk_exe:
|
||||
jdk_exe = shutil.which("java") # try to fall back to system java
|
||||
if not jdk_exe:
|
||||
raise Exception("Could not find Java. Is Java installed on the system?")
|
||||
return jdk_exe
|
||||
|
||||
|
||||
def download_java(java: str):
|
||||
"""Download Corretto (Amazon JDK)"""
|
||||
|
||||
jdk = find_jdk_dir(java)
|
||||
if jdk is not None:
|
||||
print(f"Removing old JDK...")
|
||||
from shutil import rmtree
|
||||
rmtree(jdk)
|
||||
|
||||
print(f"Downloading Java...")
|
||||
jdk_url = f"https://corretto.aws/downloads/latest/amazon-corretto-{java}-x64-windows-jdk.zip"
|
||||
resp = requests.get(jdk_url)
|
||||
if resp.status_code == 200: # OK
|
||||
print(f"Extracting...")
|
||||
import zipfile
|
||||
from io import BytesIO
|
||||
with zipfile.ZipFile(BytesIO(resp.content)) as zf:
|
||||
zf.extractall()
|
||||
else:
|
||||
print(f"Error downloading Java (status code {resp.status_code}).")
|
||||
print(f"If this was not expected, please report this issue on the Archipelago Discord server.")
|
||||
if not prompt_yes_no("Continue anyways?"):
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def install_forge(directory: str, forge_version: str, java_version: str):
|
||||
"""download and install forge"""
|
||||
|
||||
java_exe = find_jdk(java_version)
|
||||
if java_exe is not None:
|
||||
print(f"Downloading Forge {forge_version}...")
|
||||
forge_url = f"https://maven.minecraftforge.net/net/minecraftforge/forge/{forge_version}/forge-{forge_version}-installer.jar"
|
||||
resp = requests.get(forge_url)
|
||||
if resp.status_code == 200: # OK
|
||||
forge_install_jar = os.path.join(directory, "forge_install.jar")
|
||||
if not os.path.exists(directory):
|
||||
os.mkdir(directory)
|
||||
with open(forge_install_jar, 'wb') as f:
|
||||
f.write(resp.content)
|
||||
print(f"Installing Forge...")
|
||||
install_process = Popen([java_exe, "-jar", forge_install_jar, "--installServer", directory])
|
||||
install_process.wait()
|
||||
os.remove(forge_install_jar)
|
||||
|
||||
|
||||
def run_forge_server(forge_dir: str, java_version: str, heap_arg: str) -> Popen:
|
||||
"""Run the Forge server."""
|
||||
|
||||
java_exe = find_jdk(java_version)
|
||||
if not os.path.isfile(java_exe):
|
||||
java_exe = "java" # try to fall back on java in the PATH
|
||||
|
||||
heap_arg = max_heap_re.match(heap_arg).group()
|
||||
if heap_arg[-1] in ['b', 'B']:
|
||||
heap_arg = heap_arg[:-1]
|
||||
heap_arg = "-Xmx" + heap_arg
|
||||
|
||||
os_args = "win_args.txt" if is_windows else "unix_args.txt"
|
||||
args_file = os.path.join(forge_dir, "libraries", "net", "minecraftforge", "forge", forge_version, os_args)
|
||||
forge_args = []
|
||||
with open(args_file) as argfile:
|
||||
for line in argfile:
|
||||
forge_args.extend(line.strip().split(" "))
|
||||
|
||||
args = [java_exe, heap_arg, *forge_args, "-nogui"]
|
||||
logging.info(f"Running Forge server: {args}")
|
||||
os.chdir(forge_dir)
|
||||
return Popen(args)
|
||||
|
||||
|
||||
def get_minecraft_versions(version, release_channel="release"):
|
||||
version_file_endpoint = "https://raw.githubusercontent.com/KonoTyran/Minecraft_AP_Randomizer/master/versions/minecraft_versions.json"
|
||||
resp = requests.get(version_file_endpoint)
|
||||
local = False
|
||||
if resp.status_code == 200: # OK
|
||||
try:
|
||||
data = resp.json()
|
||||
except requests.exceptions.JSONDecodeError:
|
||||
logging.warning(f"Unable to fetch version update file, using local version. (status code {resp.status_code}).")
|
||||
local = True
|
||||
else:
|
||||
logging.warning(f"Unable to fetch version update file, using local version. (status code {resp.status_code}).")
|
||||
local = True
|
||||
|
||||
if local:
|
||||
with open(Utils.user_path("minecraft_versions.json"), 'r') as f:
|
||||
data = json.load(f)
|
||||
else:
|
||||
with open(Utils.user_path("minecraft_versions.json"), 'w') as f:
|
||||
json.dump(data, f)
|
||||
|
||||
try:
|
||||
if version:
|
||||
return next(filter(lambda entry: entry["version"] == version, data[release_channel]))
|
||||
else:
|
||||
return resp.json()[release_channel][0]
|
||||
except (StopIteration, KeyError):
|
||||
logging.error(f"No compatible mod version found for client version {version} on \"{release_channel}\" channel.")
|
||||
if release_channel != "release":
|
||||
logging.error("Consider switching \"release_channel\" to \"release\" in your Host.yaml file")
|
||||
else:
|
||||
logging.error("No suitable mod found on the \"release\" channel. Please Contact us on discord to report this error.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def is_correct_forge(forge_dir) -> bool:
|
||||
if os.path.isdir(os.path.join(forge_dir, "libraries", "net", "minecraftforge", "forge", forge_version)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
Utils.init_logging("MinecraftClient")
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("apmc_file", default=None, nargs='?', help="Path to an Archipelago Minecraft data file (.apmc)")
|
||||
parser.add_argument('--install', '-i', dest='install', default=False, action='store_true',
|
||||
help="Download and install Java and the Forge server. Does not launch the client afterwards.")
|
||||
parser.add_argument('--release_channel', '-r', dest="channel", type=str, action='store',
|
||||
help="Specify release channel to use.")
|
||||
parser.add_argument('--java', '-j', metavar='17', dest='java', type=str, default=False, action='store',
|
||||
help="specify java version.")
|
||||
parser.add_argument('--forge', '-f', metavar='1.18.2-40.1.0', dest='forge', type=str, default=False, action='store',
|
||||
help="specify forge version. (Minecraft Version-Forge Version)")
|
||||
parser.add_argument('--version', '-v', metavar='9', dest='data_version', type=int, action='store',
|
||||
help="specify Mod data version to download.")
|
||||
|
||||
args = parser.parse_args()
|
||||
apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None
|
||||
|
||||
# Change to executable's working directory
|
||||
os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
|
||||
|
||||
options = get_settings().minecraft_options
|
||||
channel = args.channel or options.release_channel
|
||||
apmc_data = None
|
||||
data_version = args.data_version or None
|
||||
|
||||
if apmc_file is None and not args.install:
|
||||
apmc_file = Utils.open_filename('Select APMC file', (('APMC File', ('.apmc',)),))
|
||||
|
||||
if apmc_file is not None and data_version is None:
|
||||
apmc_data = read_apmc_file(apmc_file)
|
||||
data_version = apmc_data.get('client_version', '')
|
||||
|
||||
versions = get_minecraft_versions(data_version, channel)
|
||||
|
||||
forge_dir = options.forge_directory
|
||||
max_heap = options.max_heap_size
|
||||
forge_version = args.forge or versions["forge"]
|
||||
java_version = args.java or versions["java"]
|
||||
mod_url = versions["url"]
|
||||
java_dir = find_jdk_dir(java_version)
|
||||
|
||||
if args.install:
|
||||
if is_windows:
|
||||
print("Installing Java")
|
||||
download_java(java_version)
|
||||
if not is_correct_forge(forge_dir):
|
||||
print("Installing Minecraft Forge")
|
||||
install_forge(forge_dir, forge_version, java_version)
|
||||
else:
|
||||
print("Correct Forge version already found, skipping install.")
|
||||
sys.exit(0)
|
||||
|
||||
if apmc_data is None:
|
||||
raise FileNotFoundError(f"APMC file does not exist or is inaccessible at the given location ({apmc_file})")
|
||||
|
||||
if is_windows:
|
||||
if java_dir is None or not os.path.isdir(java_dir):
|
||||
if prompt_yes_no("Did not find java directory. Download and install java now?"):
|
||||
download_java(java_version)
|
||||
java_dir = find_jdk_dir(java_version)
|
||||
if java_dir is None or not os.path.isdir(java_dir):
|
||||
raise NotADirectoryError(f"Path {java_dir} does not exist or could not be accessed.")
|
||||
|
||||
if not is_correct_forge(forge_dir):
|
||||
if prompt_yes_no(f"Did not find forge version {forge_version} download and install it now?"):
|
||||
install_forge(forge_dir, forge_version, java_version)
|
||||
if not os.path.isdir(forge_dir):
|
||||
raise NotADirectoryError(f"Path {forge_dir} does not exist or could not be accessed.")
|
||||
|
||||
if not max_heap_re.match(max_heap):
|
||||
raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.")
|
||||
|
||||
update_mod(forge_dir, mod_url)
|
||||
replace_apmc_files(forge_dir, apmc_file)
|
||||
check_eula(forge_dir)
|
||||
server_process = run_forge_server(forge_dir, java_version, max_heap)
|
||||
server_process.wait()
|
||||
21
NetUtils.py
21
NetUtils.py
@@ -106,6 +106,27 @@ def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any:
|
||||
return obj
|
||||
|
||||
|
||||
_base_types = str | int | bool | float | None | tuple["_base_types", ...] | dict["_base_types", "base_types"]
|
||||
|
||||
|
||||
def convert_to_base_types(obj: typing.Any) -> _base_types:
|
||||
if isinstance(obj, (tuple, list, set, frozenset)):
|
||||
return tuple(convert_to_base_types(o) for o in obj)
|
||||
elif isinstance(obj, dict):
|
||||
return {convert_to_base_types(key): convert_to_base_types(value) for key, value in obj.items()}
|
||||
elif obj is None or type(obj) in (str, int, float, bool):
|
||||
return obj
|
||||
# unwrap simple types to their base, such as StrEnum
|
||||
elif isinstance(obj, str):
|
||||
return str(obj)
|
||||
elif isinstance(obj, int):
|
||||
return int(obj)
|
||||
elif isinstance(obj, float):
|
||||
return float(obj)
|
||||
else:
|
||||
raise Exception(f"Cannot handle {type(obj)}")
|
||||
|
||||
|
||||
_encode = JSONEncoder(
|
||||
ensure_ascii=False,
|
||||
check_circular=False,
|
||||
|
||||
@@ -7,7 +7,6 @@ Currently, the following games are supported:
|
||||
|
||||
* The Legend of Zelda: A Link to the Past
|
||||
* Factorio
|
||||
* Minecraft
|
||||
* Subnautica
|
||||
* Risk of Rain 2
|
||||
* The Legend of Zelda: Ocarina of Time
|
||||
@@ -15,7 +14,6 @@ Currently, the following games are supported:
|
||||
* Super Metroid
|
||||
* Secret of Evermore
|
||||
* Final Fantasy
|
||||
* Rogue Legacy
|
||||
* VVVVVV
|
||||
* Raft
|
||||
* Super Mario 64
|
||||
@@ -42,7 +40,6 @@ Currently, the following games are supported:
|
||||
* The Messenger
|
||||
* Kingdom Hearts 2
|
||||
* The Legend of Zelda: Link's Awakening DX
|
||||
* Clique
|
||||
* Adventure
|
||||
* DLC Quest
|
||||
* Noita
|
||||
|
||||
7
Utils.py
7
Utils.py
@@ -166,6 +166,10 @@ def home_path(*path: str) -> str:
|
||||
os.symlink(home_path.cached_path, legacy_home_path)
|
||||
else:
|
||||
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
|
||||
elif sys.platform == 'darwin':
|
||||
import platformdirs
|
||||
home_path.cached_path = platformdirs.user_data_dir("Archipelago", False)
|
||||
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
|
||||
else:
|
||||
# not implemented
|
||||
home_path.cached_path = local_path() # this will generate the same exceptions we got previously
|
||||
@@ -177,7 +181,7 @@ def user_path(*path: str) -> str:
|
||||
"""Returns either local_path or home_path based on write permissions."""
|
||||
if hasattr(user_path, "cached_path"):
|
||||
pass
|
||||
elif os.access(local_path(), os.W_OK):
|
||||
elif os.access(local_path(), os.W_OK) and not (is_macos and is_frozen()):
|
||||
user_path.cached_path = local_path()
|
||||
else:
|
||||
user_path.cached_path = home_path()
|
||||
@@ -438,6 +442,7 @@ class RestrictedUnpickler(pickle.Unpickler):
|
||||
if module == "builtins" and name in safe_builtins:
|
||||
return getattr(builtins, name)
|
||||
# used by OptionCounter
|
||||
# necessary because the actual Options class instances are pickled when transfered to WebHost generation pool
|
||||
if module == "collections" and name == "Counter":
|
||||
return collections.Counter
|
||||
# used by MultiServer -> savegame/multidata
|
||||
|
||||
@@ -61,12 +61,7 @@ def download_slot_file(room_id, player_id: int):
|
||||
else:
|
||||
import io
|
||||
|
||||
if slot_data.game == "Minecraft":
|
||||
from worlds.minecraft import mc_update_output
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
|
||||
data = mc_update_output(slot_data.data, server=app.config['HOST_ADDRESS'], port=room.last_port)
|
||||
return send_file(io.BytesIO(data), as_attachment=True, download_name=fname)
|
||||
elif slot_data.game == "Factorio":
|
||||
if slot_data.game == "Factorio":
|
||||
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
|
||||
for name in zf.namelist():
|
||||
if name.endswith("info.json"):
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
window.addEventListener('load', () => {
|
||||
// Reload tracker every 15 seconds
|
||||
const url = window.location;
|
||||
setInterval(() => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
|
||||
// Create a fake DOM using the returned HTML
|
||||
const domParser = new DOMParser();
|
||||
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
|
||||
|
||||
// Update item tracker
|
||||
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
|
||||
// Update only counters in the location-table
|
||||
let counters = document.getElementsByClassName('counter');
|
||||
const fakeCounters = fakeDOM.getElementsByClassName('counter');
|
||||
for (let i = 0; i < counters.length; i++) {
|
||||
counters[i].innerHTML = fakeCounters[i].innerHTML;
|
||||
}
|
||||
};
|
||||
ajax.open('GET', url);
|
||||
ajax.send();
|
||||
}, 15000)
|
||||
|
||||
// Collapsible advancement sections
|
||||
const categories = document.getElementsByClassName("location-category");
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
let hide_id = categories[i].id.split('-')[0];
|
||||
if (hide_id == 'Total') {
|
||||
continue;
|
||||
}
|
||||
categories[i].addEventListener('click', function() {
|
||||
// Toggle the advancement list
|
||||
document.getElementById(hide_id).classList.toggle("hide");
|
||||
// Change text of the header
|
||||
const tab_header = document.getElementById(hide_id+'-header').children[0];
|
||||
const orig_text = tab_header.innerHTML;
|
||||
let new_text;
|
||||
if (orig_text.includes("▼")) {
|
||||
new_text = orig_text.replace("▼", "▲");
|
||||
}
|
||||
else {
|
||||
new_text = orig_text.replace("▲", "▼");
|
||||
}
|
||||
tab_header.innerHTML = new_text;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,102 +0,0 @@
|
||||
#player-tracker-wrapper{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#inventory-table{
|
||||
border-top: 2px solid #000000;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding: 3px 3px 10px;
|
||||
width: 384px;
|
||||
background-color: #42b149;
|
||||
}
|
||||
|
||||
#inventory-table td{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#inventory-table img{
|
||||
height: 100%;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
filter: grayscale(100%) contrast(75%) brightness(30%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired{
|
||||
filter: none;
|
||||
}
|
||||
|
||||
#inventory-table div.counted-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#inventory-table div.item-count {
|
||||
position: absolute;
|
||||
color: white;
|
||||
font-family: "Minecraftia", monospace;
|
||||
font-weight: bold;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#location-table{
|
||||
width: 384px;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-bottom: 2px solid #000000;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background-color: #42b149;
|
||||
padding: 0 3px 3px;
|
||||
font-family: "Minecraftia", monospace;
|
||||
font-size: 14px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#location-table th{
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#location-table td{
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#location-table td.counter {
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#location-table td.toggle-arrow {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#location-table tr#Total-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table img{
|
||||
height: 100%;
|
||||
max-width: 30px;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
#location-table tbody.locations {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#location-table td.location-name {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
@@ -26,10 +26,7 @@
|
||||
<td>{{ patch.game }}</td>
|
||||
<td>
|
||||
{% if patch.data %}
|
||||
{% if patch.game == "Minecraft" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APMC File...</a>
|
||||
{% elif patch.game == "VVVVVV" and room.seed.slots|length == 1 %}
|
||||
{% if patch.game == "VVVVVV" and room.seed.slots|length == 1 %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APV6 File...</a>
|
||||
{% elif patch.game == "Super Mario 64" and room.seed.slots|length == 1 %}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/minecraftTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/minecraftTracker.js') }}"></script>
|
||||
<link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/minecraftia" type="text/css"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ tools_url }}" class="{{ 'acquired' }}" title="Progressive Tools" /></td>
|
||||
<td><img src="{{ weapons_url }}" class="{{ 'acquired' }}" title="Progressive Weapons" /></td>
|
||||
<td><img src="{{ armor_url }}" class="{{ 'acquired' }}" title="Progressive Armor" /></td>
|
||||
<td><img src="{{ resource_crafting_url }}" class="{{ 'acquired' if 'Progressive Resource Crafting' in acquired_items }}"
|
||||
title="Progressive Resource Crafting" /></td>
|
||||
<td><img src="{{ icons['Brewing Stand'] }}" class="{{ 'acquired' if 'Brewing' in acquired_items }}" title="Brewing" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Ender Pearl'] }}" class="{{ 'acquired' if '3 Ender Pearls' in acquired_items }}" title="Ender Pearls" />
|
||||
<div class="item-count">{{ pearls_count }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Bucket'] }}" class="{{ 'acquired' if 'Bucket' in acquired_items }}" title="Bucket" /></td>
|
||||
<td><img src="{{ icons['Bow'] }}" class="{{ 'acquired' if 'Archery' in acquired_items }}" title="Archery" /></td>
|
||||
<td><img src="{{ icons['Shield'] }}" class="{{ 'acquired' if 'Shield' in acquired_items }}" title="Shield" /></td>
|
||||
<td><img src="{{ icons['Red Bed'] }}" class="{{ 'acquired' if 'Bed' in acquired_items }}" title="Bed" /></td>
|
||||
<td><img src="{{ icons['Water Bottle'] }}" class="{{ 'acquired' if 'Bottles' in acquired_items }}" title="Bottles" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Netherite Scrap'] }}" class="{{ 'acquired' if '8 Netherite Scrap' in acquired_items }}" title="Netherite Scrap" />
|
||||
<div class="item-count">{{ scrap_count }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Flint and Steel'] }}" class="{{ 'acquired' if 'Flint and Steel' in acquired_items }}" title="Flint and Steel" /></td>
|
||||
<td><img src="{{ icons['Enchanting Table'] }}" class="{{ 'acquired' if 'Enchanting' in acquired_items }}" title="Enchanting" /></td>
|
||||
<td><img src="{{ icons['Fishing Rod'] }}" class="{{ 'acquired' if 'Fishing Rod' in acquired_items }}" title="Fishing Rod" /></td>
|
||||
<td><img src="{{ icons['Campfire'] }}" class="{{ 'acquired' if 'Campfire' in acquired_items }}" title="Campfire" /></td>
|
||||
<td><img src="{{ icons['Spyglass'] }}" class="{{ 'acquired' if 'Spyglass' in acquired_items }}" title="Spyglass" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Dragon Egg Shard'] }}" class="{{ 'acquired' if 'Dragon Egg Shard' in acquired_items }}" title="Dragon Egg Shard" />
|
||||
<div class="item-count">{{ shard_count }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Lead'] }}" class="{{ 'acquired' if 'Lead' in acquired_items }}" title="Lead" /></td>
|
||||
<td><img src="{{ icons['Saddle'] }}" class="{{ 'acquired' if 'Saddle' in acquired_items }}" title="Saddle" /></td>
|
||||
<td><img src="{{ icons['Channeling Book'] }}" class="{{ 'acquired' if 'Channeling Book' in acquired_items }}" title="Channeling Book" /></td>
|
||||
<td><img src="{{ icons['Silk Touch Book'] }}" class="{{ 'acquired' if 'Silk Touch Book' in acquired_items }}" title="Silk Touch Book" /></td>
|
||||
<td><img src="{{ icons['Piercing IV Book'] }}" class="{{ 'acquired' if 'Piercing IV Book' in acquired_items }}" title="Piercing IV Book" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="location-table">
|
||||
{% for area in checks_done %}
|
||||
<tr class="location-category" id="{{area}}-header">
|
||||
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
|
||||
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||
</tr>
|
||||
<tbody class="locations hide" id="{{area}}">
|
||||
{% for location in location_info[area] %}
|
||||
<tr>
|
||||
<td class="location-name">{{ location }}</td>
|
||||
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -706,127 +706,6 @@ if "A Link to the Past" in network_data_package["games"]:
|
||||
_multiworld_trackers["A Link to the Past"] = render_ALinkToThePast_multiworld_tracker
|
||||
_player_trackers["A Link to the Past"] = render_ALinkToThePast_tracker
|
||||
|
||||
if "Minecraft" in network_data_package["games"]:
|
||||
def render_Minecraft_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
|
||||
icons = {
|
||||
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
|
||||
"Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png",
|
||||
"Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png",
|
||||
"Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png",
|
||||
"Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png",
|
||||
"Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png",
|
||||
"Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png",
|
||||
"Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png",
|
||||
"Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png",
|
||||
"Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png",
|
||||
"Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png",
|
||||
"Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png",
|
||||
"Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png",
|
||||
"Brewing Stand": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b3/Brewing_Stand_%28empty%29_JE10.png",
|
||||
"Ender Pearl": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png",
|
||||
"Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png",
|
||||
"Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png",
|
||||
"Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png",
|
||||
"Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png",
|
||||
"Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png",
|
||||
"Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png",
|
||||
"Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif",
|
||||
"Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png",
|
||||
"Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
|
||||
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
|
||||
"Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
|
||||
"Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png",
|
||||
"Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png",
|
||||
"Saddle": "https://i.imgur.com/2QtDyR0.png",
|
||||
"Channeling Book": "https://i.imgur.com/J3WsYZw.png",
|
||||
"Silk Touch Book": "https://i.imgur.com/iqERxHQ.png",
|
||||
"Piercing IV Book": "https://i.imgur.com/OzJptGz.png",
|
||||
}
|
||||
|
||||
minecraft_location_ids = {
|
||||
"Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070,
|
||||
42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077],
|
||||
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
|
||||
42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014],
|
||||
"The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046],
|
||||
"Adventure": [42047, 42050, 42096, 42097, 42098, 42059, 42055, 42072, 42003, 42109, 42035, 42016, 42020,
|
||||
42048, 42054, 42068, 42043, 42106, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42105,
|
||||
42099, 42103, 42110, 42100],
|
||||
"Husbandry": [42065, 42067, 42078, 42022, 42113, 42107, 42007, 42079, 42013, 42028, 42036, 42108, 42111,
|
||||
42112,
|
||||
42057, 42063, 42053, 42102, 42101, 42092, 42093, 42094, 42095],
|
||||
"Archipelago": [42080, 42081, 42082, 42083, 42084, 42085, 42086, 42087, 42088, 42089, 42090, 42091],
|
||||
}
|
||||
|
||||
display_data = {}
|
||||
|
||||
# Determine display for progressive items
|
||||
progressive_items = {
|
||||
"Progressive Tools": 45013,
|
||||
"Progressive Weapons": 45012,
|
||||
"Progressive Armor": 45014,
|
||||
"Progressive Resource Crafting": 45001
|
||||
}
|
||||
progressive_names = {
|
||||
"Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"],
|
||||
"Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"],
|
||||
"Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"],
|
||||
"Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"]
|
||||
}
|
||||
|
||||
inventory = tracker_data.get_player_inventory_counts(team, player)
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower().replace(" ", "_")
|
||||
display_data[base_name + "_url"] = icons[display_name]
|
||||
|
||||
# Multi-items
|
||||
multi_items = {
|
||||
"3 Ender Pearls": 45029,
|
||||
"8 Netherite Scrap": 45015,
|
||||
"Dragon Egg Shard": 45043
|
||||
}
|
||||
for item_name, item_id in multi_items.items():
|
||||
base_name = item_name.split()[-1].lower()
|
||||
count = inventory[item_id]
|
||||
if count >= 0:
|
||||
display_data[base_name + "_count"] = count
|
||||
|
||||
# Victory condition
|
||||
game_state = tracker_data.get_player_client_status(team, player)
|
||||
display_data["game_finished"] = game_state == 30
|
||||
|
||||
# Turn location IDs into advancement tab counts
|
||||
checked_locations = tracker_data.get_player_checked_locations(team, player)
|
||||
lookup_name = lambda id: tracker_data.location_id_to_name["Minecraft"][id]
|
||||
location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_done["Total"] = len(checked_locations)
|
||||
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_in_area["Total"] = sum(checks_in_area.values())
|
||||
|
||||
lookup_any_item_id_to_name = tracker_data.item_id_to_name["Minecraft"]
|
||||
return render_template(
|
||||
"tracker__Minecraft.html",
|
||||
inventory=inventory,
|
||||
icons=icons,
|
||||
acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0},
|
||||
player=player,
|
||||
team=team,
|
||||
room=tracker_data.room,
|
||||
player_name=tracker_data.get_player_name(team, player),
|
||||
saving_second=tracker_data.get_room_saving_second(),
|
||||
checks_done=checks_done,
|
||||
checks_in_area=checks_in_area,
|
||||
location_info=location_info,
|
||||
**display_data,
|
||||
)
|
||||
|
||||
_player_trackers["Minecraft"] = render_Minecraft_tracker
|
||||
|
||||
if "Ocarina of Time" in network_data_package["games"]:
|
||||
def render_OcarinaOfTime_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
|
||||
icons = {
|
||||
|
||||
@@ -135,11 +135,6 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||
multidata = None
|
||||
|
||||
# Minecraft
|
||||
elif file.filename.endswith(".apmc"):
|
||||
data = zfile.open(file, "r").read()
|
||||
metadata = json.loads(base64.b64decode(data).decode("utf-8"))
|
||||
files[metadata["player_id"]] = data
|
||||
|
||||
# Factorio
|
||||
elif file.filename.endswith(".zip"):
|
||||
|
||||
@@ -24,9 +24,20 @@
|
||||
<BaseButton>:
|
||||
ripple_color: app.theme_cls.primaryColor
|
||||
ripple_duration_in_fast: 0.2
|
||||
<MDTabsItemBase>:
|
||||
ripple_color: app.theme_cls.primaryColor
|
||||
ripple_duration_in_fast: 0.2
|
||||
<MDNavigationItemBase>:
|
||||
on_release: app.screens.switch_screens(self)
|
||||
|
||||
MDNavigationItemLabel:
|
||||
text: root.text
|
||||
theme_text_color: "Custom"
|
||||
text_color_active: self.theme_cls.primaryColor
|
||||
text_color_normal: 1, 1, 1, 1
|
||||
# indicator is on icon only for some reason
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: self.theme_cls.secondaryContainerColor if root.active else self.theme_cls.transparentColor
|
||||
Rectangle:
|
||||
size: root.size
|
||||
<TooltipLabel>:
|
||||
adaptive_height: True
|
||||
theme_font_size: "Custom"
|
||||
|
||||
@@ -477,7 +477,7 @@ function main()
|
||||
elseif (curstate == STATE_UNINITIALIZED) then
|
||||
-- If we're uninitialized, attempt to make the connection.
|
||||
if (frame % 120 == 0) then
|
||||
server:settimeout(2)
|
||||
server:settimeout(120)
|
||||
local client, timeout = server:accept()
|
||||
if timeout == nil then
|
||||
print('Initial Connection Made')
|
||||
|
||||
BIN
data/mcicon.ico
BIN
data/mcicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -48,9 +48,6 @@
|
||||
# Civilization VI
|
||||
/worlds/civ6/ @hesto2
|
||||
|
||||
# Clique
|
||||
/worlds/clique/ @ThePhar
|
||||
|
||||
# Dark Souls III
|
||||
/worlds/dark_souls_3/ @Marechal-L @nex3
|
||||
|
||||
@@ -121,9 +118,6 @@
|
||||
# The Messenger
|
||||
/worlds/messenger/ @alwaysintreble
|
||||
|
||||
# Minecraft
|
||||
/worlds/minecraft/ @KonoTyran @espeon65536
|
||||
|
||||
# Mega Man 2
|
||||
/worlds/mm2/ @Silvris
|
||||
|
||||
@@ -151,9 +145,6 @@
|
||||
# Raft
|
||||
/worlds/raft/ @SunnyBat
|
||||
|
||||
# Rogue Legacy
|
||||
/worlds/rogue_legacy/ @ThePhar
|
||||
|
||||
# Risk of Rain 2
|
||||
/worlds/ror2/ @kindasneaki
|
||||
|
||||
|
||||
@@ -117,12 +117,6 @@ flowchart LR
|
||||
%% Java Based Games
|
||||
subgraph Java
|
||||
JM[Mod with Archipelago.MultiClient.Java]
|
||||
subgraph Minecraft
|
||||
MCS[Minecraft Forge Server]
|
||||
JMC[Any Java Minecraft Clients]
|
||||
MCS <-- TCP --> JMC
|
||||
end
|
||||
JM <-- Forge Mod Loader --> MCS
|
||||
end
|
||||
AS <-- WebSockets --> JM
|
||||
|
||||
@@ -131,10 +125,8 @@ flowchart LR
|
||||
NM[Mod with Archipelago.MultiClient.Net]
|
||||
subgraph FNA/XNA
|
||||
TS[Timespinner]
|
||||
RL[Rogue Legacy]
|
||||
end
|
||||
NM <-- TsRandomizer --> TS
|
||||
NM <-- RogueLegacyRandomizer --> RL
|
||||
subgraph Unity
|
||||
ROR[Risk of Rain 2]
|
||||
SN[Subnautica]
|
||||
@@ -183,4 +175,4 @@ flowchart LR
|
||||
FMOD <--> FMAPI
|
||||
end
|
||||
CC <-- Integrated --> FC
|
||||
```
|
||||
```
|
||||
|
||||
@@ -258,31 +258,6 @@ another flag like "progression", it means "an especially useful progression item
|
||||
* `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that
|
||||
will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres
|
||||
|
||||
### Events
|
||||
|
||||
An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to
|
||||
track certain logic interactions, with the Event Item being required for access in other locations or regions, but not
|
||||
being "real". Since the item and location have no ID, they get dropped at the end of generation and so the server is
|
||||
never made aware of them and these locations can never be checked, nor can the items be received during play.
|
||||
They may also be used for making the spoiler log look nicer, i.e. by having a `"Victory"` Event Item, that
|
||||
is required to finish the game. This makes it very clear when the player finishes, rather than only seeing their last
|
||||
relevant Item. Events function just like any other Location, and can still have their own access rules, etc.
|
||||
By convention, the Event "pair" of Location and Item typically have the same name, though this is not a requirement.
|
||||
They must not exist in the `name_to_id` lookups, as they have no ID.
|
||||
|
||||
The most common way to create an Event pair is to create and place the Item on the Location as soon as it's created:
|
||||
|
||||
```python
|
||||
from worlds.AutoWorld import World
|
||||
from BaseClasses import ItemClassification
|
||||
from .subclasses import MyGameLocation, MyGameItem
|
||||
|
||||
|
||||
class MyGameWorld(World):
|
||||
victory_loc = MyGameLocation(self.player, "Victory", None)
|
||||
victory_loc.place_locked_item(MyGameItem("Victory", ItemClassification.progression, None, self.player))
|
||||
```
|
||||
|
||||
### Regions
|
||||
|
||||
Regions are logical containers that typically hold locations that share some common access rules. If location logic is
|
||||
@@ -291,7 +266,7 @@ like entrance randomization in logic.
|
||||
|
||||
Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
|
||||
|
||||
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)),
|
||||
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L310-L311)),
|
||||
from which the logic unfolds. AP assumes that a player will always be able to return to this starting region by resetting the game ("Save and quit").
|
||||
|
||||
### Entrances
|
||||
@@ -339,6 +314,63 @@ avoiding the need for indirect conditions at the expense of performance.
|
||||
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
|
||||
reject the placement of an item there.
|
||||
|
||||
### Events (or "generation-only items/locations")
|
||||
|
||||
An event item or location is one that only exists during multiworld generation; the server is never made aware of them.
|
||||
Event locations can never be checked by the player, and event items cannot be received during play.
|
||||
|
||||
Events are used to represent in-game actions (that aren't regular Archipelago locations) when either:
|
||||
|
||||
* We want to show in the spoiler log when the player is expected to perform the in-game action.
|
||||
* It's the cleanest way to represent how that in-game action impacts logic.
|
||||
|
||||
Typical examples include completing the goal, defeating a boss, or flipping a switch that affects multiple areas.
|
||||
|
||||
To be precise: the term "event" on its own refers to the special combination of an "event item" placed on an "event
|
||||
location". Event items and locations are created the same way as normal items and locations, except that they have an
|
||||
`id` of `None`, and an event item must be placed on an event location
|
||||
(and vice versa). Finally, although events are often described as "fake" items and locations, it's important to
|
||||
understand that they are perfectly real during generation.
|
||||
|
||||
The most common way to create an event is to create the event item and the event location, then immediately call
|
||||
`Location.place_locked_item()`:
|
||||
|
||||
```python
|
||||
victory_loc = MyGameLocation(self.player, "Defeat the Final Boss", None, final_boss_arena_region)
|
||||
victory_loc.place_locked_item(MyGameItem("Victory", ItemClassification.progression, None, self.player))
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
set_rule(victory_loc, lambda state: state.has("Boss Defeating Sword", self.player))
|
||||
```
|
||||
|
||||
Requiring an event to finish the game will make the spoiler log display an additional
|
||||
`Defeat the Final Boss: Victory` line when the player is expected to finish, rather than only showing their last
|
||||
relevant item. But events aren't just about the spoiler log; a more substantial example of using events to structure
|
||||
your logic might be:
|
||||
|
||||
```python
|
||||
water_loc = MyGameLocation(self.player, "Water Level Switch", None, pump_station_region)
|
||||
water_loc.place_locked_item(MyGameItem("Lowered Water Level", ItemClassification.progression, None, self.player))
|
||||
pump_station_region.locations.append(water_loc)
|
||||
set_rule(water_loc, lambda state: state.has("Double Jump", self.player)) # the switch is really high up
|
||||
...
|
||||
basement_loc = MyGameLocation(self.player, "Flooded House - Basement Chest", None, flooded_house_region)
|
||||
flooded_house_region.locations += [upstairs_loc, ground_floor_loc, basement_loc]
|
||||
...
|
||||
set_rule(basement_loc, lambda state: state.has("Lowered Water Level", self.player))
|
||||
```
|
||||
|
||||
This creates a "Lowered Water Level" event and a regular location whose access rule depends on that
|
||||
event being reachable. If you made several more locations the same way, this would ensure all of those locations can
|
||||
only become reachable when the event location is reachable (i.e. when the water level can be lowered), without
|
||||
copy-pasting the event location's access rule and then repeatedly re-evaluating it. Also, the spoiler log will show
|
||||
`Water Level Switch: Lowered Water Level` when the player is expected to do this.
|
||||
|
||||
To be clear, this example could also be modeled with a second Region (perhaps "Un-Flooded House"). Or you could modify
|
||||
the game so flipping that switch checks a regular AP location in addition to lowering the water level.
|
||||
Events are never required, but it may be cleaner to use an event if e.g. flipping that switch affects the logic in
|
||||
dozens of half-flooded areas that would all otherwise need additional Regions, and you don't want it to be a regular
|
||||
location. It depends on the game.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Your World
|
||||
@@ -488,8 +520,8 @@ In addition, the following methods can be implemented and are called in this ord
|
||||
If it's hard to separate, this can be done during `generate_early` or `create_items` as well.
|
||||
* `create_items(self)`
|
||||
called to place player's items into the MultiWorld's itempool. By the end of this step all regions, locations and
|
||||
items have to be in the MultiWorld's regions and itempool. You cannot add or remove items, locations, or regions
|
||||
after this step. Locations cannot be moved to different regions after this step.
|
||||
items have to be in the MultiWorld's regions and itempool. You cannot add or remove items, locations, or regions after
|
||||
this step. Locations cannot be moved to different regions after this step. This includes event items and locations.
|
||||
* `set_rules(self)`
|
||||
called to set access and item rules on locations and entrances.
|
||||
* `connect_entrances(self)`
|
||||
@@ -501,7 +533,7 @@ In addition, the following methods can be implemented and are called in this ord
|
||||
called to modify item placement before, during, and after the regular fill process; all finishing before
|
||||
`generate_output`. Any items that need to be placed during `pre_fill` should not exist in the itempool, and if there
|
||||
are any items that need to be filled this way, but need to be in state while you fill other items, they can be
|
||||
returned from `get_prefill_items`.
|
||||
returned from `get_pre_fill_items`.
|
||||
* `generate_output(self, output_directory: str)`
|
||||
creates the output files if there is output to be generated. When this is called,
|
||||
`self.multiworld.get_locations(self.player)` has all locations for the player, with attribute `item` pointing to the
|
||||
|
||||
@@ -138,11 +138,6 @@ Root: HKCR; Subkey: "{#MyAppName}kdl3patch"; ValueData: "Arc
|
||||
Root: HKCR; Subkey: "{#MyAppName}kdl3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}kdl3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata\shell\open\command"; ValueData: """{app}\ArchipelagoMinecraftClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apz5"; ValueData: "{#MyAppName}n64zpf"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archipelago Ocarina of Time Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: "";
|
||||
|
||||
178
kvui.py
178
kvui.py
@@ -60,7 +60,10 @@ from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogSupporting
|
||||
from kivymd.uix.gridlayout import MDGridLayout
|
||||
from kivymd.uix.floatlayout import MDFloatLayout
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
from kivymd.uix.tab.tab import MDTabsSecondary, MDTabsItem, MDTabsItemText, MDTabsCarousel
|
||||
from kivymd.uix.navigationbar import MDNavigationBar, MDNavigationItem
|
||||
from kivymd.uix.screen import MDScreen
|
||||
from kivymd.uix.screenmanager import MDScreenManager
|
||||
|
||||
from kivymd.uix.menu import MDDropdownMenu
|
||||
from kivymd.uix.menu.menu import MDDropdownTextItem
|
||||
from kivymd.uix.dropdownitem import MDDropDownItem, MDDropDownItemText
|
||||
@@ -726,6 +729,10 @@ class MessageBox(Popup):
|
||||
self.height += max(0, label.height - 18)
|
||||
|
||||
|
||||
class MDNavigationItemBase(MDNavigationItem):
|
||||
text = StringProperty(None)
|
||||
|
||||
|
||||
class ButtonsPrompt(MDDialog):
|
||||
def __init__(self, title: str, text: str, response: typing.Callable[[str], None],
|
||||
*prompts: str, **kwargs) -> None:
|
||||
@@ -766,58 +773,34 @@ class ButtonsPrompt(MDDialog):
|
||||
)
|
||||
|
||||
|
||||
class ClientTabs(MDTabsSecondary):
|
||||
carousel: MDTabsCarousel
|
||||
lock_swiping = True
|
||||
class MDScreenManagerBase(MDScreenManager):
|
||||
current_tab: MDNavigationItemBase
|
||||
local_screen_names: list[str]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.carousel = MDTabsCarousel(lock_swiping=True, anim_move_duration=0.2)
|
||||
super().__init__(*args, MDDivider(size_hint_y=None, height=dp(1)), self.carousel, **kwargs)
|
||||
self.size_hint_y = 1
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.local_screen_names = []
|
||||
|
||||
def _check_panel_height(self, *args):
|
||||
self.ids.tab_scroll.height = dp(38)
|
||||
|
||||
def update_indicator(
|
||||
self, x: float = 0.0, w: float = 0.0, instance: MDTabsItem = None
|
||||
) -> None:
|
||||
def update_indicator(*args):
|
||||
indicator_pos = (0, 0)
|
||||
indicator_size = (0, 0)
|
||||
|
||||
item_text_object = self._get_tab_item_text_icon_object()
|
||||
|
||||
if item_text_object:
|
||||
indicator_pos = (
|
||||
instance.x + dp(12),
|
||||
self.indicator.pos[1]
|
||||
if not self._tabs_carousel
|
||||
else self._tabs_carousel.height,
|
||||
)
|
||||
indicator_size = (
|
||||
instance.width - dp(24),
|
||||
self.indicator_height,
|
||||
)
|
||||
|
||||
Animation(
|
||||
pos=indicator_pos,
|
||||
size=indicator_size,
|
||||
d=0 if not self.indicator_anim else self.indicator_duration,
|
||||
t=self.indicator_transition,
|
||||
).start(self.indicator)
|
||||
|
||||
if not instance:
|
||||
self.indicator.pos = (x, self.indicator.pos[1])
|
||||
self.indicator.size = (w, self.indicator_height)
|
||||
def add_widget(self, widget: Widget, *args, **kwargs) -> None:
|
||||
super().add_widget(widget, *args, **kwargs)
|
||||
if "index" in kwargs:
|
||||
self.local_screen_names.insert(kwargs["index"], widget.name)
|
||||
else:
|
||||
Clock.schedule_once(update_indicator)
|
||||
self.local_screen_names.append(widget.name)
|
||||
|
||||
def remove_tab(self, tab, content=None):
|
||||
if content is None:
|
||||
content = tab.content
|
||||
self.ids.container.remove_widget(tab)
|
||||
self.carousel.remove_widget(content)
|
||||
self.on_size(self, self.size)
|
||||
def switch_screens(self, new_tab: MDNavigationItemBase) -> None:
|
||||
"""
|
||||
Called whenever the user clicks a tab to switch to a different screen.
|
||||
|
||||
:param new_tab: The new screen to switch to's tab.
|
||||
"""
|
||||
name = new_tab.text
|
||||
if self.local_screen_names.index(name) > self.local_screen_names.index(self.current_screen.name):
|
||||
self.transition.direction = "left"
|
||||
else:
|
||||
self.transition.direction = "right"
|
||||
self.current = name
|
||||
self.current_tab = new_tab
|
||||
|
||||
|
||||
class CommandButton(MDButton, MDTooltip):
|
||||
@@ -845,6 +828,9 @@ class GameManager(ThemedApp):
|
||||
main_area_container: MDGridLayout
|
||||
""" subclasses can add more columns beside the tabs """
|
||||
|
||||
tabs: MDNavigationBar
|
||||
screens: MDScreenManagerBase
|
||||
|
||||
def __init__(self, ctx: context_type):
|
||||
self.title = self.base_title
|
||||
self.ctx = ctx
|
||||
@@ -874,7 +860,7 @@ class GameManager(ThemedApp):
|
||||
@property
|
||||
def tab_count(self):
|
||||
if hasattr(self, "tabs"):
|
||||
return max(1, len(self.tabs.tab_list))
|
||||
return max(1, len(self.tabs.children))
|
||||
return 1
|
||||
|
||||
def on_start(self):
|
||||
@@ -914,30 +900,32 @@ class GameManager(ThemedApp):
|
||||
self.grid.add_widget(self.progressbar)
|
||||
|
||||
# middle part
|
||||
self.tabs = ClientTabs(pos_hint={"center_x": 0.5, "center_y": 0.5})
|
||||
self.tabs.add_widget(MDTabsItem(MDTabsItemText(text="All" if len(self.logging_pairs) > 1 else "Archipelago")))
|
||||
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
|
||||
for logger_name, name in
|
||||
self.logging_pairs))
|
||||
self.tabs.carousel.add_widget(self.tabs.default_tab_content)
|
||||
self.screens = MDScreenManagerBase(pos_hint={"center_x": 0.5})
|
||||
self.tabs = MDNavigationBar(orientation="horizontal", size_hint_y=None, height=dp(40), set_bars_color=True)
|
||||
# bind the method to the bar for back compatibility
|
||||
self.tabs.remove_tab = self.remove_client_tab
|
||||
self.screens.current_tab = self.add_client_tab(
|
||||
"All" if len(self.logging_pairs) > 1 else "Archipelago",
|
||||
UILog(*(logging.getLogger(logger_name) for logger_name, name in self.logging_pairs)),
|
||||
)
|
||||
self.log_panels["All"] = self.screens.current_tab.content
|
||||
self.screens.current_tab.active = True
|
||||
|
||||
for logger_name, display_name in self.logging_pairs:
|
||||
bridge_logger = logging.getLogger(logger_name)
|
||||
self.log_panels[display_name] = UILog(bridge_logger)
|
||||
if len(self.logging_pairs) > 1:
|
||||
panel = MDTabsItem(MDTabsItemText(text=display_name))
|
||||
panel.content = self.log_panels[display_name]
|
||||
# show Archipelago tab if other logging is present
|
||||
self.tabs.carousel.add_widget(panel.content)
|
||||
self.tabs.add_widget(panel)
|
||||
self.add_client_tab(display_name, self.log_panels[display_name])
|
||||
|
||||
hint_panel = self.add_client_tab("Hints", HintLayout())
|
||||
self.hint_log = HintLog(self.json_to_kivy_parser)
|
||||
hint_panel = self.add_client_tab("Hints", HintLayout(self.hint_log))
|
||||
self.log_panels["Hints"] = hint_panel.content
|
||||
hint_panel.content.add_widget(self.hint_log)
|
||||
|
||||
self.main_area_container = MDGridLayout(size_hint_y=1, rows=1)
|
||||
self.main_area_container.add_widget(self.tabs)
|
||||
tab_container = MDGridLayout(size_hint_y=1, cols=1)
|
||||
tab_container.add_widget(self.tabs)
|
||||
tab_container.add_widget(self.screens)
|
||||
self.main_area_container.add_widget(tab_container)
|
||||
|
||||
self.grid.add_widget(self.main_area_container)
|
||||
|
||||
@@ -974,25 +962,61 @@ class GameManager(ThemedApp):
|
||||
|
||||
return self.container
|
||||
|
||||
def add_client_tab(self, title: str, content: Widget, index: int = -1) -> Widget:
|
||||
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content.
|
||||
Returns the new tab widget, with the provided content being placed on the tab as content."""
|
||||
new_tab = MDTabsItem(MDTabsItemText(text=title))
|
||||
def add_client_tab(self, title: str, content: Widget, index: int = -1) -> MDNavigationItemBase:
|
||||
"""
|
||||
Adds a new tab to the client window with a given title, and provides a given Widget as its content.
|
||||
Returns the new tab widget, with the provided content being placed on the tab as content.
|
||||
|
||||
:param title: The title of the tab.
|
||||
:param content: The Widget to be added as content for this tab's new MDScreen. Will also be added to the
|
||||
returned tab as tab.content.
|
||||
:param index: The index to insert the tab at. Defaults to -1, meaning the tab will be appended to the end.
|
||||
|
||||
:return: The new tab.
|
||||
"""
|
||||
if self.tabs.children:
|
||||
self.tabs.add_widget(MDDivider(orientation="vertical"))
|
||||
new_tab = MDNavigationItemBase(text=title)
|
||||
new_tab.content = content
|
||||
if -1 < index <= len(self.tabs.carousel.slides):
|
||||
new_tab.bind(on_release=self.tabs.set_active_item)
|
||||
new_tab._tabs = self.tabs
|
||||
self.tabs.ids.container.add_widget(new_tab, index=index)
|
||||
self.tabs.carousel.add_widget(new_tab.content, index=len(self.tabs.carousel.slides) - index)
|
||||
new_screen = MDScreen(name=title)
|
||||
new_screen.add_widget(content)
|
||||
if -1 < index <= len(self.tabs.children):
|
||||
remapped_index = len(self.tabs.children) - index
|
||||
self.tabs.add_widget(new_tab, index=remapped_index)
|
||||
self.screens.add_widget(new_screen, index=index)
|
||||
else:
|
||||
self.tabs.add_widget(new_tab)
|
||||
self.tabs.carousel.add_widget(new_tab.content)
|
||||
self.screens.add_widget(new_screen)
|
||||
return new_tab
|
||||
|
||||
def remove_client_tab(self, tab: MDNavigationItemBase) -> None:
|
||||
"""
|
||||
Called to remove a tab and its screen.
|
||||
|
||||
:param tab: The tab to remove.
|
||||
"""
|
||||
tab_index = self.tabs.children.index(tab)
|
||||
# if the tab is currently active we need to swap before removing it
|
||||
if tab == self.screens.current_tab:
|
||||
if not tab_index:
|
||||
# account for the divider
|
||||
swap_index = tab_index + 2
|
||||
else:
|
||||
swap_index = tab_index - 2
|
||||
self.tabs.children[swap_index].on_release()
|
||||
# self.screens.switch_screens(self.tabs.children[swap_index])
|
||||
# get the divider to the left if we can
|
||||
if not tab_index:
|
||||
divider_index = tab_index + 1
|
||||
else:
|
||||
divider_index = tab_index - 1
|
||||
self.tabs.remove_widget(self.tabs.children[divider_index])
|
||||
self.tabs.remove_widget(tab)
|
||||
self.screens.remove_widget(self.screens.get_screen(tab.text))
|
||||
|
||||
def update_texts(self, dt):
|
||||
for slide in self.tabs.carousel.slides:
|
||||
if hasattr(slide, "fix_heights"):
|
||||
slide.fix_heights() # TODO: remove this when Kivy fixes this upstream
|
||||
if hasattr(self.screens.current_tab.content, "fix_heights"):
|
||||
getattr(self.screens.current_tab.content, "fix_heights")()
|
||||
if self.ctx.server:
|
||||
self.title = self.base_title + " " + Utils.__version__ + \
|
||||
f" | Connected to: {self.ctx.server_address} " \
|
||||
|
||||
1
setup.py
1
setup.py
@@ -63,7 +63,6 @@ non_apworlds: set[str] = {
|
||||
"Adventure",
|
||||
"ArchipIDLE",
|
||||
"Archipelago",
|
||||
"Clique",
|
||||
"Lufia II Ancient Cave",
|
||||
"Meritous",
|
||||
"Ocarina of Time",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import unittest
|
||||
|
||||
from Fill import distribute_items_restrictive
|
||||
from NetUtils import encode
|
||||
from NetUtils import convert_to_base_types
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all
|
||||
from worlds import failed_world_loads
|
||||
from . import setup_solo_multiworld
|
||||
@@ -47,7 +47,7 @@ class TestImplemented(unittest.TestCase):
|
||||
call_all(multiworld, "post_fill")
|
||||
for key, data in multiworld.worlds[1].fill_slot_data().items():
|
||||
self.assertIsInstance(key, str, "keys in slot data must be a string")
|
||||
self.assertIsInstance(encode(data), str, f"object {type(data).__name__} not serializable.")
|
||||
convert_to_base_types(data) # only put base data types into slot data
|
||||
|
||||
def test_no_failed_world_loads(self):
|
||||
if failed_world_loads:
|
||||
|
||||
@@ -63,12 +63,12 @@ if __name__ == "__main__":
|
||||
spacer = '=' * 80
|
||||
|
||||
with TemporaryDirectory() as tempdir:
|
||||
multis = [["Clique"], ["Temp World"], ["Clique", "Temp World"]]
|
||||
multis = [["VVVVVV"], ["Temp World"], ["VVVVVV", "Temp World"]]
|
||||
p1_games = []
|
||||
data_paths = []
|
||||
rooms = []
|
||||
|
||||
copy_world("Clique", "Temp World")
|
||||
copy_world("VVVVVV", "Temp World")
|
||||
try:
|
||||
for n, games in enumerate(multis, 1):
|
||||
print(f"Generating [{n}] {', '.join(games)}")
|
||||
@@ -101,7 +101,7 @@ if __name__ == "__main__":
|
||||
with Client(host.address, game, "Player1") as client:
|
||||
local_data_packages = client.games_packages
|
||||
local_collected_items = len(client.checked_locations)
|
||||
if collected_items < 2: # Clique only has 2 Locations
|
||||
if collected_items < 2: # Don't collect anything on the last iteration
|
||||
client.collect_any()
|
||||
# TODO: Ctrl+C test here as well
|
||||
|
||||
@@ -125,7 +125,7 @@ if __name__ == "__main__":
|
||||
with Client(host.address, game, "Player1") as client:
|
||||
web_data_packages = client.games_packages
|
||||
web_collected_items = len(client.checked_locations)
|
||||
if collected_items < 2: # Clique only has 2 Locations
|
||||
if collected_items < 2: # Don't collect anything on the last iteration
|
||||
client.collect_any()
|
||||
if collected_items == 1:
|
||||
sleep(1) # wait for the server to collect the item
|
||||
|
||||
@@ -34,7 +34,7 @@ def _generate_local_inner(games: Iterable[str],
|
||||
f.write(json.dumps({
|
||||
"name": f"Player{n}",
|
||||
"game": game,
|
||||
game: {"hard_mode": "true"},
|
||||
game: {},
|
||||
"description": f"generate_local slot {n} ('Player{n}'): {game}",
|
||||
}))
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ def copy(src: str, dst: str) -> None:
|
||||
_new_worlds[dst] = str(dst_folder)
|
||||
with open(dst_folder / "__init__.py", "r", encoding="utf-8-sig") as f:
|
||||
contents = f.read()
|
||||
contents = re.sub(r'game\s*=\s*[\'"]' + re.escape(src) + r'[\'"]', f'game = "{dst}"', contents)
|
||||
contents = re.sub(r'game\s*(:\s*[a-zA-Z\[\]]+)?\s*=\s*[\'"]' + re.escape(src) + r'[\'"]', f'game = "{dst}"', contents)
|
||||
with open(dst_folder / "__init__.py", "w", encoding="utf-8") as f:
|
||||
f.write(contents)
|
||||
|
||||
|
||||
@@ -382,7 +382,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
def create_items(self) -> None:
|
||||
"""
|
||||
Method for creating and submitting items to the itempool. Items and Regions must *not* be created and submitted
|
||||
to the MultiWorld after this step. If items need to be placed during pre_fill use `get_prefill_items`.
|
||||
to the MultiWorld after this step. If items need to be placed during pre_fill use `get_pre_fill_items`.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -221,9 +221,6 @@ components: List[Component] = [
|
||||
Component('Links Awakening DX Client', 'LinksAwakeningClient',
|
||||
file_identifier=SuffixIdentifier('.apladx')),
|
||||
Component('LttP Adjuster', 'LttPAdjuster'),
|
||||
# Minecraft
|
||||
Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True,
|
||||
file_identifier=SuffixIdentifier('.apmc')),
|
||||
# Ocarina of Time
|
||||
Component('OoT Client', 'OoTClient',
|
||||
file_identifier=SuffixIdentifier('.apz5')),
|
||||
@@ -246,6 +243,5 @@ components: List[Component] = [
|
||||
# if registering an icon from within an apworld, the format "ap:module.name/path/to/file.png" can be used
|
||||
icon_paths = {
|
||||
'icon': local_path('data', 'icon.png'),
|
||||
'mcicon': local_path('data', 'mcicon.png'),
|
||||
'discord': local_path('data', 'discord-mark-blue.png'),
|
||||
}
|
||||
|
||||
@@ -548,10 +548,12 @@ def set_up_take_anys(multiworld, world, player):
|
||||
old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots)
|
||||
multiworld.shops.append(old_man_take_any.shop)
|
||||
|
||||
swords = [item for item in multiworld.itempool if item.player == player and item.type == 'Sword']
|
||||
if swords:
|
||||
sword = multiworld.random.choice(swords)
|
||||
multiworld.itempool.remove(sword)
|
||||
sword_indices = [
|
||||
index for index, item in enumerate(multiworld.itempool) if item.player == player and item.type == 'Sword'
|
||||
]
|
||||
if sword_indices:
|
||||
sword_index = multiworld.random.choice(sword_indices)
|
||||
sword = multiworld.itempool.pop(sword_index)
|
||||
multiworld.itempool.append(item_factory('Rupees (20)', world))
|
||||
old_man_take_any.shop.add_inventory(0, sword.name, 0, 0)
|
||||
loc_name = "Old Man Sword Cave"
|
||||
|
||||
@@ -38,7 +38,7 @@ class DungeonFillTestBase(TestCase):
|
||||
def test_original_dungeons(self):
|
||||
self.generate_with_options(DungeonItem.option_original_dungeon)
|
||||
for location in self.multiworld.get_filled_locations():
|
||||
with (self.subTest(location=location)):
|
||||
with (self.subTest(location_name=location.name)):
|
||||
if location.parent_region.dungeon is None:
|
||||
self.assertIs(location.item.dungeon, None)
|
||||
else:
|
||||
@@ -52,7 +52,7 @@ class DungeonFillTestBase(TestCase):
|
||||
def test_own_dungeons(self):
|
||||
self.generate_with_options(DungeonItem.option_own_dungeons)
|
||||
for location in self.multiworld.get_filled_locations():
|
||||
with self.subTest(location=location):
|
||||
with self.subTest(location_name=location.name):
|
||||
if location.parent_region.dungeon is None:
|
||||
self.assertIs(location.item.dungeon, None)
|
||||
else:
|
||||
|
||||
@@ -4,7 +4,7 @@ Date: Fri, 15 Mar 2024 18:41:40 +0000
|
||||
Description: Used to manage Regions in the Aquaria game multiworld randomizer
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, Iterable
|
||||
from BaseClasses import MultiWorld, Region, Entrance, Item, ItemClassification, CollectionState
|
||||
from .Items import AquariaItem, ItemNames
|
||||
from .Locations import AquariaLocations, AquariaLocation, AquariaLocationNames
|
||||
@@ -34,10 +34,15 @@ def _has_li(state: CollectionState, player: int) -> bool:
|
||||
return state.has(ItemNames.LI_AND_LI_SONG, player)
|
||||
|
||||
|
||||
def _has_damaging_item(state: CollectionState, player: int) -> bool:
|
||||
"""`player` in `state` has the shield song item"""
|
||||
return state.has_any({ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM, ItemNames.LI_AND_LI_SONG,
|
||||
ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA, ItemNames.BABY_BLASTER}, player)
|
||||
DAMAGING_ITEMS:Iterable[str] = [
|
||||
ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM,
|
||||
ItemNames.LI_AND_LI_SONG, ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA,
|
||||
ItemNames.BABY_BLASTER
|
||||
]
|
||||
|
||||
def _has_damaging_item(state: CollectionState, player: int, damaging_items:Iterable[str] = DAMAGING_ITEMS) -> bool:
|
||||
"""`player` in `state` has the an item that do damage other than the ones in `to_remove`"""
|
||||
return state.has_any(damaging_items, player)
|
||||
|
||||
|
||||
def _has_energy_attack_item(state: CollectionState, player: int) -> bool:
|
||||
@@ -566,9 +571,11 @@ class AquariaRegions:
|
||||
self.__connect_one_way_regions(self.openwater_tr, self.openwater_tr_turtle,
|
||||
lambda state: _has_beast_form_or_arnassi_armor(state, self.player))
|
||||
self.__connect_one_way_regions(self.openwater_tr_turtle, self.openwater_tr)
|
||||
damaging_items_minus_nature_form = [item for item in DAMAGING_ITEMS if item != ItemNames.NATURE_FORM]
|
||||
self.__connect_one_way_regions(self.openwater_tr, self.openwater_tr_urns,
|
||||
lambda state: _has_bind_song(state, self.player) or
|
||||
_has_damaging_item(state, self.player))
|
||||
_has_damaging_item(state, self.player,
|
||||
damaging_items_minus_nature_form))
|
||||
self.__connect_regions(self.openwater_tr, self.openwater_br)
|
||||
self.__connect_regions(self.openwater_tr, self.mithalas_city)
|
||||
self.__connect_regions(self.openwater_tr, self.veil_b)
|
||||
|
||||
@@ -78,8 +78,8 @@ boosts: List[CivVIBoostData] = [
|
||||
CivVIBoostData(
|
||||
"BOOST_TECH_IRON_WORKING",
|
||||
"ERA_CLASSICAL",
|
||||
["TECH_MINING"],
|
||||
1,
|
||||
["TECH_MINING", "TECH_BRONZE_WORKING"],
|
||||
2,
|
||||
"DEFAULT",
|
||||
),
|
||||
CivVIBoostData(
|
||||
@@ -165,15 +165,9 @@ boosts: List[CivVIBoostData] = [
|
||||
"BOOST_TECH_CASTLES",
|
||||
"ERA_MEDIEVAL",
|
||||
[
|
||||
"CIVIC_DIVINE_RIGHT",
|
||||
"CIVIC_EXPLORATION",
|
||||
"CIVIC_REFORMED_CHURCH",
|
||||
"CIVIC_SUFFRAGE",
|
||||
"CIVIC_TOTALITARIANISM",
|
||||
"CIVIC_CLASS_STRUGGLE",
|
||||
"CIVIC_DIGITAL_DEMOCRACY",
|
||||
"CIVIC_CORPORATE_LIBERTARIANISM",
|
||||
"CIVIC_SYNTHETIC_TECHNOCRACY",
|
||||
],
|
||||
1,
|
||||
"DEFAULT",
|
||||
@@ -393,9 +387,6 @@ boosts: List[CivVIBoostData] = [
|
||||
"CIVIC_SUFFRAGE",
|
||||
"CIVIC_TOTALITARIANISM",
|
||||
"CIVIC_CLASS_STRUGGLE",
|
||||
"CIVIC_DIGITAL_DEMOCRACY",
|
||||
"CIVIC_CORPORATE_LIBERTARIANISM",
|
||||
"CIVIC_SYNTHETIC_TECHNOCRACY",
|
||||
],
|
||||
1,
|
||||
"DEFAULT",
|
||||
|
||||
@@ -20,16 +20,17 @@ A short period after receiving an item, you will get a notification indicating y
|
||||
|
||||
## FAQs
|
||||
- Do I need the DLC to play this?
|
||||
- Yes, you need both Rise & Fall and Gathering Storm.
|
||||
- You need both expansions, Rise & Fall and Gathering Storm. You do not need the other DLCs but they fully work with this.
|
||||
- Does this work with Multiplayer?
|
||||
- It does not and, despite my best efforts, probably won't until there's a new way for external programs to be able to interact with the game.
|
||||
- Does my mod that reskins Barbarians as various Pro Wrestlers work with this?
|
||||
- Only one way to find out! Any mods that modify techs/civics will most likely cause issues, though.
|
||||
- Does this work with other mods?
|
||||
- A lot of mods seem to work without issues combined with this, but you should avoid any mods that change things in the tech or civic tree, as even if they would work it could cause issues with the logic.
|
||||
- "Help! I can't see any of the items that have been sent to me!"
|
||||
- Both trees by default will show you the researchable Archipelago locations. To view the normal tree, you can click "Toggle Archipelago Tree" in the top-left corner of the tree view.
|
||||
- "Oh no! I received the Machinery tech and now instead of getting an Archer next turn, I have to wait an additional 10 turns to get a Crossbowman!"
|
||||
- Vanilla prevents you from building units of the same class from an earlier tech level after you have researched a later variant. For example, this could be problematic if someone unlocks Crossbowmen for you right out the gate since you won't be able to make Archers (which have a much lower production cost).
|
||||
Solution: You can now go in to the tech tree, click "Toggle Archipelago Tree" to view your unlocked techs, and then can click any tech you have unlocked to toggle whether it is currently active or not.
|
||||
- Solution: You can now go in to the tech tree, click "Toggle Archipelago Tree" to view your unlocked techs, and then can click any tech you have unlocked to toggle whether it is currently active or not.
|
||||
- If you think you should be able to make Field Cannons but seemingly can't try disabling `Telecommunications`
|
||||
- "How does DeathLink work? Am I going to have to start a new game every time one of my friends dies?"
|
||||
- Heavens no, my fellow Archipelago appreciator. When configuring your Archipelago options for Civilization on the options page, there are several choices available for you to fine tune the way you'd like to be punished for the follies of your friends. These include: Having a random unit destroyed, losing a percentage of gold or faith, or even losing a point on your era score. If you can't make up your mind, you can elect to have any of them be selected every time a death link is sent your way.
|
||||
In the event you lose one of your units in combat (this means captured units don't count), then you will send a death link event to the rest of your friends.
|
||||
@@ -39,7 +40,8 @@ Solution: You can now go in to the tech tree, click "Toggle Archipelago Tree" to
|
||||
1. `TECH_WRITING`
|
||||
2. `TECH_EDUCATION`
|
||||
3. `TECH_CHEMISTRY`
|
||||
- If you want to see the details around each item, you can review [this file](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/progressive_districts.json).
|
||||
- An important thing to note is that the seaport is part of progressive industrial zones, due to electricity having both an industrial zone building and the seaport.
|
||||
- If you want to see the details around each item, you can review [this file](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/progressive_districts.py).
|
||||
|
||||
## Boostsanity
|
||||
Boostsanity takes all of the Eureka & Inspiration events and makes them location checks. This feature is the one to change up the way Civilization is played in an AP multiworld/randomizer. What normally are mundane tasks that are passively collected now become a novel and interesting bucket list that you need to pay attention to in order to unlock items for yourself and others!
|
||||
@@ -56,4 +58,3 @@ Boosts have logic associated with them in order to verify you can always reach t
|
||||
- The unpredictable timing of boosts and unlocking them can occasionally lead to scenarios where you'll have to first encounter a locked era defeat and then load a previous save. To help reduce the frequency of this, local `PROGRESSIVE_ERA` items will never be located at a boost check.
|
||||
- There's too many boosts, how will I know which one's I should focus on?!
|
||||
- In order to give a little more focus to all the boosts rather than just arbitrarily picking them at random, items in both of the vanilla trees will now have an advisor icon on them if its associated boost contains a progression item.
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ This guide is meant to help you get up and running with Civilization VI in Archi
|
||||
|
||||
The following are required in order to play Civ VI in Archipelago:
|
||||
|
||||
- Windows OS (Firaxis does not support the necessary tooling for Mac, or Linux)
|
||||
- Windows OS (Firaxis does not support the necessary tooling for Mac, or Linux).
|
||||
|
||||
- Installed [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) v0.4.5 or higher.
|
||||
- Installed [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
|
||||
- The latest version of the [Civ VI AP Mod](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
|
||||
|
||||
- A copy of the game `Civilization VI` including the two expansions `Rise & Fall` and `Gathering Storm` (both the Steam and Epic version should work).
|
||||
|
||||
## Enabling the tuner
|
||||
|
||||
In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
|
||||
@@ -20,27 +22,32 @@ In the main menu, navigate to the "Game Options" page. On the "Game" menu, make
|
||||
|
||||
1. Download and unzip the latest release of the mod from [GitHub](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
|
||||
|
||||
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. If you use OneDrive, check if the folder is instead located in your OneDrive file structure.
|
||||
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. If you use OneDrive, check if the folder is instead located in your OneDrive file structure, and use that path when relevant in future steps.
|
||||
|
||||
3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. Associate the file with the Archipelago Launcher and double click it.
|
||||
|
||||
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. If double clicking the `.apcivvi` file doesn't generate a folder, you can just rename it to a file ending with `.zip` and extract its contents to a new folder. To do this, right click the `.apcivvi` file and click "Rename", make sure it ends in `.zip`, then right click it again and select "Extract All".
|
||||
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. If double clicking the `.apcivvi` file doesn't generate a folder, you can instead open it as a zip file. You can do this by either right clicking it and opening it with a program that handles zip files, or by right clicking and renaming the file extension from `apcivvi` to `zip`.
|
||||
|
||||
5. Your finished mod folder should look something like this:
|
||||
|
||||
- Civ VI Mods Directory
|
||||
- civilization_archipelago_mod
|
||||
- NewItems.xml
|
||||
- InitOptions.lua
|
||||
- Archipelago.modinfo
|
||||
- All the other mod files, etc.
|
||||
5. Place the files generated from the `.apcivvi` in your archipelago mod folder (there should be five files placed there from the apcivvi file, overwrite if asked). Your mod path should look something like `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods\civilization_archipelago_mod`.
|
||||
|
||||
## Configuring your game
|
||||
|
||||
When configuring your game, make sure to start the game in the Ancient Era and leave all settings related to starting technologies and civics as the defaults. Other than that, configure difficulty, AI, etc. as you normally would.
|
||||
Make sure you enable the mod in the main title under Additional Content > Mods. When configuring your game, make sure to start the game in the Ancient Era and leave all settings related to starting technologies and civics as the defaults. Other than that, configure difficulty, AI, etc. as you normally would.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If you have troubles with file extension related stuff, make sure Windows shows file extensions as they are turned off by default. If you don't know how to turn them on it is just a quick google search away.
|
||||
|
||||
- If you are getting an error: "The remote computer refused the network connection", or something else related to the client (or tuner) not being able to connect, it likely indicates the tuner is not actually enabled. One simple way to verify that it is enabled is, after completing the setup steps, go to Main Menu → Options → Look for an option named "Tuner" and verify it is set to "Enabled"
|
||||
|
||||
- If your game gets in a state where someone has sent you items or you have sent locations but these are not correctly sent to the multiworld, you can run `/resync` from the Civ 6 client. This may take up to a minute depending on how many items there are.
|
||||
- If your game gets in a state where someone has sent you items or you have sent locations but these are not correctly sent to the multiworld, you can run `/resync` from the Civ 6 client. This may take up to a minute depending on how many items there are. This can resend certain items to you, like one time bonuses.
|
||||
|
||||
- If the archipelago mod does not appear in the mod selector in the game, make sure the mod is correctly placed as a folder in the `Sid Meier's Civilization VI\Mods` folder, there should not be any loose files in there only folders. As in the path should look something like `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods\civilization_archipelago_mod`.
|
||||
|
||||
- If it still does not appear make sure you have the right folder, one way to verify you are in the right place is to find the general folder area where your Civ VI save files are located.
|
||||
|
||||
- If you get an error when trying to start a game saying `Error - One or more Mods failed to load content`, make sure the files from the `.apcivvi` are placed into the `civilization_archipelago_mod` as loose files and not as a folder.
|
||||
|
||||
- If you still have any errors make sure the two expansions Rise & Fall and Gathering Storm are active in the mod selector (all the official DLC works without issues but Rise & Fall and Gathering Storm are required for the mod).
|
||||
|
||||
- If boostsanity is enabled and those items are not being sent out but regular techs are, make sure you placed the files from your new room in the mod folder.
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
from typing import Callable, Dict, NamedTuple, Optional, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import CliqueWorld
|
||||
|
||||
|
||||
class CliqueItem(Item):
|
||||
game = "Clique"
|
||||
|
||||
|
||||
class CliqueItemData(NamedTuple):
|
||||
code: Optional[int] = None
|
||||
type: ItemClassification = ItemClassification.filler
|
||||
can_create: Callable[["CliqueWorld"], bool] = lambda world: True
|
||||
|
||||
|
||||
item_data_table: Dict[str, CliqueItemData] = {
|
||||
"Feeling of Satisfaction": CliqueItemData(
|
||||
code=69696969,
|
||||
type=ItemClassification.progression,
|
||||
),
|
||||
"Button Activation": CliqueItemData(
|
||||
code=69696968,
|
||||
type=ItemClassification.progression,
|
||||
can_create=lambda world: world.options.hard_mode,
|
||||
),
|
||||
"A Cool Filler Item (No Satisfaction Guaranteed)": CliqueItemData(
|
||||
code=69696967,
|
||||
can_create=lambda world: False # Only created from `get_filler_item_name`.
|
||||
),
|
||||
"The Urge to Push": CliqueItemData(
|
||||
type=ItemClassification.progression,
|
||||
),
|
||||
}
|
||||
|
||||
item_table = {name: data.code for name, data in item_data_table.items() if data.code is not None}
|
||||
@@ -1,37 +0,0 @@
|
||||
from typing import Callable, Dict, NamedTuple, Optional, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Location
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import CliqueWorld
|
||||
|
||||
|
||||
class CliqueLocation(Location):
|
||||
game = "Clique"
|
||||
|
||||
|
||||
class CliqueLocationData(NamedTuple):
|
||||
region: str
|
||||
address: Optional[int] = None
|
||||
can_create: Callable[["CliqueWorld"], bool] = lambda world: True
|
||||
locked_item: Optional[str] = None
|
||||
|
||||
|
||||
location_data_table: Dict[str, CliqueLocationData] = {
|
||||
"The Big Red Button": CliqueLocationData(
|
||||
region="The Button Realm",
|
||||
address=69696969,
|
||||
),
|
||||
"The Item on the Desk": CliqueLocationData(
|
||||
region="The Button Realm",
|
||||
address=69696968,
|
||||
can_create=lambda world: world.options.hard_mode,
|
||||
),
|
||||
"In the Player's Mind": CliqueLocationData(
|
||||
region="The Button Realm",
|
||||
locked_item="The Urge to Push",
|
||||
),
|
||||
}
|
||||
|
||||
location_table = {name: data.address for name, data in location_data_table.items() if data.address is not None}
|
||||
locked_locations = {name: data for name, data in location_data_table.items() if data.locked_item}
|
||||
@@ -1,34 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
from Options import Choice, Toggle, PerGameCommonOptions, StartInventoryPool
|
||||
|
||||
|
||||
class HardMode(Toggle):
|
||||
"""Only for the most masochistically inclined... Requires button activation!"""
|
||||
display_name = "Hard Mode"
|
||||
|
||||
|
||||
class ButtonColor(Choice):
|
||||
"""Customize your button! Now available in 12 unique colors."""
|
||||
display_name = "Button Color"
|
||||
option_red = 0
|
||||
option_orange = 1
|
||||
option_yellow = 2
|
||||
option_green = 3
|
||||
option_cyan = 4
|
||||
option_blue = 5
|
||||
option_magenta = 6
|
||||
option_purple = 7
|
||||
option_pink = 8
|
||||
option_brown = 9
|
||||
option_white = 10
|
||||
option_black = 11
|
||||
|
||||
|
||||
@dataclass
|
||||
class CliqueOptions(PerGameCommonOptions):
|
||||
color: ButtonColor
|
||||
hard_mode: HardMode
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
|
||||
# DeathLink is always on. Always.
|
||||
# death_link: DeathLink
|
||||
@@ -1,11 +0,0 @@
|
||||
from typing import Dict, List, NamedTuple
|
||||
|
||||
|
||||
class CliqueRegionData(NamedTuple):
|
||||
connecting_regions: List[str] = []
|
||||
|
||||
|
||||
region_data_table: Dict[str, CliqueRegionData] = {
|
||||
"Menu": CliqueRegionData(["The Button Realm"]),
|
||||
"The Button Realm": CliqueRegionData(),
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
from typing import Callable, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import CliqueWorld
|
||||
|
||||
|
||||
def get_button_rule(world: "CliqueWorld") -> Callable[[CollectionState], bool]:
|
||||
if world.options.hard_mode:
|
||||
return lambda state: state.has("Button Activation", world.player)
|
||||
|
||||
return lambda state: True
|
||||
@@ -1,102 +0,0 @@
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from BaseClasses import Region, Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from .Items import CliqueItem, item_data_table, item_table
|
||||
from .Locations import CliqueLocation, location_data_table, location_table, locked_locations
|
||||
from .Options import CliqueOptions
|
||||
from .Regions import region_data_table
|
||||
from .Rules import get_button_rule
|
||||
|
||||
|
||||
class CliqueWebWorld(WebWorld):
|
||||
theme = "partyTime"
|
||||
|
||||
setup_en = Tutorial(
|
||||
tutorial_name="Start Guide",
|
||||
description="A guide to playing Clique.",
|
||||
language="English",
|
||||
file_name="guide_en.md",
|
||||
link="guide/en",
|
||||
authors=["Phar"]
|
||||
)
|
||||
|
||||
setup_de = Tutorial(
|
||||
tutorial_name="Anleitung zum Anfangen",
|
||||
description="Eine Anleitung um Clique zu spielen.",
|
||||
language="Deutsch",
|
||||
file_name="guide_de.md",
|
||||
link="guide/de",
|
||||
authors=["Held_der_Zeit"]
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_de]
|
||||
game_info_languages = ["en", "de"]
|
||||
|
||||
|
||||
class CliqueWorld(World):
|
||||
"""The greatest game of all time."""
|
||||
|
||||
game = "Clique"
|
||||
web = CliqueWebWorld()
|
||||
options: CliqueOptions
|
||||
options_dataclass = CliqueOptions
|
||||
location_name_to_id = location_table
|
||||
item_name_to_id = item_table
|
||||
|
||||
def create_item(self, name: str) -> CliqueItem:
|
||||
return CliqueItem(name, item_data_table[name].type, item_data_table[name].code, self.player)
|
||||
|
||||
def create_items(self) -> None:
|
||||
item_pool: List[CliqueItem] = []
|
||||
for name, item in item_data_table.items():
|
||||
if item.code and item.can_create(self):
|
||||
item_pool.append(self.create_item(name))
|
||||
|
||||
self.multiworld.itempool += item_pool
|
||||
|
||||
def create_regions(self) -> None:
|
||||
# Create regions.
|
||||
for region_name in region_data_table.keys():
|
||||
region = Region(region_name, self.player, self.multiworld)
|
||||
self.multiworld.regions.append(region)
|
||||
|
||||
# Create locations.
|
||||
for region_name, region_data in region_data_table.items():
|
||||
region = self.get_region(region_name)
|
||||
region.add_locations({
|
||||
location_name: location_data.address for location_name, location_data in location_data_table.items()
|
||||
if location_data.region == region_name and location_data.can_create(self)
|
||||
}, CliqueLocation)
|
||||
region.add_exits(region_data_table[region_name].connecting_regions)
|
||||
|
||||
# Place locked locations.
|
||||
for location_name, location_data in locked_locations.items():
|
||||
# Ignore locations we never created.
|
||||
if not location_data.can_create(self):
|
||||
continue
|
||||
|
||||
locked_item = self.create_item(location_data_table[location_name].locked_item)
|
||||
self.get_location(location_name).place_locked_item(locked_item)
|
||||
|
||||
# Set priority location for the Big Red Button!
|
||||
self.options.priority_locations.value.add("The Big Red Button")
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return "A Cool Filler Item (No Satisfaction Guaranteed)"
|
||||
|
||||
def set_rules(self) -> None:
|
||||
button_rule = get_button_rule(self)
|
||||
self.get_location("The Big Red Button").access_rule = button_rule
|
||||
self.get_location("In the Player's Mind").access_rule = button_rule
|
||||
|
||||
# Do not allow button activations on buttons.
|
||||
self.get_location("The Big Red Button").item_rule = lambda item: item.name != "Button Activation"
|
||||
|
||||
# Completion condition.
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("The Urge to Push", self.player)
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"color": self.options.color.current_key
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
# Clique
|
||||
|
||||
## Was ist das für ein Spiel?
|
||||
|
||||
~~Clique ist ein psychologisches Überlebens-Horror Spiel, in dem der Spieler der Versuchung wiederstehen muss große~~
|
||||
~~(rote) Knöpfe zu drücken.~~
|
||||
|
||||
Clique ist ein scherzhaftes Spiel, welches für Archipelago im März 2023 entwickelt wurde, um zu zeigen, wie einfach
|
||||
es sein kann eine Welt für Archipelago zu entwicklen. Das Ziel des Spiels ist es den großen (standardmäßig) roten
|
||||
Knopf zu drücken. Wenn ein Spieler auf dem `hard_mode` (schwieriger Modus) spielt, muss dieser warten bis jemand
|
||||
anderes in der Multiworld den Knopf aktiviert, damit er gedrückt werden kann.
|
||||
|
||||
Clique kann auf den meisten modernen, HTML5-fähigen Browsern gespielt werden.
|
||||
|
||||
## Wo ist die Seite für die Einstellungen?
|
||||
|
||||
Die [Seite für die Spielereinstellungen dieses Spiels](../player-options) enthält alle Optionen die man benötigt um
|
||||
eine YAML-Datei zu konfigurieren und zu exportieren.
|
||||
@@ -1,16 +0,0 @@
|
||||
# Clique
|
||||
|
||||
## What is this game?
|
||||
|
||||
~~Clique is a psychological survival horror game where a player must survive the temptation to press red buttons.~~
|
||||
|
||||
Clique is a joke game developed for Archipelago in March 2023 to showcase how easy it can be to develop a world for
|
||||
Archipelago. The objective of the game is to press the big red button. If a player is playing on `hard_mode`, they must
|
||||
wait for someone else in the multiworld to "activate" their button before they can press it.
|
||||
|
||||
Clique can be played on most modern HTML5-capable browsers.
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure
|
||||
and export a config file.
|
||||
@@ -1,25 +0,0 @@
|
||||
# Clique Anleitung
|
||||
|
||||
Nachdem dein Seed generiert wurde, gehe auf die Website von [Clique dem Spiel](http://clique.pharware.com/) und gib
|
||||
Server-Daten, deinen Slot-Namen und ein Passwort (falls vorhanden) ein. Klicke dann auf "Connect" (Verbinden).
|
||||
|
||||
Wenn du auf "Einfach" spielst, kannst du unbedenklich den Knopf drücken und deine "Befriedigung" erhalten.
|
||||
|
||||
Wenn du auf "Schwer" spielst, ist es sehr wahrscheinlich, dass du warten musst bevor du dein Ziel erreichen kannst.
|
||||
Glücklicherweise läuft Click auf den meißten großen Browsern, die HTML5 unterstützen. Das heißt du kannst Clique auf
|
||||
deinem Handy starten und produktiv sein während du wartest!
|
||||
|
||||
Falls du einige Ideen brauchst was du tun kannst, während du wartest bis der Knopf aktiviert wurde, versuche
|
||||
(mindestens) eins der Folgenden:
|
||||
|
||||
- Dein Zimmer aufräumen.
|
||||
- Die Wäsche machen.
|
||||
- Etwas Essen von einem X-Belieben Fast Food Restaruant holen.
|
||||
- Das tägliche Wordle machen.
|
||||
- ~~Deine Seele an **Phar** verkaufen.~~
|
||||
- Deine Hausaufgaben erledigen.
|
||||
- Deine Post abholen.
|
||||
|
||||
|
||||
~~Solltest du auf irgendwelche Probleme in diesem Spiel stoßen, solltest du keinesfalls nicht **thephar** auf~~
|
||||
~~Discord kontaktieren. *zwinker* *zwinker*~~
|
||||
@@ -1,22 +0,0 @@
|
||||
# Clique Start Guide
|
||||
|
||||
After rolling your seed, go to the [Clique Game](http://clique.pharware.com/) site and enter the server details, your
|
||||
slot name, and a room password if one is required. Then click "Connect".
|
||||
|
||||
If you're playing on "easy mode", just click the button and receive "Satisfaction".
|
||||
|
||||
If you're playing on "hard mode", you may need to wait for activation before you can complete your objective. Luckily,
|
||||
Clique runs in most major browsers that support HTML5, so you can load Clique on your phone and be productive while
|
||||
you wait!
|
||||
|
||||
If you need some ideas for what to do while waiting for button activation, give the following a try:
|
||||
|
||||
- Clean your room.
|
||||
- Wash the dishes.
|
||||
- Get some food from a non-descript fast food restaurant.
|
||||
- Do the daily Wordle.
|
||||
- ~~Sell your soul to Phar.~~
|
||||
- Do your school work.
|
||||
|
||||
|
||||
~~If you run into any issues with this game, definitely do not contact **thephar** on discord. *wink* *wink*~~
|
||||
@@ -734,8 +734,8 @@ def get_start_inventory_data(world: "CVCotMWorld") -> Tuple[Dict[int, bytes], bo
|
||||
magic_items_array[array_offset] += 1
|
||||
|
||||
# Add the start inventory arrays to the offset data in bytes form.
|
||||
start_inventory_data[0x680080] = bytes(magic_items_array)
|
||||
start_inventory_data[0x6800A0] = bytes(cards_array)
|
||||
start_inventory_data[0x690080] = bytes(magic_items_array)
|
||||
start_inventory_data[0x6900A0] = bytes(cards_array)
|
||||
|
||||
# Add the extra max HP/MP/Hearts to all classes' base stats. Doing it this way makes us less likely to hit the max
|
||||
# possible Max Ups.
|
||||
|
||||
@@ -132,40 +132,40 @@ start_inventory_giver = [
|
||||
|
||||
# Magic Items
|
||||
0x13, 0x48, # ldr r0, =0x202572F
|
||||
0x14, 0x49, # ldr r1, =0x8680080
|
||||
0x14, 0x49, # ldr r1, =0x8690080
|
||||
0x00, 0x22, # mov r2, #0
|
||||
0x8B, 0x5C, # ldrb r3, [r1, r2]
|
||||
0x83, 0x54, # strb r3, [r0, r2]
|
||||
0x01, 0x32, # adds r2, #1
|
||||
0x08, 0x2A, # cmp r2, #8
|
||||
0xFA, 0xDB, # blt 0x8680006
|
||||
0xFA, 0xDB, # blt 0x8690006
|
||||
# Max Ups
|
||||
0x11, 0x48, # ldr r0, =0x202572C
|
||||
0x12, 0x49, # ldr r1, =0x8680090
|
||||
0x12, 0x49, # ldr r1, =0x8690090
|
||||
0x00, 0x22, # mov r2, #0
|
||||
0x8B, 0x5C, # ldrb r3, [r1, r2]
|
||||
0x83, 0x54, # strb r3, [r0, r2]
|
||||
0x01, 0x32, # adds r2, #1
|
||||
0x03, 0x2A, # cmp r2, #3
|
||||
0xFA, 0xDB, # blt 0x8680016
|
||||
0xFA, 0xDB, # blt 0x8690016
|
||||
# Cards
|
||||
0x0F, 0x48, # ldr r0, =0x2025674
|
||||
0x10, 0x49, # ldr r1, =0x86800A0
|
||||
0x10, 0x49, # ldr r1, =0x86900A0
|
||||
0x00, 0x22, # mov r2, #0
|
||||
0x8B, 0x5C, # ldrb r3, [r1, r2]
|
||||
0x83, 0x54, # strb r3, [r0, r2]
|
||||
0x01, 0x32, # adds r2, #1
|
||||
0x14, 0x2A, # cmp r2, #0x14
|
||||
0xFA, 0xDB, # blt 0x8680026
|
||||
0xFA, 0xDB, # blt 0x8690026
|
||||
# Inventory Items (not currently supported)
|
||||
0x0D, 0x48, # ldr r0, =0x20256ED
|
||||
0x0E, 0x49, # ldr r1, =0x86800C0
|
||||
0x0E, 0x49, # ldr r1, =0x86900C0
|
||||
0x00, 0x22, # mov r2, #0
|
||||
0x8B, 0x5C, # ldrb r3, [r1, r2]
|
||||
0x83, 0x54, # strb r3, [r0, r2]
|
||||
0x01, 0x32, # adds r2, #1
|
||||
0x36, 0x2A, # cmp r2, #36
|
||||
0xFA, 0xDB, # blt 0x8680036
|
||||
0xFA, 0xDB, # blt 0x8690036
|
||||
# Return to the function that checks for Magician Mode.
|
||||
0xBA, 0x21, # movs r1, #0xBA
|
||||
0x89, 0x00, # lsls r1, r1, #2
|
||||
@@ -176,13 +176,13 @@ start_inventory_giver = [
|
||||
# LDR number pool
|
||||
0x78, 0x7F, 0x00, 0x08,
|
||||
0x2F, 0x57, 0x02, 0x02,
|
||||
0x80, 0x00, 0x68, 0x08,
|
||||
0x80, 0x00, 0x69, 0x08,
|
||||
0x2C, 0x57, 0x02, 0x02,
|
||||
0x90, 0x00, 0x68, 0x08,
|
||||
0x90, 0x00, 0x69, 0x08,
|
||||
0x74, 0x56, 0x02, 0x02,
|
||||
0xA0, 0x00, 0x68, 0x08,
|
||||
0xA0, 0x00, 0x69, 0x08,
|
||||
0xED, 0x56, 0x02, 0x02,
|
||||
0xC0, 0x00, 0x68, 0x08,
|
||||
0xC0, 0x00, 0x69, 0x08,
|
||||
]
|
||||
|
||||
max_max_up_checker = [
|
||||
|
||||
@@ -335,8 +335,8 @@ class CVCotMPatchExtensions(APPatchExtension):
|
||||
rom_data.write_bytes(0x679A60, patches.kickless_roc_height_shortener)
|
||||
|
||||
# Give the player their Start Inventory upon entering their name on a new file.
|
||||
rom_data.write_bytes(0x7F70, [0x00, 0x48, 0x87, 0x46, 0x00, 0x00, 0x68, 0x08])
|
||||
rom_data.write_bytes(0x680000, patches.start_inventory_giver)
|
||||
rom_data.write_bytes(0x7F70, [0x00, 0x48, 0x87, 0x46, 0x00, 0x00, 0x69, 0x08])
|
||||
rom_data.write_bytes(0x690000, patches.start_inventory_giver)
|
||||
|
||||
# Prevent Max Ups from exceeding 255.
|
||||
rom_data.write_bytes(0x5E170, [0x00, 0x4A, 0x97, 0x46, 0x00, 0x00, 0x6A, 0x08])
|
||||
|
||||
@@ -75,6 +75,13 @@ class DarkSouls3World(World):
|
||||
"""The pool of all items within this particular world. This is a subset of
|
||||
`self.multiworld.itempool`."""
|
||||
|
||||
missable_dupe_prog_locs: Set[str] = {"PC: Storm Ruler - Siegward",
|
||||
"US: Pyromancy Flame - Cornyx",
|
||||
"US: Tower Key - kill Irina"}
|
||||
"""Locations whose vanilla item is a missable duplicate of a non-missable progression item.
|
||||
If vanilla, these locations shouldn't be expected progression, so they aren't created and don't get rules.
|
||||
"""
|
||||
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
super().__init__(multiworld, player)
|
||||
self.all_excluded_locations = set()
|
||||
@@ -258,10 +265,7 @@ class DarkSouls3World(World):
|
||||
new_location.progress_type = LocationProgressType.EXCLUDED
|
||||
else:
|
||||
# Don't allow missable duplicates of progression items to be expected progression.
|
||||
if location.name in {"PC: Storm Ruler - Siegward",
|
||||
"US: Pyromancy Flame - Cornyx",
|
||||
"US: Tower Key - kill Irina"}:
|
||||
continue
|
||||
if location.name in self.missable_dupe_prog_locs: continue
|
||||
|
||||
# Replace non-randomized items with events that give the default item
|
||||
event_item = (
|
||||
@@ -1286,8 +1290,9 @@ class DarkSouls3World(World):
|
||||
data = location_dictionary[location]
|
||||
if data.dlc and not self.options.enable_dlc: continue
|
||||
if data.ngp and not self.options.enable_ngp: continue
|
||||
# Don't add rules to missable duplicates of progression items
|
||||
if location in self.missable_dupe_prog_locs and not self._is_location_available(location): continue
|
||||
|
||||
if not self._is_location_available(location): continue
|
||||
if isinstance(rule, str):
|
||||
assert item_dictionary[rule].classification == ItemClassification.progression
|
||||
rule = lambda state, item=rule: state.has(item, self.player)
|
||||
|
||||
@@ -73,7 +73,7 @@ things to keep in mind:
|
||||
|
||||
* To run the game itself, just run `launchmod_darksouls3.bat` under Proton.
|
||||
|
||||
[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/8.0
|
||||
[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/6.0
|
||||
[WINE]: https://www.winehq.org/
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -30,7 +30,6 @@ class Group(enum.Enum):
|
||||
Deprecated = enum.auto()
|
||||
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ItemData:
|
||||
code_without_offset: offset
|
||||
@@ -98,14 +97,15 @@ def create_trap_items(world, world_options: Options.DLCQuestOptions, trap_needed
|
||||
return traps
|
||||
|
||||
|
||||
def create_items(world, world_options: Options.DLCQuestOptions, locations_count: int, excluded_items: list[str], random: Random):
|
||||
def create_items(world, world_options: Options.DLCQuestOptions, locations_count: int, excluded_items: list[str],
|
||||
random: Random):
|
||||
created_items = []
|
||||
if world_options.campaign == Options.Campaign.option_basic or world_options.campaign == Options.Campaign.option_both:
|
||||
create_items_basic(world_options, created_items, world, excluded_items)
|
||||
create_items_campaign(world_options, created_items, world, excluded_items, Group.DLCQuest, 825, 250)
|
||||
|
||||
if (world_options.campaign == Options.Campaign.option_live_freemium_or_die or
|
||||
world_options.campaign == Options.Campaign.option_both):
|
||||
create_items_lfod(world_options, created_items, world, excluded_items)
|
||||
create_items_campaign(world_options, created_items, world, excluded_items, Group.Freemium, 889, 200)
|
||||
|
||||
trap_items = create_trap_items(world, world_options, locations_count - len(created_items), random)
|
||||
created_items += trap_items
|
||||
@@ -113,27 +113,8 @@ def create_items(world, world_options: Options.DLCQuestOptions, locations_count:
|
||||
return created_items
|
||||
|
||||
|
||||
def create_items_lfod(world_options, created_items, world, excluded_items):
|
||||
for item in items_by_group[Group.Freemium]:
|
||||
if item.name in excluded_items:
|
||||
excluded_items.remove(item)
|
||||
continue
|
||||
|
||||
if item.has_any_group(Group.DLC):
|
||||
created_items.append(world.create_item(item))
|
||||
if item.has_any_group(Group.Item) and world_options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||
created_items.append(world.create_item(item))
|
||||
if item.has_any_group(Group.Twice):
|
||||
created_items.append(world.create_item(item))
|
||||
if world_options.coinsanity == Options.CoinSanity.option_coin:
|
||||
if world_options.coinbundlequantity == -1:
|
||||
create_coin_piece(created_items, world, 889, 200, Group.Freemium)
|
||||
return
|
||||
create_coin(world_options, created_items, world, 889, 200, Group.Freemium)
|
||||
|
||||
|
||||
def create_items_basic(world_options, created_items, world, excluded_items):
|
||||
for item in items_by_group[Group.DLCQuest]:
|
||||
def create_items_campaign(world_options: Options.DLCQuestOptions, created_items: list[DLCQuestItem], world, excluded_items: list[str], group: Group, total_coins: int, required_coins: int):
|
||||
for item in items_by_group[group]:
|
||||
if item.name in excluded_items:
|
||||
excluded_items.remove(item.name)
|
||||
continue
|
||||
@@ -146,14 +127,15 @@ def create_items_basic(world_options, created_items, world, excluded_items):
|
||||
created_items.append(world.create_item(item))
|
||||
if world_options.coinsanity == Options.CoinSanity.option_coin:
|
||||
if world_options.coinbundlequantity == -1:
|
||||
create_coin_piece(created_items, world, 825, 250, Group.DLCQuest)
|
||||
create_coin_piece(created_items, world, total_coins, required_coins, group)
|
||||
return
|
||||
create_coin(world_options, created_items, world, 825, 250, Group.DLCQuest)
|
||||
create_coin(world_options, created_items, world, total_coins, required_coins, group)
|
||||
|
||||
|
||||
def create_coin(world_options, created_items, world, total_coins, required_coins, group):
|
||||
coin_bundle_required = math.ceil(required_coins / world_options.coinbundlequantity)
|
||||
coin_bundle_useful = math.ceil((total_coins - coin_bundle_required * world_options.coinbundlequantity) / world_options.coinbundlequantity)
|
||||
coin_bundle_useful = math.ceil(
|
||||
(total_coins - coin_bundle_required * world_options.coinbundlequantity) / world_options.coinbundlequantity)
|
||||
for item in items_by_group[group]:
|
||||
if item.has_any_group(Group.Coin):
|
||||
for i in range(coin_bundle_required):
|
||||
@@ -165,7 +147,7 @@ def create_coin(world_options, created_items, world, total_coins, required_coins
|
||||
def create_coin_piece(created_items, world, total_coins, required_coins, group):
|
||||
for item in items_by_group[group]:
|
||||
if item.has_any_group(Group.Piece):
|
||||
for i in range(required_coins*10):
|
||||
for i in range(required_coins * 10):
|
||||
created_items.append(world.create_item(item))
|
||||
for i in range((total_coins - required_coins) * 10):
|
||||
created_items.append(world.create_item(item, ItemClassification.useful))
|
||||
|
||||
@@ -321,7 +321,7 @@ class InventorySpillTrapCount(TrapCount):
|
||||
|
||||
class FactorioWorldGen(OptionDict):
|
||||
"""World Generation settings. Overview of options at https://wiki.factorio.com/Map_generator,
|
||||
with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings"""
|
||||
with in-depth documentation at https://lua-api.factorio.com/latest/concepts/MapGenSettings.html"""
|
||||
display_name = "World Generation"
|
||||
# FIXME: do we want default be a rando-optimized default or in-game DS?
|
||||
value: dict[str, dict[str, typing.Any]]
|
||||
|
||||
@@ -288,7 +288,7 @@ world and the beginning of another world. You can also combine multiple files by
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
description: Example of generating multiple worlds. World 1 of 3
|
||||
description: Example of generating multiple worlds. World 1 of 2
|
||||
name: Mario
|
||||
game: Super Mario 64
|
||||
requires:
|
||||
@@ -310,31 +310,6 @@ Super Mario 64:
|
||||
|
||||
---
|
||||
|
||||
description: Example of generating multiple worlds. World 2 of 3
|
||||
name: Minecraft
|
||||
game: Minecraft
|
||||
Minecraft:
|
||||
progression_balancing: 50
|
||||
accessibility: items
|
||||
advancement_goal: 40
|
||||
combat_difficulty: hard
|
||||
include_hard_advancements: false
|
||||
include_unreasonable_advancements: false
|
||||
include_postgame_advancements: false
|
||||
shuffle_structures: true
|
||||
structure_compasses: true
|
||||
send_defeated_mobs: true
|
||||
bee_traps: 15
|
||||
egg_shards_required: 7
|
||||
egg_shards_available: 10
|
||||
required_bosses:
|
||||
none: 0
|
||||
ender_dragon: 1
|
||||
wither: 0
|
||||
both: 0
|
||||
|
||||
---
|
||||
|
||||
description: Example of generating multiple worlds. World 2 of 2
|
||||
name: ExampleFinder
|
||||
game: ChecksFinder
|
||||
@@ -344,6 +319,6 @@ ChecksFinder:
|
||||
accessibility: items
|
||||
```
|
||||
|
||||
The above example will generate 3 worlds - one Super Mario 64, one Minecraft, and one ChecksFinder.
|
||||
The above example will generate 2 worlds - one Super Mario 64 and one ChecksFinder.
|
||||
|
||||
|
||||
|
||||
@@ -27,73 +27,176 @@ requires:
|
||||
plando: bosses, items, texts, connections
|
||||
```
|
||||
|
||||
For a basic understanding of YAML files, refer to
|
||||
[YAML Formatting](/tutorial/Archipelago/advanced_settings/en#yaml-formatting)
|
||||
in Advanced Settings.
|
||||
|
||||
## Item Plando
|
||||
Item plando allows a player to place an item in a specific location or specific locations, or place multiple items into a
|
||||
list of specific locations both in their own game or in another player's game.
|
||||
|
||||
* The options for item plando are `from_pool`, `world`, `percentage`, `force`, `count`, and either `item` and
|
||||
`location`, or `items` and `locations`.
|
||||
* `from_pool` determines if the item should be taken *from* the item pool or *added* to it. This can be true or
|
||||
false and defaults to true if omitted.
|
||||
* `world` is the target world to place the item in.
|
||||
* It gets ignored if only one world is generated.
|
||||
* Can be a number, name, true, false, null, or a list. False is the default.
|
||||
* If a number is used, it targets that slot or player number in the multiworld.
|
||||
* If a name is used, it will target the world with that player name.
|
||||
* If set to true, it will be any player's world besides your own.
|
||||
* If set to false, it will target your own world.
|
||||
* If set to null, it will target a random world in the multiworld.
|
||||
* If a list of names is used, it will target the games with the player names specified.
|
||||
* `force` determines whether the generator will fail if the item can't be placed in the location. Can be true, false,
|
||||
or silent. Silent is the default.
|
||||
* If set to true, the item must be placed and the generator will throw an error if it is unable to do so.
|
||||
* If set to false, the generator will log a warning if the placement can't be done but will still generate.
|
||||
* If set to silent and the placement fails, it will be ignored entirely.
|
||||
* `percentage` is the percentage chance for the relevant block to trigger. This can be any value from 0 to 100 and
|
||||
if omitted will default to 100.
|
||||
* Single Placement is when you use a plando block to place a single item at a single location.
|
||||
* `item` is the item you would like to place and `location` is the location to place it.
|
||||
* Multi Placement uses a plando block to place multiple items in multiple locations until either list is exhausted.
|
||||
* `items` defines the items to use, each with a number for the amount. Using `true` instead of a number uses however many of that item are in your item pool.
|
||||
* `locations` is a list of possible locations those items can be placed in.
|
||||
* Some special location group names can be specified:
|
||||
* `early_locations` will add all sphere 1 locations (locations logically reachable only with your starting inventory)
|
||||
* `non_early_locations` will add all locations beyond sphere 1 (locations that require finding at least one item before they become logically reachable)
|
||||
* Using the multi placement method, placements are picked randomly.
|
||||
Item Plando allows a player to place an item in a specific location or locations, or place multiple items into a list
|
||||
of specific locations in their own game and/or in another player's game.
|
||||
|
||||
* `count` can be used to set the maximum number of items placed from the block. The default is 1 if using `item` and False if using `items`
|
||||
* If a number is used, it will try to place this number of items.
|
||||
* If set to false, it will try to place as many items from the block as it can.
|
||||
* If `min` and `max` are defined, it will try to place a number of items between these two numbers at random.
|
||||
To add item plando to your player yaml, you add them under the `plando_items` block. You should start with `item` if you
|
||||
want to do Single Placement, or `items` if you want to do Multi Placement. A list of items can still be defined under
|
||||
`item` but only one of them will be chosen at random to be used.
|
||||
|
||||
After you define `item/items`, you would define `location` or `locations`, depending on if you want to fill one
|
||||
location or many. Note that both `location` and `locations` are optional. A list of locations can still be defined under
|
||||
`location` but only one of them will be chosen at random to be used.
|
||||
|
||||
You may do any combination of `item/items` and `location/locations` in a plando block, but the block only places items
|
||||
in locations **until the shorter of the two lists is used up.**
|
||||
|
||||
Once you are satisfied with your first block, you may continue to define ones under the same `plando_items` parent.
|
||||
Each block can have several different options to tailor it the way you like.
|
||||
|
||||
* The `items` section defines the items to use. Each item name can be followed by a colon and a value.
|
||||
* A numerical value indicates the amount of that item.
|
||||
* A `true` value uses all copies of that item that are in your item pool.
|
||||
|
||||
* The `item` section defines a list of items to use, from which one will be chosen at random. Each item name can be
|
||||
followed by a colon and a value. The value indicates the weight of that item being chosen.
|
||||
|
||||
* The `locations` section defines possible locations those items can be placed in. Two special location groups exist:
|
||||
* `early_locations` will add all sphere 1 locations (locations logically reachable only with your starting
|
||||
inventory).
|
||||
* `non_early_locations` will add all locations beyond sphere 1 (locations that require finding at least one item
|
||||
before they become logically reachable).
|
||||
|
||||
* `from_pool` determines if the item should be taken *from* the item pool or *created* from scratch.
|
||||
* `false`: Create a new item with the same name (the world will determine its properties e.g. classification).
|
||||
* `true`: Take the existing item, if it exists, from the item pool. If it does not exist, one will be created from
|
||||
scratch. **(Default)**
|
||||
|
||||
* `world` is the target world to place the item in. It gets ignored if only one world is generated.
|
||||
* **A number:** Use this slot or player number in the multiworld.
|
||||
* **A name:** Use the world with that player name.
|
||||
* **A list of names:** Use the worlds with the player names specified.
|
||||
* `true`: Locations will be in any player's world besides your own.
|
||||
* `false`: Locations will be in your own world. **(Default)**
|
||||
* `null`: Locations will be in a random world in the multiworld.
|
||||
|
||||
* `force` determines whether the generator will fail if the plando block cannot be fulfilled.
|
||||
* `true`: The generator will throw an error if it is unable to place an item.
|
||||
* `false`: The generator will log a warning if it is unable to place an item, but it will still generate.
|
||||
* `silent`: If the placement fails, it will be ignored entirely. **(Default)**
|
||||
|
||||
* `percentage` is the percentage chance for the block to trigger. This can be any integer from 0 to 100.
|
||||
**(Default: 100)**
|
||||
|
||||
* `count` sets the number of items placed from the list.
|
||||
* **Default: 1 if using `item` or `location`, and `false` otherwise.**
|
||||
* **A number:** It will place this number of items.
|
||||
* `false`: It will place as many items from the list as it can.
|
||||
* **If `min` is defined,** it will place at least `min` many items (can be combined with `max`).
|
||||
* **If `max` is defined,** it will place at most `max` many items (can be combined with `min`).
|
||||
|
||||
### Available Items and Locations
|
||||
|
||||
A list of all available items and locations can be found in the [website's datapackage](/datapackage). The items and locations will be in the `"item_name_to_id"` and `"location_name_to_id"` sections of the relevant game. You do not need the quotes but the name must be entered in the same as it appears on that page and is case-sensitive.
|
||||
A list of all available items and locations can be found in the [website's datapackage](/datapackage). The items and
|
||||
locations will be in the `"item_name_to_id"` and `"location_name_to_id"` sections of the relevant game. Names are
|
||||
case-sensitive. You can also use item groups and location groups that are defined in the datapackage.
|
||||
|
||||
### Examples
|
||||
## Item Plando Examples
|
||||
```yaml
|
||||
plando_items:
|
||||
# Example block - Pokémon Red and Blue
|
||||
- items:
|
||||
Potion: 3
|
||||
locations:
|
||||
- "Route 1 - Free Sample Man"
|
||||
- "Mt Moon 1F - West Item"
|
||||
- "Mt Moon 1F - South Item"
|
||||
```
|
||||
This block will lock 3 Potion items on the Route 1 Pokémart employee and 2 Mt Moon items. Note these are all
|
||||
Potions in the vanilla game. The world value has not been specified, so these locations must be in this player's own
|
||||
world by default.
|
||||
|
||||
```yaml
|
||||
plando_items:
|
||||
# example block 1 - Timespinner
|
||||
# Example block - A Link to the Past
|
||||
- items:
|
||||
Progressive Sword: 4
|
||||
world:
|
||||
- BobsWitness
|
||||
- BobsRogueLegacy
|
||||
count:
|
||||
min: 1
|
||||
max: 4
|
||||
```
|
||||
This block will attempt to place a random number, between 1 and 4, of Progressive Swords into any locations within the
|
||||
game slots named "BobsWitness" and "BobsRogueLegacy."
|
||||
|
||||
```yaml
|
||||
plando_items:
|
||||
# Example block - Secret of Evermore
|
||||
- items:
|
||||
Levitate: 1
|
||||
Revealer: 1
|
||||
Energize: 1
|
||||
locations:
|
||||
- Master Sword Pedestal
|
||||
- Desert Discard
|
||||
world: true
|
||||
count: 2
|
||||
```
|
||||
This block will choose 2 from the Levitate, Revealer, and Energize items at random and attempt to put them into the
|
||||
locations named "Master Sword Pedestal" and "Desert Discard". Because the world value is `true`, these locations
|
||||
must be in other players' worlds.
|
||||
|
||||
```yaml
|
||||
plando_items:
|
||||
# Example block - Timespinner
|
||||
- item:
|
||||
Empire Orb: 1
|
||||
Radiant Orb: 1
|
||||
Radiant Orb: 3
|
||||
location: Starter Chest 1
|
||||
from_pool: true
|
||||
from_pool: false
|
||||
world: true
|
||||
percentage: 50
|
||||
|
||||
# example block 2 - Ocarina of Time
|
||||
```
|
||||
This block will place a single item, either the Empire Orb or Radiant Orb, on the location "Starter Chest 1". There is
|
||||
a 25% chance it is Empire Orb, and 75% chance it is Radiant Orb (1 to 3 odds). The world value is `true`, so this
|
||||
location must be in another player's world. Because the from_pool value is `false`, a copy of these items is added to
|
||||
these locations, while the originals remain in the item pool to be shuffled. Unlike the previous examples, which will
|
||||
always trigger, this block only has a 50% chance to trigger.
|
||||
|
||||
```yaml
|
||||
plando_items:
|
||||
# Example block - Factorio
|
||||
- items:
|
||||
progressive-electric-energy-distribution: 2
|
||||
electric-energy-accumulators: 1
|
||||
progressive-turret: 2
|
||||
locations:
|
||||
- AP-1-001
|
||||
- AP-1-002
|
||||
- AP-1-003
|
||||
- AP-1-004
|
||||
percentage: 80
|
||||
force: true
|
||||
from_pool: true
|
||||
world: false
|
||||
```
|
||||
This block lists 5 items but only 4 locations, so it will place all but 1 of the items randomly among the locations
|
||||
chosen here. This block has an 80% chance of occurring. Because force is `true`, the Generator will fail if it cannot
|
||||
place one of the selected items (not including the fifth item). From_pool and World have been set to their default
|
||||
values here, but they can be omitted and have the same result: items will be removed from the pool, and the locations
|
||||
are in this player's own world.
|
||||
|
||||
**NOTE:** Factorio's locations are dynamically generated, so the locations listed above may not exist in your game,
|
||||
they are here for demonstration only.
|
||||
|
||||
```yaml
|
||||
plando_items:
|
||||
# Example block - Ocarina of Time
|
||||
- items:
|
||||
Kokiri Sword: 1
|
||||
Biggoron Sword: 1
|
||||
Bow: 1
|
||||
Magic Meter: 1
|
||||
Progressive Strength Upgrade: 3
|
||||
Progressive Hookshot: 2
|
||||
locations:
|
||||
- Deku Tree Slingshot Chest
|
||||
- Dodongos Cavern Bomb Bag Chest
|
||||
- Jabu Jabus Belly Boomerang Chest
|
||||
- Bottom of the Well Lens of Truth Chest
|
||||
@@ -102,53 +205,16 @@ A list of all available items and locations can be found in the [website's datap
|
||||
- Water Temple Longshot Chest
|
||||
- Shadow Temple Hover Boots Chest
|
||||
- Spirit Temple Silver Gauntlets Chest
|
||||
world: false
|
||||
|
||||
# example block 3 - Factorio
|
||||
- items:
|
||||
progressive-electric-energy-distribution: 2
|
||||
electric-energy-accumulators: 1
|
||||
progressive-turret: 2
|
||||
locations:
|
||||
- military
|
||||
- gun-turret
|
||||
- logistic-science-pack
|
||||
- steel-processing
|
||||
percentage: 80
|
||||
force: true
|
||||
|
||||
# example block 4 - Secret of Evermore
|
||||
- items:
|
||||
Levitate: 1
|
||||
Revealer: 1
|
||||
Energize: 1
|
||||
locations:
|
||||
- Master Sword Pedestal
|
||||
- Boss Relic 1
|
||||
world: true
|
||||
count: 2
|
||||
|
||||
# example block 5 - A Link to the Past
|
||||
- items:
|
||||
Progressive Sword: 4
|
||||
world:
|
||||
- BobsSlaytheSpire
|
||||
- BobsRogueLegacy
|
||||
count:
|
||||
min: 1
|
||||
max: 4
|
||||
from_pool: false
|
||||
|
||||
- item: Kokiri Sword
|
||||
location: Deku Tree Slingshot Chest
|
||||
from_pool: false
|
||||
```
|
||||
1. This block has a 50% chance to occur, and if it does, it will place either the Empire Orb or Radiant Orb on another
|
||||
player's Starter Chest 1 and removes the chosen item from the item pool.
|
||||
2. This block will always trigger and will place the player's swords, bow, magic meter, strength upgrades, and hookshots
|
||||
in their own dungeon major item chests.
|
||||
3. This block has an 80% chance of occurring, and when it does, it will place all but 1 of the items randomly among the
|
||||
four locations chosen here.
|
||||
4. This block will always trigger and will attempt to place a random 2 of Levitate, Revealer and Energize into
|
||||
other players' Master Sword Pedestals or Boss Relic 1 locations.
|
||||
5. This block will always trigger and will attempt to place a random number, between 1 and 4, of progressive swords
|
||||
into any locations within the game slots named BobsSlaytheSpire and BobsRogueLegacy.
|
||||
|
||||
The first block will place the player's Biggoron Sword, Bow, Magic Meter, strength upgrades, and hookshots in the
|
||||
dungeon major item chests. Because the from_pool value is `false`, a copy of these items is added to these locations,
|
||||
while the originals remain in the item pool to be shuffled. The second block will place the Kokiri Sword in the Deku
|
||||
Tree Slingshot Chest, again not from the pool.
|
||||
|
||||
## Boss Plando
|
||||
|
||||
@@ -194,7 +260,7 @@ relevant guide: [A Link to the Past Plando Guide](/tutorial/A%20Link%20to%20the%
|
||||
|
||||
## Connection Plando
|
||||
|
||||
This is currently only supported by a few games, including A Link to the Past, Minecraft, and Ocarina of Time. As the way that these games interact with their
|
||||
This is currently only supported by a few games, including A Link to the Past and Ocarina of Time. As the way that these games interact with their
|
||||
connections is different, only the basics are explained here. More specific information for connection plando in A Link to the Past can be found in
|
||||
its [plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en#connections).
|
||||
|
||||
@@ -207,7 +273,6 @@ its [plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en#connections).
|
||||
|
||||
[A Link to the Past connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852)
|
||||
|
||||
[Minecraft connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/data/regions.json#L18****)
|
||||
|
||||
### Examples
|
||||
|
||||
@@ -223,19 +288,10 @@ its [plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en#connections).
|
||||
- entrance: Agahnims Tower
|
||||
exit: Old Man Cave Exit (West)
|
||||
direction: exit
|
||||
|
||||
# example block 2 - Minecraft
|
||||
- entrance: Overworld Structure 1
|
||||
exit: Nether Fortress
|
||||
direction: both
|
||||
- entrance: Overworld Structure 2
|
||||
exit: Village
|
||||
direction: both
|
||||
|
||||
```
|
||||
|
||||
1. These connections are decoupled, so going into the Lake Hylia Cave Shop will take you to the inside of Cave 45, and
|
||||
when you leave the interior, you will exit to the Cave 45 ledge. Going into the Cave 45 entrance will then take you to
|
||||
the Lake Hylia Cave Shop. Walking into the entrance for the Old Man Cave and Agahnim's Tower entrance will both take
|
||||
you to their locations as normal, but leaving Old Man Cave will exit at Agahnim's Tower.
|
||||
2. This will force a Nether fortress and a village to be the Overworld structures for your game. Note that for the
|
||||
Minecraft connection plando to work structure shuffle must be enabled.
|
||||
|
||||
@@ -367,7 +367,7 @@ def find_root_directory(ctx: JakAndDaxterContext):
|
||||
f" Close all launchers, games, clients, and console windows, then restart Archipelago.")
|
||||
|
||||
if not os.path.exists(settings_path):
|
||||
msg = (f"{err_title}: the OpenGOAL settings file does not exist.\n"
|
||||
msg = (f"{err_title}: The OpenGOAL settings file does not exist.\n"
|
||||
f"{alt_instructions}")
|
||||
ctx.on_log_error(logger, msg)
|
||||
return
|
||||
@@ -375,14 +375,44 @@ def find_root_directory(ctx: JakAndDaxterContext):
|
||||
with open(settings_path, "r") as f:
|
||||
load = json.load(f)
|
||||
|
||||
jak1_installed = load["games"]["Jak 1"]["isInstalled"]
|
||||
# This settings file has changed format once before, and may do so again in the future.
|
||||
# Guard against future incompatibilities by checking the file version first, and use that to determine
|
||||
# what JSON keys to look for next.
|
||||
try:
|
||||
settings_version = load["version"]
|
||||
logger.debug(f"OpenGOAL settings file version: {settings_version}")
|
||||
except KeyError:
|
||||
msg = (f"{err_title}: The OpenGOAL settings file has no version number!\n"
|
||||
f"{alt_instructions}")
|
||||
ctx.on_log_error(logger, msg)
|
||||
return
|
||||
|
||||
try:
|
||||
if settings_version == "2.0":
|
||||
jak1_installed = load["games"]["Jak 1"]["isInstalled"]
|
||||
mod_sources = load["games"]["Jak 1"]["modsInstalledVersion"]
|
||||
|
||||
elif settings_version == "3.0":
|
||||
jak1_installed = load["games"]["jak1"]["isInstalled"]
|
||||
mod_sources = load["games"]["jak1"]["mods"]
|
||||
|
||||
else:
|
||||
msg = (f"{err_title}: The OpenGOAL settings file has an unknown version number ({settings_version}).\n"
|
||||
f"{alt_instructions}")
|
||||
ctx.on_log_error(logger, msg)
|
||||
return
|
||||
except KeyError as e:
|
||||
msg = (f"{err_title}: The OpenGOAL settings file does not contain key entry {e}!\n"
|
||||
f"{alt_instructions}")
|
||||
ctx.on_log_error(logger, msg)
|
||||
return
|
||||
|
||||
if not jak1_installed:
|
||||
msg = (f"{err_title}: The OpenGOAL Launcher is missing a normal install of Jak 1!\n"
|
||||
f"{alt_instructions}")
|
||||
ctx.on_log_error(logger, msg)
|
||||
return
|
||||
|
||||
mod_sources = load["games"]["Jak 1"]["modsInstalledVersion"]
|
||||
if mod_sources is None:
|
||||
msg = (f"{err_title}: No mod sources have been configured in the OpenGOAL Launcher!\n"
|
||||
f"{alt_instructions}")
|
||||
|
||||
@@ -90,7 +90,7 @@ def cmd_gift(self: "SNIClientCommandProcessor") -> None:
|
||||
async_start(update_object(self.ctx, f"Giftboxes;{self.ctx.team}", {
|
||||
f"{self.ctx.slot}":
|
||||
{
|
||||
"IsOpen": handler.gifting,
|
||||
"is_open": handler.gifting,
|
||||
**kdl3_gifting_options
|
||||
}
|
||||
}))
|
||||
@@ -175,11 +175,11 @@ class KDL3SNIClient(SNIClient):
|
||||
key, gift = ctx.stored_data[self.giftbox_key].popitem()
|
||||
await pop_object(ctx, self.giftbox_key, key)
|
||||
# first, special cases
|
||||
traits = [trait["Trait"] for trait in gift["Traits"]]
|
||||
traits = [trait["trait"] for trait in gift["traits"]]
|
||||
if "Candy" in traits or "Invincible" in traits:
|
||||
# apply invincibility candy
|
||||
self.item_queue.append(0x43)
|
||||
elif "Tomato" in traits or "tomato" in gift["ItemName"].lower():
|
||||
elif "Tomato" in traits or "tomato" in gift["item_name"].lower():
|
||||
# apply maxim tomato
|
||||
# only want tomatos here, no other vegetable is that good
|
||||
self.item_queue.append(0x42)
|
||||
@@ -187,7 +187,7 @@ class KDL3SNIClient(SNIClient):
|
||||
# Apply 1-Up
|
||||
self.item_queue.append(0x41)
|
||||
elif "Currency" in traits or "Star" in traits:
|
||||
value = gift["ItemValue"]
|
||||
value = gift.get("item_value", 1)
|
||||
if value >= 50000:
|
||||
self.item_queue.append(0x46)
|
||||
elif value >= 30000:
|
||||
@@ -210,8 +210,8 @@ class KDL3SNIClient(SNIClient):
|
||||
# check if it's tasty
|
||||
if any(x in traits for x in ["Consumable", "Food", "Drink", "Heal", "Health"]):
|
||||
# it's tasty!, use quality to decide how much to heal
|
||||
quality = max((trait["Quality"] for trait in gift["Traits"]
|
||||
if trait["Trait"] in ["Consumable", "Food", "Drink", "Heal", "Health"]))
|
||||
quality = max((trait.get("quality", 1.0) for trait in gift["traits"]
|
||||
if trait["trait"] in ["Consumable", "Food", "Drink", "Heal", "Health"]))
|
||||
quality = min(10, quality * 2)
|
||||
else:
|
||||
# it's not really edible, but he'll eat it anyway
|
||||
@@ -236,23 +236,23 @@ class KDL3SNIClient(SNIClient):
|
||||
for slot, info in ctx.stored_data[self.motherbox_key].items():
|
||||
if int(slot) == ctx.slot and len(ctx.stored_data[self.motherbox_key]) > 1:
|
||||
continue
|
||||
desire = len(set(info["DesiredTraits"]).intersection([trait["Trait"] for trait in gift_base["Traits"]]))
|
||||
desire = len(set(info["desired_traits"]).intersection([trait["trait"] for trait in gift_base["traits"]]))
|
||||
if desire > most_applicable:
|
||||
most_applicable = desire
|
||||
most_applicable_slot = int(slot)
|
||||
elif most_applicable_slot != ctx.slot and most_applicable == -1 and info["AcceptsAnyGift"]:
|
||||
elif most_applicable_slot == ctx.slot and most_applicable == -1 and info["accepts_any_gift"]:
|
||||
# only send to ourselves if no one else will take it
|
||||
most_applicable_slot = int(slot)
|
||||
# print(most_applicable, most_applicable_slot)
|
||||
item_uuid = uuid.uuid4().hex
|
||||
item = {
|
||||
**gift_base,
|
||||
"ID": item_uuid,
|
||||
"Sender": ctx.player_names[ctx.slot],
|
||||
"Receiver": ctx.player_names[most_applicable_slot],
|
||||
"SenderTeam": ctx.team,
|
||||
"ReceiverTeam": ctx.team, # for the moment
|
||||
"IsRefund": False
|
||||
"id": item_uuid,
|
||||
"sender_slot": ctx.slot,
|
||||
"receiver_slot": most_applicable_slot,
|
||||
"sender_team": ctx.team,
|
||||
"receiver_team": ctx.team, # for the moment
|
||||
"is_refund": False
|
||||
}
|
||||
# print(item)
|
||||
await update_object(ctx, f"Giftbox;{ctx.team};{most_applicable_slot}", {
|
||||
@@ -276,8 +276,9 @@ class KDL3SNIClient(SNIClient):
|
||||
if not self.initialize_gifting:
|
||||
self.giftbox_key = f"Giftbox;{ctx.team};{ctx.slot}"
|
||||
self.motherbox_key = f"Giftboxes;{ctx.team}"
|
||||
enable_gifting = await snes_read(ctx, KDL3_GIFTING_FLAG, 0x01)
|
||||
await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key, bool(enable_gifting[0]))
|
||||
enable_gifting = await snes_read(ctx, KDL3_GIFTING_FLAG, 0x02)
|
||||
await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key,
|
||||
bool(int.from_bytes(enable_gifting, "little")))
|
||||
self.initialize_gifting = True
|
||||
# can't check debug anymore, without going and copying the value. might be important later.
|
||||
if not self.levels:
|
||||
@@ -350,19 +351,19 @@ class KDL3SNIClient(SNIClient):
|
||||
self.item_queue.append(item_idx | 0x80)
|
||||
|
||||
# handle gifts here
|
||||
gifting_status = await snes_read(ctx, KDL3_GIFTING_FLAG, 0x01)
|
||||
if hasattr(ctx, "gifting") and ctx.gifting:
|
||||
if gifting_status[0]:
|
||||
gifting_status = int.from_bytes(await snes_read(ctx, KDL3_GIFTING_FLAG, 0x02), "little")
|
||||
if hasattr(self, "gifting") and self.gifting:
|
||||
if gifting_status:
|
||||
gift = await snes_read(ctx, KDL3_GIFTING_SEND, 0x01)
|
||||
if gift[0]:
|
||||
# we have a gift to send
|
||||
await self.pick_gift_recipient(ctx, gift[0])
|
||||
snes_buffered_write(ctx, KDL3_GIFTING_SEND, bytes([0x00]))
|
||||
else:
|
||||
snes_buffered_write(ctx, KDL3_GIFTING_FLAG, bytes([0x01]))
|
||||
snes_buffered_write(ctx, KDL3_GIFTING_FLAG, bytes([0x01, 0x00]))
|
||||
else:
|
||||
if gifting_status[0]:
|
||||
snes_buffered_write(ctx, KDL3_GIFTING_FLAG, bytes([0x00]))
|
||||
if gifting_status:
|
||||
snes_buffered_write(ctx, KDL3_GIFTING_FLAG, bytes([0x00, 0x00]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
|
||||
@@ -37,157 +37,158 @@ async def initialize_giftboxes(ctx: "SNIContext", giftbox_key: str, motherbox_ke
|
||||
ctx.set_notify(motherbox_key, giftbox_key)
|
||||
await update_object(ctx, f"Giftboxes;{ctx.team}", {f"{ctx.slot}":
|
||||
{
|
||||
"IsOpen": is_open,
|
||||
"is_open": is_open,
|
||||
**kdl3_gifting_options
|
||||
}})
|
||||
await update_object(ctx, f"Giftbox;{ctx.team};{ctx.slot}", {})
|
||||
ctx.client_handler.gifting = is_open
|
||||
|
||||
|
||||
kdl3_gifting_options = {
|
||||
"AcceptsAnyGift": True,
|
||||
"DesiredTraits": [
|
||||
"accepts_any_gift": True,
|
||||
"desired_traits": [
|
||||
"Consumable", "Food", "Drink", "Candy", "Tomato",
|
||||
"Invincible", "Life", "Heal", "Health", "Trap",
|
||||
"Goo", "Gel", "Slow", "Slowness", "Eject", "Removal"
|
||||
],
|
||||
"MinimumGiftVersion": 2,
|
||||
"minimum_gift_version": 3,
|
||||
}
|
||||
|
||||
kdl3_gifts = {
|
||||
1: {
|
||||
"ItemName": "1-Up",
|
||||
"Amount": 1,
|
||||
"ItemValue": 400000,
|
||||
"Traits": [
|
||||
"item_name": "1-Up",
|
||||
"amount": 1,
|
||||
"item_value": 400000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Consumable",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Consumable",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Life",
|
||||
"Quality": 1,
|
||||
"Duration": 1
|
||||
"trait": "Life",
|
||||
"quality": 1,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
2: {
|
||||
"ItemName": "Maxim Tomato",
|
||||
"Amount": 1,
|
||||
"ItemValue": 500000,
|
||||
"Traits": [
|
||||
"item_name": "Maxim Tomato",
|
||||
"amount": 1,
|
||||
"item_value": 500000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Consumable",
|
||||
"Quality": 5,
|
||||
"Duration": 1,
|
||||
"trait": "Consumable",
|
||||
"quality": 5,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Heal",
|
||||
"Quality": 5,
|
||||
"Duration": 1,
|
||||
"trait": "Heal",
|
||||
"quality": 5,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Food",
|
||||
"Quality": 5,
|
||||
"Duration": 1,
|
||||
"trait": "Food",
|
||||
"quality": 5,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Tomato",
|
||||
"Quality": 5,
|
||||
"Duration": 1,
|
||||
"trait": "Tomato",
|
||||
"quality": 5,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Vegetable",
|
||||
"Quality": 5,
|
||||
"Duration": 1,
|
||||
"trait": "Vegetable",
|
||||
"quality": 5,
|
||||
"duration": 1,
|
||||
}
|
||||
]
|
||||
},
|
||||
3: {
|
||||
"ItemName": "Energy Drink",
|
||||
"Amount": 1,
|
||||
"ItemValue": 100000,
|
||||
"Traits": [
|
||||
"item_name": "Energy Drink",
|
||||
"amount": 1,
|
||||
"item_value": 100000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Consumable",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Consumable",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Heal",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Heal",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Drink",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Drink",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
]
|
||||
},
|
||||
5: {
|
||||
"ItemName": "Small Star Piece",
|
||||
"Amount": 1,
|
||||
"ItemValue": 10000,
|
||||
"Traits": [
|
||||
"item_name": "Small Star Piece",
|
||||
"amount": 1,
|
||||
"item_value": 10000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Currency",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Currency",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Money",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Money",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Star",
|
||||
"Quality": 1,
|
||||
"Duration": 1
|
||||
"trait": "Star",
|
||||
"quality": 1,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
6: {
|
||||
"ItemName": "Medium Star Piece",
|
||||
"Amount": 1,
|
||||
"ItemValue": 30000,
|
||||
"Traits": [
|
||||
"item_name": "Medium Star Piece",
|
||||
"amount": 1,
|
||||
"item_value": 30000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Currency",
|
||||
"Quality": 3,
|
||||
"Duration": 1,
|
||||
"trait": "Currency",
|
||||
"quality": 3,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Money",
|
||||
"Quality": 3,
|
||||
"Duration": 1,
|
||||
"trait": "Money",
|
||||
"quality": 3,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Star",
|
||||
"Quality": 3,
|
||||
"Duration": 1
|
||||
"trait": "Star",
|
||||
"quality": 3,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
7: {
|
||||
"ItemName": "Large Star Piece",
|
||||
"Amount": 1,
|
||||
"ItemValue": 50000,
|
||||
"Traits": [
|
||||
"item_name": "Large Star Piece",
|
||||
"amount": 1,
|
||||
"item_value": 50000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Currency",
|
||||
"Quality": 5,
|
||||
"Duration": 1,
|
||||
"trait": "Currency",
|
||||
"quality": 5,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Money",
|
||||
"Quality": 5,
|
||||
"Duration": 1,
|
||||
"trait": "Money",
|
||||
"quality": 5,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Star",
|
||||
"Quality": 5,
|
||||
"Duration": 1
|
||||
"trait": "Star",
|
||||
"quality": 5,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -195,90 +196,90 @@ kdl3_gifts = {
|
||||
|
||||
kdl3_trap_gifts = {
|
||||
0: {
|
||||
"ItemName": "Gooey Bag",
|
||||
"Amount": 1,
|
||||
"ItemValue": 10000,
|
||||
"Traits": [
|
||||
"item_name": "Gooey Bag",
|
||||
"amount": 1,
|
||||
"item_value": 10000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Trap",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Trap",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Goo",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Goo",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Gel",
|
||||
"Quality": 1,
|
||||
"Duration": 1
|
||||
"trait": "Gel",
|
||||
"quality": 1,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
1: {
|
||||
"ItemName": "Slowness",
|
||||
"Amount": 1,
|
||||
"ItemValue": 10000,
|
||||
"Traits": [
|
||||
"item_name": "Slowness",
|
||||
"amount": 1,
|
||||
"item_value": 10000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Trap",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Trap",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Slow",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Slow",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Slowness",
|
||||
"Quality": 1,
|
||||
"Duration": 1
|
||||
"trait": "Slowness",
|
||||
"quality": 1,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
2: {
|
||||
"ItemName": "Eject Ability",
|
||||
"Amount": 1,
|
||||
"ItemValue": 10000,
|
||||
"Traits": [
|
||||
"item_name": "Eject Ability",
|
||||
"amount": 1,
|
||||
"item_value": 10000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Trap",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Trap",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Eject",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Eject",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Removal",
|
||||
"Quality": 1,
|
||||
"Duration": 1
|
||||
"trait": "Removal",
|
||||
"quality": 1,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
3: {
|
||||
"ItemName": "Bad Meal",
|
||||
"Amount": 1,
|
||||
"ItemValue": 10000,
|
||||
"Traits": [
|
||||
"item_name": "Bad Meal",
|
||||
"amount": 1,
|
||||
"item_value": 10000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Trap",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Trap",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Damage",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Damage",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Food",
|
||||
"Quality": 1,
|
||||
"Duration": 1
|
||||
"trait": "Food",
|
||||
"quality": 1,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -289,7 +289,7 @@ class KirbyFlavorPreset(Choice):
|
||||
option_lime = 12
|
||||
option_lavender = 13
|
||||
option_miku = 14
|
||||
option_custom = 15
|
||||
option_custom = -1
|
||||
default = 0
|
||||
|
||||
@classmethod
|
||||
@@ -297,7 +297,7 @@ class KirbyFlavorPreset(Choice):
|
||||
text = text.lower()
|
||||
if text == "random":
|
||||
choice_list = list(cls.name_lookup)
|
||||
choice_list.remove(14)
|
||||
choice_list.remove(-1)
|
||||
return cls(random.choice(choice_list))
|
||||
return super().from_text(text)
|
||||
|
||||
@@ -347,7 +347,7 @@ class GooeyFlavorPreset(Choice):
|
||||
option_orange = 11
|
||||
option_lime = 12
|
||||
option_lavender = 13
|
||||
option_custom = 14
|
||||
option_custom = -1
|
||||
default = 0
|
||||
|
||||
@classmethod
|
||||
@@ -355,7 +355,7 @@ class GooeyFlavorPreset(Choice):
|
||||
text = text.lower()
|
||||
if text == "random":
|
||||
choice_list = list(cls.name_lookup)
|
||||
choice_list.remove(14)
|
||||
choice_list.remove(-1)
|
||||
return cls(random.choice(choice_list))
|
||||
return super().from_text(text)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import hashlib
|
||||
import os
|
||||
import struct
|
||||
|
||||
import settings
|
||||
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension
|
||||
from .aesthetics import get_palette_bytes, kirby_target_palettes, get_kirby_palette, gooey_target_palettes, \
|
||||
get_gooey_palette
|
||||
@@ -475,8 +474,7 @@ def patch_rom(world: "KDL3World", patch: KDL3ProcedurePatch) -> None:
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D016, world.options.ow_boss_requirement.value.to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D018, world.options.consumables.value.to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D01A, world.options.starsanity.value.to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D01C, world.options.gifting.value.to_bytes(2, "little")
|
||||
if world.multiworld.players > 1 else bytes([0, 0]))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D01C, world.options.gifting.value.to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D01E, world.options.strict_bosses.value.to_bytes(2, "little"))
|
||||
# don't write gifting for solo game, since there's no one to send anything to
|
||||
|
||||
@@ -594,9 +592,9 @@ def get_base_rom_bytes() -> bytes:
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options: settings.Settings = settings.get_settings()
|
||||
from . import KDL3World
|
||||
if not file_name:
|
||||
file_name = options["kdl3_options"]["rom_file"]
|
||||
file_name = KDL3World.settings.rom_file
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
|
||||
@@ -220,7 +220,6 @@ To this day I still don't know if we inconvenienced the Mad Batter or not.
|
||||
Oh, hi #####
|
||||
People forgot I was playable in Hyrule Warriors
|
||||
Join our Discord. Or else.
|
||||
Also try Minecraft!
|
||||
I see you're finally awake...
|
||||
OwO
|
||||
This is Todd Howard, and today I'm pleased to announce... The Elder Scrolls V: Skyrim for the Nintendo Game Boy Color!
|
||||
@@ -256,7 +255,6 @@ Try Bumper Stickers!
|
||||
Try Castlevania 64!
|
||||
Try Celeste 64!
|
||||
Try ChecksFinder!
|
||||
Try Clique!
|
||||
Try Dark Souls III!
|
||||
Try DLCQuest!
|
||||
Try Donkey Kong Country 3!
|
||||
@@ -269,6 +267,7 @@ Try A Hat in Time!
|
||||
Try Heretic!
|
||||
Try Hollow Knight!
|
||||
Try Hylics 2!
|
||||
Try Jak and Daxter: The Precursor Legacy!
|
||||
Try Kingdom Hearts 2!
|
||||
Try Kirby's Dream Land 3!
|
||||
Try Landstalker - The Treasures of King Nole!
|
||||
@@ -281,7 +280,6 @@ Try Mario & Luigi Superstar Saga!
|
||||
Try MegaMan Battle Network 3!
|
||||
Try Meritous!
|
||||
Try The Messenger!
|
||||
Try Minecraft!
|
||||
Try Muse Dash!
|
||||
Try Noita!
|
||||
Try Ocarina of Time!
|
||||
@@ -290,11 +288,10 @@ Try Pokemon Emerald!
|
||||
Try Pokemon Red and Blue!
|
||||
Try Raft!
|
||||
Try Risk of Rain 2!
|
||||
Try Rogue Legacy!
|
||||
Try Secret of Evermore!
|
||||
Try shapez!
|
||||
Try Shivers!
|
||||
Try A Short Hike!
|
||||
Try Slay the Spire!
|
||||
Try SMZ3!
|
||||
Try Sonic Adventure 2 Battle!
|
||||
Try Starcraft 2!
|
||||
@@ -302,6 +299,7 @@ Try Stardew Valley!
|
||||
Try Subnautica!
|
||||
Try Sudoku!
|
||||
Try Super Mario 64!
|
||||
Try Super Mario Land 2: 6 Golden Coins!
|
||||
Try Super Mario World!
|
||||
Try Super Metroid!
|
||||
Try Terraria!
|
||||
@@ -314,7 +312,6 @@ Try The Witness!
|
||||
Try Yoshi's Island!
|
||||
Try Yu-Gi-Oh! 2006!
|
||||
Try Zillion!
|
||||
Try Zork Grand Inquisitor!
|
||||
Try Old School Runescape!
|
||||
Try Kingdom Hearts!
|
||||
Try Mega Man 2!
|
||||
@@ -371,7 +368,6 @@ Have they added Among Us to AP yet?
|
||||
Every copy of LADX is personalized, David.
|
||||
Looks like you're going on A Short Hike. Bring back feathers please?
|
||||
Functioning Brain is at...\nWait. This isn't Witness. Wrong game, sorry.
|
||||
Don't forget to check your Clique!\nIf, y'know, you have one. No pressure...
|
||||
:3
|
||||
Sorry ######, but your progression item is in another world.
|
||||
&newgames\n&oldgames
|
||||
|
||||
@@ -335,7 +335,9 @@ class LinksAwakeningWorld(World):
|
||||
start_item = next((item for item in start_items if opens_new_regions(item)), None)
|
||||
|
||||
if start_item:
|
||||
itempool.remove(start_item)
|
||||
# Make sure we're removing the same copy of the item that we're placing
|
||||
# (.remove checks __eq__, which could be a different copy, so we find the first index and use .pop)
|
||||
start_item = itempool.pop(itempool.index(start_item))
|
||||
start_loc.place_locked_item(start_item)
|
||||
else:
|
||||
logging.getLogger("Link's Awakening Logger").warning(f"No {self.options.tarins_gift.current_option_name} available for Tarin's Gift.")
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import os
|
||||
import json
|
||||
import pkgutil
|
||||
|
||||
def load_data_file(*args) -> dict:
|
||||
fname = "/".join(["data", *args])
|
||||
return json.loads(pkgutil.get_data(__name__, fname).decode())
|
||||
|
||||
# For historical reasons, these values are different.
|
||||
# They remain different to ensure datapackage consistency.
|
||||
# Do not separate other games' location and item IDs like this.
|
||||
item_id_offset: int = 45000
|
||||
location_id_offset: int = 42000
|
||||
|
||||
item_info = load_data_file("items.json")
|
||||
item_name_to_id = {name: item_id_offset + index \
|
||||
for index, name in enumerate(item_info["all_items"])}
|
||||
item_name_to_id["Bee Trap"] = item_id_offset + 100 # historical reasons
|
||||
|
||||
location_info = load_data_file("locations.json")
|
||||
location_name_to_id = {name: location_id_offset + index \
|
||||
for index, name in enumerate(location_info["all_locations"])}
|
||||
|
||||
exclusion_info = load_data_file("excluded_locations.json")
|
||||
|
||||
region_info = load_data_file("regions.json")
|
||||
@@ -1,55 +0,0 @@
|
||||
from math import ceil
|
||||
from typing import List
|
||||
|
||||
from BaseClasses import Item
|
||||
|
||||
from . import Constants
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MinecraftWorld
|
||||
|
||||
|
||||
def get_junk_item_names(rand, k: int) -> str:
|
||||
junk_weights = Constants.item_info["junk_weights"]
|
||||
junk = rand.choices(
|
||||
list(junk_weights.keys()),
|
||||
weights=list(junk_weights.values()),
|
||||
k=k)
|
||||
return junk
|
||||
|
||||
def build_item_pool(world: "MinecraftWorld") -> List[Item]:
|
||||
multiworld = world.multiworld
|
||||
player = world.player
|
||||
|
||||
itempool = []
|
||||
total_location_count = len(multiworld.get_unfilled_locations(player))
|
||||
|
||||
required_pool = Constants.item_info["required_pool"]
|
||||
|
||||
# Add required progression items
|
||||
for item_name, num in required_pool.items():
|
||||
itempool += [world.create_item(item_name) for _ in range(num)]
|
||||
|
||||
# Add structure compasses
|
||||
if world.options.structure_compasses:
|
||||
compasses = [name for name in world.item_name_to_id if "Structure Compass" in name]
|
||||
for item_name in compasses:
|
||||
itempool.append(world.create_item(item_name))
|
||||
|
||||
# Dragon egg shards
|
||||
if world.options.egg_shards_required > 0:
|
||||
num = world.options.egg_shards_available
|
||||
itempool += [world.create_item("Dragon Egg Shard") for _ in range(num)]
|
||||
|
||||
# Bee traps
|
||||
bee_trap_percentage = world.options.bee_traps * 0.01
|
||||
if bee_trap_percentage > 0:
|
||||
bee_trap_qty = ceil(bee_trap_percentage * (total_location_count - len(itempool)))
|
||||
itempool += [world.create_item("Bee Trap") for _ in range(bee_trap_qty)]
|
||||
|
||||
# Fill remaining itempool with randomly generated junk
|
||||
junk = get_junk_item_names(world.random, total_location_count - len(itempool))
|
||||
itempool += [world.create_item(name) for name in junk]
|
||||
|
||||
return itempool
|
||||
@@ -1,143 +0,0 @@
|
||||
from Options import Choice, Toggle, DefaultOnToggle, Range, OptionList, DeathLink, PlandoConnections, \
|
||||
PerGameCommonOptions
|
||||
from .Constants import region_info
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class AdvancementGoal(Range):
|
||||
"""Number of advancements required to spawn bosses."""
|
||||
display_name = "Advancement Goal"
|
||||
range_start = 0
|
||||
range_end = 114
|
||||
default = 40
|
||||
|
||||
|
||||
class EggShardsRequired(Range):
|
||||
"""Number of dragon egg shards to collect to spawn bosses."""
|
||||
display_name = "Egg Shards Required"
|
||||
range_start = 0
|
||||
range_end = 50
|
||||
default = 0
|
||||
|
||||
|
||||
class EggShardsAvailable(Range):
|
||||
"""Number of dragon egg shards available to collect."""
|
||||
display_name = "Egg Shards Available"
|
||||
range_start = 0
|
||||
range_end = 50
|
||||
default = 0
|
||||
|
||||
|
||||
class BossGoal(Choice):
|
||||
"""Bosses which must be defeated to finish the game."""
|
||||
display_name = "Required Bosses"
|
||||
option_none = 0
|
||||
option_ender_dragon = 1
|
||||
option_wither = 2
|
||||
option_both = 3
|
||||
default = 1
|
||||
|
||||
@property
|
||||
def dragon(self):
|
||||
return self.value % 2 == 1
|
||||
|
||||
@property
|
||||
def wither(self):
|
||||
return self.value > 1
|
||||
|
||||
|
||||
class ShuffleStructures(DefaultOnToggle):
|
||||
"""Enables shuffling of villages, outposts, fortresses, bastions, and end cities."""
|
||||
display_name = "Shuffle Structures"
|
||||
|
||||
|
||||
class StructureCompasses(DefaultOnToggle):
|
||||
"""Adds structure compasses to the item pool, which point to the nearest indicated structure."""
|
||||
display_name = "Structure Compasses"
|
||||
|
||||
|
||||
class BeeTraps(Range):
|
||||
"""Replaces a percentage of junk items with bee traps, which spawn multiple angered bees around every player when
|
||||
received."""
|
||||
display_name = "Bee Trap Percentage"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 0
|
||||
|
||||
|
||||
class CombatDifficulty(Choice):
|
||||
"""Modifies the level of items logically required for exploring dangerous areas and fighting bosses."""
|
||||
display_name = "Combat Difficulty"
|
||||
option_easy = 0
|
||||
option_normal = 1
|
||||
option_hard = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class HardAdvancements(Toggle):
|
||||
"""Enables certain RNG-reliant or tedious advancements."""
|
||||
display_name = "Include Hard Advancements"
|
||||
|
||||
|
||||
class UnreasonableAdvancements(Toggle):
|
||||
"""Enables the extremely difficult advancements "How Did We Get Here?" and "Adventuring Time.\""""
|
||||
display_name = "Include Unreasonable Advancements"
|
||||
|
||||
|
||||
class PostgameAdvancements(Toggle):
|
||||
"""Enables advancements that require spawning and defeating the required bosses."""
|
||||
display_name = "Include Postgame Advancements"
|
||||
|
||||
|
||||
class SendDefeatedMobs(Toggle):
|
||||
"""Send killed mobs to other Minecraft worlds which have this option enabled."""
|
||||
display_name = "Send Defeated Mobs"
|
||||
|
||||
|
||||
class StartingItems(OptionList):
|
||||
"""Start with these items. Each entry should be of this format: {item: "item_name", amount: #}
|
||||
`item` can include components, and should be in an identical format to a `/give` command with
|
||||
`"` escaped for json reasons.
|
||||
|
||||
`amount` is optional and will default to 1 if omitted.
|
||||
|
||||
example:
|
||||
```
|
||||
starting_items: [
|
||||
{ "item": "minecraft:stick[minecraft:custom_name=\"{'text':'pointy stick'}\"]" },
|
||||
{ "item": "minecraft:arrow[minecraft:rarity=epic]", amount: 64 }
|
||||
]
|
||||
```
|
||||
"""
|
||||
display_name = "Starting Items"
|
||||
|
||||
|
||||
class MCPlandoConnections(PlandoConnections):
|
||||
entrances = set(connection[0] for connection in region_info["default_connections"])
|
||||
exits = set(connection[1] for connection in region_info["default_connections"])
|
||||
|
||||
@classmethod
|
||||
def can_connect(cls, entrance, exit):
|
||||
if exit in region_info["illegal_connections"] and entrance in region_info["illegal_connections"][exit]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@dataclass
|
||||
class MinecraftOptions(PerGameCommonOptions):
|
||||
plando_connections: MCPlandoConnections
|
||||
advancement_goal: AdvancementGoal
|
||||
egg_shards_required: EggShardsRequired
|
||||
egg_shards_available: EggShardsAvailable
|
||||
required_bosses: BossGoal
|
||||
shuffle_structures: ShuffleStructures
|
||||
structure_compasses: StructureCompasses
|
||||
|
||||
combat_difficulty: CombatDifficulty
|
||||
include_hard_advancements: HardAdvancements
|
||||
include_unreasonable_advancements: UnreasonableAdvancements
|
||||
include_postgame_advancements: PostgameAdvancements
|
||||
bee_traps: BeeTraps
|
||||
send_defeated_mobs: SendDefeatedMobs
|
||||
death_link: DeathLink
|
||||
starting_items: StartingItems
|
||||
@@ -1,508 +0,0 @@
|
||||
from BaseClasses import CollectionState
|
||||
from worlds.generic.Rules import exclusion_rules
|
||||
|
||||
from . import Constants
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MinecraftWorld
|
||||
|
||||
|
||||
# Helper functions
|
||||
# moved from logicmixin
|
||||
|
||||
def has_iron_ingots(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player)
|
||||
|
||||
|
||||
def has_copper_ingots(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player)
|
||||
|
||||
|
||||
def has_gold_ingots(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return (state.has('Progressive Resource Crafting', player)
|
||||
and (
|
||||
state.has('Progressive Tools', player, 2)
|
||||
or state.can_reach_region('The Nether', player)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def has_diamond_pickaxe(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Progressive Tools', player, 3) and has_iron_ingots(world, state, player)
|
||||
|
||||
|
||||
def craft_crossbow(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Archery', player) and has_iron_ingots(world, state, player)
|
||||
|
||||
|
||||
def has_bottle(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Bottles', player) and state.has('Progressive Resource Crafting', player)
|
||||
|
||||
|
||||
def has_spyglass(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return (has_copper_ingots(world, state, player)
|
||||
and state.has('Spyglass', player)
|
||||
and can_adventure(world, state, player)
|
||||
)
|
||||
|
||||
|
||||
def can_enchant(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Enchanting', player) and has_diamond_pickaxe(world, state, player) # mine obsidian and lapis
|
||||
|
||||
|
||||
def can_use_anvil(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return (state.has('Enchanting', player)
|
||||
and state.has('Progressive Resource Crafting', player,2)
|
||||
and has_iron_ingots(world, state, player)
|
||||
)
|
||||
|
||||
|
||||
def fortress_loot(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: # saddles, blaze rods, wither skulls
|
||||
return state.can_reach_region('Nether Fortress', player) and basic_combat(world, state, player)
|
||||
|
||||
|
||||
def can_brew_potions(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Blaze Rods', player) and state.has('Brewing', player) and has_bottle(world, state, player)
|
||||
|
||||
|
||||
def can_piglin_trade(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return (has_gold_ingots(world, state, player)
|
||||
and (
|
||||
state.can_reach_region('The Nether', player)
|
||||
or state.can_reach_region('Bastion Remnant', player)
|
||||
))
|
||||
|
||||
|
||||
def overworld_villager(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
village_region = state.multiworld.get_region('Village', player).entrances[0].parent_region.name
|
||||
if village_region == 'The Nether': # 2 options: cure zombie villager or build portal in village
|
||||
return (state.can_reach_location('Zombie Doctor', player)
|
||||
or (
|
||||
has_diamond_pickaxe(world, state, player)
|
||||
and state.can_reach_region('Village', player)
|
||||
))
|
||||
elif village_region == 'The End':
|
||||
return state.can_reach_location('Zombie Doctor', player)
|
||||
return state.can_reach_region('Village', player)
|
||||
|
||||
|
||||
def enter_stronghold(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Blaze Rods', player) and state.has('Brewing', player) and state.has('3 Ender Pearls', player)
|
||||
|
||||
|
||||
# Difficulty-dependent functions
|
||||
def combat_difficulty(world: "MinecraftWorld", state: CollectionState, player: int) -> str:
|
||||
return world.options.combat_difficulty.current_key
|
||||
|
||||
|
||||
def can_adventure(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
death_link_check = not world.options.death_link or state.has('Bed', player)
|
||||
if combat_difficulty(world, state, player) == 'easy':
|
||||
return state.has('Progressive Weapons', player, 2) and has_iron_ingots(world, state, player) and death_link_check
|
||||
elif combat_difficulty(world, state, player) == 'hard':
|
||||
return True
|
||||
return (state.has('Progressive Weapons', player) and death_link_check and
|
||||
(state.has('Progressive Resource Crafting', player) or state.has('Campfire', player)))
|
||||
|
||||
|
||||
def basic_combat(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
if combat_difficulty(world, state, player) == 'easy':
|
||||
return (state.has('Progressive Weapons', player, 2)
|
||||
and state.has('Progressive Armor', player)
|
||||
and state.has('Shield', player)
|
||||
and has_iron_ingots(world, state, player)
|
||||
)
|
||||
elif combat_difficulty(world, state, player) == 'hard':
|
||||
return True
|
||||
return (state.has('Progressive Weapons', player)
|
||||
and (
|
||||
state.has('Progressive Armor', player)
|
||||
or state.has('Shield', player)
|
||||
)
|
||||
and has_iron_ingots(world, state, player)
|
||||
)
|
||||
|
||||
|
||||
def complete_raid(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
reach_regions = (state.can_reach_region('Village', player)
|
||||
and state.can_reach_region('Pillager Outpost', player))
|
||||
if combat_difficulty(world, state, player) == 'easy':
|
||||
return (reach_regions
|
||||
and state.has('Progressive Weapons', player, 3)
|
||||
and state.has('Progressive Armor', player, 2)
|
||||
and state.has('Shield', player)
|
||||
and state.has('Archery', player)
|
||||
and state.has('Progressive Tools', player, 2)
|
||||
and has_iron_ingots(world, state, player)
|
||||
)
|
||||
elif combat_difficulty(world, state, player) == 'hard': # might be too hard?
|
||||
return (reach_regions
|
||||
and state.has('Progressive Weapons', player, 2)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and (
|
||||
state.has('Progressive Armor', player)
|
||||
or state.has('Shield', player)
|
||||
)
|
||||
)
|
||||
return (reach_regions
|
||||
and state.has('Progressive Weapons', player, 2)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and state.has('Progressive Armor', player)
|
||||
and state.has('Shield', player)
|
||||
)
|
||||
|
||||
|
||||
def can_kill_wither(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
normal_kill = (state.has("Progressive Weapons", player, 3)
|
||||
and state.has("Progressive Armor", player, 2)
|
||||
and can_brew_potions(world, state, player)
|
||||
and can_enchant(world, state, player)
|
||||
)
|
||||
if combat_difficulty(world, state, player) == 'easy':
|
||||
return (fortress_loot(world, state, player)
|
||||
and normal_kill
|
||||
and state.has('Archery', player)
|
||||
)
|
||||
elif combat_difficulty(world, state, player) == 'hard': # cheese kill using bedrock ceilings
|
||||
return (fortress_loot(world, state, player)
|
||||
and (
|
||||
normal_kill
|
||||
or state.can_reach_region('The Nether', player)
|
||||
or state.can_reach_region('The End', player)
|
||||
)
|
||||
)
|
||||
|
||||
return fortress_loot(world, state, player) and normal_kill
|
||||
|
||||
|
||||
def can_respawn_ender_dragon(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return (state.can_reach_region('The Nether', player)
|
||||
and state.can_reach_region('The End', player)
|
||||
and state.has('Progressive Resource Crafting', player) # smelt sand into glass
|
||||
)
|
||||
|
||||
|
||||
def can_kill_ender_dragon(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
if combat_difficulty(world, state, player) == 'easy':
|
||||
return (state.has("Progressive Weapons", player, 3)
|
||||
and state.has("Progressive Armor", player, 2)
|
||||
and state.has('Archery', player)
|
||||
and can_brew_potions(world, state, player)
|
||||
and can_enchant(world, state, player)
|
||||
)
|
||||
if combat_difficulty(world, state, player) == 'hard':
|
||||
return (
|
||||
(
|
||||
state.has('Progressive Weapons', player, 2)
|
||||
and state.has('Progressive Armor', player)
|
||||
) or (
|
||||
state.has('Progressive Weapons', player, 1)
|
||||
and state.has('Bed', player) # who needs armor when you can respawn right outside the chamber
|
||||
)
|
||||
)
|
||||
return (state.has('Progressive Weapons', player, 2)
|
||||
and state.has('Progressive Armor', player)
|
||||
and state.has('Archery', player)
|
||||
)
|
||||
|
||||
|
||||
def has_structure_compass(world: "MinecraftWorld", state: CollectionState, entrance_name: str, player: int) -> bool:
|
||||
if not world.options.structure_compasses:
|
||||
return True
|
||||
return state.has(f"Structure Compass ({state.multiworld.get_entrance(entrance_name, player).connected_region.name})", player)
|
||||
|
||||
|
||||
def get_rules_lookup(world, player: int):
|
||||
rules_lookup = {
|
||||
"entrances": {
|
||||
"Nether Portal": lambda state: state.has('Flint and Steel', player)
|
||||
and (
|
||||
state.has('Bucket', player)
|
||||
or state.has('Progressive Tools', player, 3)
|
||||
)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"End Portal": lambda state: enter_stronghold(world, state, player)
|
||||
and state.has('3 Ender Pearls', player, 4),
|
||||
"Overworld Structure 1": lambda state: can_adventure(world, state, player)
|
||||
and has_structure_compass(world, state, "Overworld Structure 1", player),
|
||||
"Overworld Structure 2": lambda state: can_adventure(world, state, player)
|
||||
and has_structure_compass(world, state, "Overworld Structure 2", player),
|
||||
"Nether Structure 1": lambda state: can_adventure(world, state, player)
|
||||
and has_structure_compass(world, state, "Nether Structure 1", player),
|
||||
"Nether Structure 2": lambda state: can_adventure(world, state, player)
|
||||
and has_structure_compass(world, state, "Nether Structure 2", player),
|
||||
"The End Structure": lambda state: can_adventure(world, state, player)
|
||||
and has_structure_compass(world, state, "The End Structure", player),
|
||||
},
|
||||
"locations": {
|
||||
"Ender Dragon": lambda state: can_respawn_ender_dragon(world, state, player)
|
||||
and can_kill_ender_dragon(world, state, player),
|
||||
"Wither": lambda state: can_kill_wither(world, state, player),
|
||||
"Blaze Rods": lambda state: fortress_loot(world, state, player),
|
||||
"Who is Cutting Onions?": lambda state: can_piglin_trade(world, state, player),
|
||||
"Oh Shiny": lambda state: can_piglin_trade(world, state, player),
|
||||
"Suit Up": lambda state: state.has("Progressive Armor", player)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Very Very Frightening": lambda state: state.has("Channeling Book", player)
|
||||
and can_use_anvil(world, state, player)
|
||||
and can_enchant(world, state, player)
|
||||
and overworld_villager(world, state, player),
|
||||
"Hot Stuff": lambda state: state.has("Bucket", player)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Free the End": lambda state: can_respawn_ender_dragon(world, state, player)
|
||||
and can_kill_ender_dragon(world, state, player),
|
||||
"A Furious Cocktail": lambda state: (can_brew_potions(world, state, player)
|
||||
and state.has("Fishing Rod", player) # Water Breathing
|
||||
and state.can_reach_region("The Nether", player) # Regeneration, Fire Resistance, gold nuggets
|
||||
and state.can_reach_region("Village", player) # Night Vision, Invisibility
|
||||
and state.can_reach_location("Bring Home the Beacon", player)),
|
||||
# Resistance
|
||||
"Bring Home the Beacon": lambda state: can_kill_wither(world, state, player)
|
||||
and has_diamond_pickaxe(world, state, player)
|
||||
and state.has("Progressive Resource Crafting", player, 2),
|
||||
"Not Today, Thank You": lambda state: state.has("Shield", player)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Isn't It Iron Pick": lambda state: state.has("Progressive Tools", player, 2)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Local Brewery": lambda state: can_brew_potions(world, state, player),
|
||||
"The Next Generation": lambda state: can_respawn_ender_dragon(world, state, player)
|
||||
and can_kill_ender_dragon(world, state, player),
|
||||
"Fishy Business": lambda state: state.has("Fishing Rod", player),
|
||||
"This Boat Has Legs": lambda state: (
|
||||
fortress_loot(world, state, player)
|
||||
or complete_raid(world, state, player)
|
||||
)
|
||||
and state.has("Saddle", player)
|
||||
and state.has("Fishing Rod", player),
|
||||
"Sniper Duel": lambda state: state.has("Archery", player),
|
||||
"Great View From Up Here": lambda state: basic_combat(world, state, player),
|
||||
"How Did We Get Here?": lambda state: (can_brew_potions(world, state, player)
|
||||
and has_gold_ingots(world, state, player) # Absorption
|
||||
and state.can_reach_region('End City', player) # Levitation
|
||||
and state.can_reach_region('The Nether', player) # potion ingredients
|
||||
and state.has("Fishing Rod", player) # Pufferfish, Nautilus Shells; spectral arrows
|
||||
and state.has("Archery", player)
|
||||
and state.can_reach_location("Bring Home the Beacon", player) # Haste
|
||||
and state.can_reach_location("Hero of the Village", player)), # Bad Omen, Hero of the Village
|
||||
"Bullseye": lambda state: state.has("Archery", player)
|
||||
and state.has("Progressive Tools", player, 2)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Spooky Scary Skeleton": lambda state: basic_combat(world, state, player),
|
||||
"Two by Two": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has("Bucket", player)
|
||||
and can_adventure(world, state, player),
|
||||
"Two Birds, One Arrow": lambda state: craft_crossbow(world, state, player)
|
||||
and can_enchant(world, state, player),
|
||||
"Who's the Pillager Now?": lambda state: craft_crossbow(world, state, player),
|
||||
"Getting an Upgrade": lambda state: state.has("Progressive Tools", player),
|
||||
"Tactical Fishing": lambda state: state.has("Bucket", player)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Zombie Doctor": lambda state: can_brew_potions(world, state, player)
|
||||
and has_gold_ingots(world, state, player),
|
||||
"Ice Bucket Challenge": lambda state: has_diamond_pickaxe(world, state, player),
|
||||
"Into Fire": lambda state: basic_combat(world, state, player),
|
||||
"War Pigs": lambda state: basic_combat(world, state, player),
|
||||
"Take Aim": lambda state: state.has("Archery", player),
|
||||
"Total Beelocation": lambda state: state.has("Silk Touch Book", player)
|
||||
and can_use_anvil(world, state, player)
|
||||
and can_enchant(world, state, player),
|
||||
"Arbalistic": lambda state: (craft_crossbow(world, state, player)
|
||||
and state.has("Piercing IV Book", player)
|
||||
and can_use_anvil(world, state, player)
|
||||
and can_enchant(world, state, player)
|
||||
),
|
||||
"The End... Again...": lambda state: can_respawn_ender_dragon(world, state, player)
|
||||
and can_kill_ender_dragon(world, state, player),
|
||||
"Acquire Hardware": lambda state: has_iron_ingots(world, state, player),
|
||||
"Not Quite \"Nine\" Lives": lambda state: can_piglin_trade(world, state, player)
|
||||
and state.has("Progressive Resource Crafting", player, 2),
|
||||
"Cover Me With Diamonds": lambda state: state.has("Progressive Armor", player, 2)
|
||||
and state.has("Progressive Tools", player, 2)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Sky's the Limit": lambda state: basic_combat(world, state, player),
|
||||
"Hired Help": lambda state: state.has("Progressive Resource Crafting", player, 2)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Sweet Dreams": lambda state: state.has("Bed", player)
|
||||
or state.can_reach_region('Village', player),
|
||||
"You Need a Mint": lambda state: can_respawn_ender_dragon(world, state, player)
|
||||
and has_bottle(world, state, player),
|
||||
"Monsters Hunted": lambda state: (can_respawn_ender_dragon(world, state, player)
|
||||
and can_kill_ender_dragon(world, state, player)
|
||||
and can_kill_wither(world, state, player)
|
||||
and state.has("Fishing Rod", player)),
|
||||
"Enchanter": lambda state: can_enchant(world, state, player),
|
||||
"Voluntary Exile": lambda state: basic_combat(world, state, player),
|
||||
"Eye Spy": lambda state: enter_stronghold(world, state, player),
|
||||
"Serious Dedication": lambda state: (can_brew_potions(world, state, player)
|
||||
and state.has("Bed", player)
|
||||
and has_diamond_pickaxe(world, state, player)
|
||||
and has_gold_ingots(world, state, player)),
|
||||
"Postmortal": lambda state: complete_raid(world, state, player),
|
||||
"Adventuring Time": lambda state: can_adventure(world, state, player),
|
||||
"Hero of the Village": lambda state: complete_raid(world, state, player),
|
||||
"Hidden in the Depths": lambda state: can_brew_potions(world, state, player)
|
||||
and state.has("Bed", player)
|
||||
and has_diamond_pickaxe(world, state, player),
|
||||
"Beaconator": lambda state: (can_kill_wither(world, state, player)
|
||||
and has_diamond_pickaxe(world, state, player)
|
||||
and state.has("Progressive Resource Crafting", player, 2)),
|
||||
"Withering Heights": lambda state: can_kill_wither(world, state, player),
|
||||
"A Balanced Diet": lambda state: (has_bottle(world, state, player)
|
||||
and has_gold_ingots(world, state, player)
|
||||
and state.has("Progressive Resource Crafting", player, 2)
|
||||
and state.can_reach_region('The End', player)),
|
||||
# notch apple, chorus fruit
|
||||
"Subspace Bubble": lambda state: has_diamond_pickaxe(world, state, player),
|
||||
"Country Lode, Take Me Home": lambda state: state.can_reach_location("Hidden in the Depths", player)
|
||||
and has_gold_ingots(world, state, player),
|
||||
"Bee Our Guest": lambda state: state.has("Campfire", player)
|
||||
and has_bottle(world, state, player),
|
||||
"Uneasy Alliance": lambda state: has_diamond_pickaxe(world, state, player)
|
||||
and state.has('Fishing Rod', player),
|
||||
"Diamonds!": lambda state: state.has("Progressive Tools", player, 2)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"A Throwaway Joke": lambda state: can_adventure(world, state, player),
|
||||
"Sticky Situation": lambda state: state.has("Campfire", player)
|
||||
and has_bottle(world, state, player),
|
||||
"Ol' Betsy": lambda state: craft_crossbow(world, state, player),
|
||||
"Cover Me in Debris": lambda state: state.has("Progressive Armor", player, 2)
|
||||
and state.has("8 Netherite Scrap", player, 2)
|
||||
and state.has("Progressive Resource Crafting", player)
|
||||
and has_diamond_pickaxe(world, state, player)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and can_brew_potions(world, state, player)
|
||||
and state.has("Bed", player),
|
||||
"Hot Topic": lambda state: state.has("Progressive Resource Crafting", player),
|
||||
"The Lie": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has("Bucket", player),
|
||||
"On a Rail": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Progressive Tools', player, 2),
|
||||
"When Pigs Fly": lambda state: (
|
||||
fortress_loot(world, state, player)
|
||||
or complete_raid(world, state, player)
|
||||
)
|
||||
and state.has("Saddle", player)
|
||||
and state.has("Fishing Rod", player)
|
||||
and can_adventure(world, state, player),
|
||||
"Overkill": lambda state: can_brew_potions(world, state, player)
|
||||
and (
|
||||
state.has("Progressive Weapons", player)
|
||||
or state.can_reach_region('The Nether', player)
|
||||
),
|
||||
"Librarian": lambda state: state.has("Enchanting", player),
|
||||
"Overpowered": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Progressive Tools', player, 2)
|
||||
and basic_combat(world, state, player),
|
||||
"Wax On": lambda state: has_copper_ingots(world, state, player)
|
||||
and state.has('Campfire', player)
|
||||
and state.has('Progressive Resource Crafting', player, 2),
|
||||
"Wax Off": lambda state: has_copper_ingots(world, state, player)
|
||||
and state.has('Campfire', player)
|
||||
and state.has('Progressive Resource Crafting', player, 2),
|
||||
"The Cutest Predator": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Bucket', player),
|
||||
"The Healing Power of Friendship": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Bucket', player),
|
||||
"Is It a Bird?": lambda state: has_spyglass(world, state, player)
|
||||
and can_adventure(world, state, player),
|
||||
"Is It a Balloon?": lambda state: has_spyglass(world, state, player),
|
||||
"Is It a Plane?": lambda state: has_spyglass(world, state, player)
|
||||
and can_respawn_ender_dragon(world, state, player),
|
||||
"Surge Protector": lambda state: state.has("Channeling Book", player)
|
||||
and can_use_anvil(world, state, player)
|
||||
and can_enchant(world, state, player)
|
||||
and overworld_villager(world, state, player),
|
||||
"Light as a Rabbit": lambda state: can_adventure(world, state, player)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and state.has('Bucket', player),
|
||||
"Glow and Behold!": lambda state: can_adventure(world, state, player),
|
||||
"Whatever Floats Your Goat!": lambda state: can_adventure(world, state, player),
|
||||
"Caves & Cliffs": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Bucket', player)
|
||||
and state.has('Progressive Tools', player, 2),
|
||||
"Feels like home": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Bucket', player)
|
||||
and state.has('Fishing Rod', player)
|
||||
and (
|
||||
fortress_loot(world, state, player)
|
||||
or complete_raid(world, state, player)
|
||||
)
|
||||
and state.has("Saddle", player),
|
||||
"Sound of Music": lambda state: state.has("Progressive Tools", player, 2)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and basic_combat(world, state, player),
|
||||
"Star Trader": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Bucket', player)
|
||||
and (
|
||||
state.can_reach_region("The Nether", player) # soul sand in nether
|
||||
or state.can_reach_region("Nether Fortress", player) # soul sand in fortress if not in nether for water elevator
|
||||
or can_piglin_trade(world, state, player) # piglins give soul sand
|
||||
)
|
||||
and overworld_villager(world, state, player),
|
||||
"Birthday Song": lambda state: state.can_reach_location("The Lie", player)
|
||||
and state.has("Progressive Tools", player, 2)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Bukkit Bukkit": lambda state: state.has("Bucket", player)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and can_adventure(world, state, player),
|
||||
"It Spreads": lambda state: can_adventure(world, state, player)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and state.has("Progressive Tools", player, 2),
|
||||
"Sneak 100": lambda state: can_adventure(world, state, player)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and state.has("Progressive Tools", player, 2),
|
||||
"When the Squad Hops into Town": lambda state: can_adventure(world, state, player)
|
||||
and state.has("Lead", player),
|
||||
"With Our Powers Combined!": lambda state: can_adventure(world, state, player)
|
||||
and state.has("Lead", player),
|
||||
}
|
||||
}
|
||||
return rules_lookup
|
||||
|
||||
|
||||
def set_rules(self: "MinecraftWorld") -> None:
|
||||
multiworld = self.multiworld
|
||||
player = self.player
|
||||
|
||||
rules_lookup = get_rules_lookup(self, player)
|
||||
|
||||
# Set entrance rules
|
||||
for entrance_name, rule in rules_lookup["entrances"].items():
|
||||
multiworld.get_entrance(entrance_name, player).access_rule = rule
|
||||
|
||||
# Set location rules
|
||||
for location_name, rule in rules_lookup["locations"].items():
|
||||
multiworld.get_location(location_name, player).access_rule = rule
|
||||
|
||||
# Set rules surrounding completion
|
||||
bosses = self.options.required_bosses
|
||||
postgame_advancements = set()
|
||||
if bosses.dragon:
|
||||
postgame_advancements.update(Constants.exclusion_info["ender_dragon"])
|
||||
if bosses.wither:
|
||||
postgame_advancements.update(Constants.exclusion_info["wither"])
|
||||
|
||||
def location_count(state: CollectionState) -> int:
|
||||
return len([location for location in multiworld.get_locations(player) if
|
||||
location.address is not None and
|
||||
location.can_reach(state)])
|
||||
|
||||
def defeated_bosses(state: CollectionState) -> bool:
|
||||
return ((not bosses.dragon or state.has("Ender Dragon", player))
|
||||
and (not bosses.wither or state.has("Wither", player)))
|
||||
|
||||
egg_shards = min(self.options.egg_shards_required.value, self.options.egg_shards_available.value)
|
||||
completion_requirements = lambda state: (location_count(state) >= self.options.advancement_goal
|
||||
and state.has("Dragon Egg Shard", player, egg_shards))
|
||||
multiworld.completion_condition[player] = lambda state: completion_requirements(state) and defeated_bosses(state)
|
||||
|
||||
# Set exclusions on hard/unreasonable/postgame
|
||||
excluded_advancements = set()
|
||||
if not self.options.include_hard_advancements:
|
||||
excluded_advancements.update(Constants.exclusion_info["hard"])
|
||||
if not self.options.include_unreasonable_advancements:
|
||||
excluded_advancements.update(Constants.exclusion_info["unreasonable"])
|
||||
if not self.options.include_postgame_advancements:
|
||||
excluded_advancements.update(postgame_advancements)
|
||||
exclusion_rules(multiworld, player, excluded_advancements)
|
||||
@@ -1,59 +0,0 @@
|
||||
from . import Constants
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from . import MinecraftWorld
|
||||
|
||||
|
||||
def shuffle_structures(self: "MinecraftWorld") -> None:
|
||||
multiworld = self.multiworld
|
||||
player = self.player
|
||||
|
||||
default_connections = Constants.region_info["default_connections"]
|
||||
illegal_connections = Constants.region_info["illegal_connections"]
|
||||
|
||||
# Get all unpaired exits and all regions without entrances (except the Menu)
|
||||
# This function is destructive on these lists.
|
||||
exits = [exit.name for r in multiworld.regions if r.player == player for exit in r.exits if exit.connected_region is None]
|
||||
structs = [r.name for r in multiworld.regions if r.player == player and r.entrances == [] and r.name != 'Menu']
|
||||
exits_spoiler = exits[:] # copy the original order for the spoiler log
|
||||
|
||||
pairs = {}
|
||||
|
||||
def set_pair(exit, struct):
|
||||
if (exit in exits) and (struct in structs) and (exit not in illegal_connections.get(struct, [])):
|
||||
pairs[exit] = struct
|
||||
exits.remove(exit)
|
||||
structs.remove(struct)
|
||||
else:
|
||||
raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({multiworld.player_name[player]})")
|
||||
|
||||
# Connect plando structures first
|
||||
if self.options.plando_connections:
|
||||
for conn in self.options.plando_connections:
|
||||
set_pair(conn.entrance, conn.exit)
|
||||
|
||||
# The algorithm tries to place the most restrictive structures first. This algorithm always works on the
|
||||
# relatively small set of restrictions here, but does not work on all possible inputs with valid configurations.
|
||||
if self.options.shuffle_structures:
|
||||
structs.sort(reverse=True, key=lambda s: len(illegal_connections.get(s, [])))
|
||||
for struct in structs[:]:
|
||||
try:
|
||||
exit = self.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])])
|
||||
except IndexError:
|
||||
raise Exception(f"No valid structure placements remaining for player {player} ({self.player_name})")
|
||||
set_pair(exit, struct)
|
||||
else: # write remaining default connections
|
||||
for (exit, struct) in default_connections:
|
||||
if exit in exits:
|
||||
set_pair(exit, struct)
|
||||
|
||||
# Make sure we actually paired everything; might fail if plando
|
||||
try:
|
||||
assert len(exits) == len(structs) == 0
|
||||
except AssertionError:
|
||||
raise Exception(f"Failed to connect all Minecraft structures for player {player} ({self.player_name})")
|
||||
|
||||
for exit in exits_spoiler:
|
||||
multiworld.get_entrance(exit, player).connect(multiworld.get_region(pairs[exit], player))
|
||||
if self.options.shuffle_structures or self.options.plando_connections:
|
||||
multiworld.spoiler.set_entrance(exit, pairs[exit], 'entrance', player)
|
||||
@@ -1,203 +0,0 @@
|
||||
import os
|
||||
import json
|
||||
import settings
|
||||
import typing
|
||||
from base64 import b64encode, b64decode
|
||||
from typing import Dict, Any
|
||||
|
||||
from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification, Location
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
|
||||
from . import Constants
|
||||
from .Options import MinecraftOptions
|
||||
from .Structures import shuffle_structures
|
||||
from .ItemPool import build_item_pool, get_junk_item_names
|
||||
from .Rules import set_rules
|
||||
|
||||
client_version = 9
|
||||
|
||||
|
||||
class MinecraftSettings(settings.Group):
|
||||
class ForgeDirectory(settings.OptionalUserFolderPath):
|
||||
pass
|
||||
|
||||
class ReleaseChannel(str):
|
||||
"""
|
||||
release channel, currently "release", or "beta"
|
||||
any games played on the "beta" channel have a high likelihood of no longer working on the "release" channel.
|
||||
"""
|
||||
|
||||
class JavaExecutable(settings.OptionalUserFilePath):
|
||||
"""
|
||||
Path to Java executable. If not set, will attempt to fall back to Java system installation.
|
||||
"""
|
||||
|
||||
forge_directory: ForgeDirectory = ForgeDirectory("Minecraft NeoForge server")
|
||||
max_heap_size: str = "2G"
|
||||
release_channel: ReleaseChannel = ReleaseChannel("release")
|
||||
java: JavaExecutable = JavaExecutable("")
|
||||
|
||||
|
||||
class MinecraftWebWorld(WebWorld):
|
||||
theme = "jungle"
|
||||
bug_report_page = "https://github.com/KonoTyran/Minecraft_AP_Randomizer/issues/new?assignees=&labels=bug&template=bug_report.yaml&title=%5BBug%5D%3A+Brief+Description+of+bug+here"
|
||||
|
||||
setup = Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Archipelago Minecraft software on your computer. This guide covers"
|
||||
"single-player, multiworld, and related software.",
|
||||
"English",
|
||||
"minecraft_en.md",
|
||||
"minecraft/en",
|
||||
["Kono Tyran"]
|
||||
)
|
||||
|
||||
setup_es = Tutorial(
|
||||
setup.tutorial_name,
|
||||
setup.description,
|
||||
"Español",
|
||||
"minecraft_es.md",
|
||||
"minecraft/es",
|
||||
["Edos"]
|
||||
)
|
||||
|
||||
setup_sv = Tutorial(
|
||||
setup.tutorial_name,
|
||||
setup.description,
|
||||
"Swedish",
|
||||
"minecraft_sv.md",
|
||||
"minecraft/sv",
|
||||
["Albinum"]
|
||||
)
|
||||
|
||||
setup_fr = Tutorial(
|
||||
setup.tutorial_name,
|
||||
setup.description,
|
||||
"Français",
|
||||
"minecraft_fr.md",
|
||||
"minecraft/fr",
|
||||
["TheLynk"]
|
||||
)
|
||||
|
||||
tutorials = [setup, setup_es, setup_sv, setup_fr]
|
||||
|
||||
|
||||
class MinecraftWorld(World):
|
||||
"""
|
||||
Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine,
|
||||
craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient
|
||||
structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim
|
||||
victory!
|
||||
"""
|
||||
game = "Minecraft"
|
||||
options_dataclass = MinecraftOptions
|
||||
options: MinecraftOptions
|
||||
settings: typing.ClassVar[MinecraftSettings]
|
||||
topology_present = True
|
||||
web = MinecraftWebWorld()
|
||||
|
||||
item_name_to_id = Constants.item_name_to_id
|
||||
location_name_to_id = Constants.location_name_to_id
|
||||
|
||||
def _get_mc_data(self) -> Dict[str, Any]:
|
||||
exits = [connection[0] for connection in Constants.region_info["default_connections"]]
|
||||
return {
|
||||
'world_seed': self.random.getrandbits(32),
|
||||
'seed_name': self.multiworld.seed_name,
|
||||
'player_name': self.player_name,
|
||||
'player_id': self.player,
|
||||
'client_version': client_version,
|
||||
'structures': {exit: self.multiworld.get_entrance(exit, self.player).connected_region.name for exit in exits},
|
||||
'advancement_goal': self.options.advancement_goal.value,
|
||||
'egg_shards_required': min(self.options.egg_shards_required.value,
|
||||
self.options.egg_shards_available.value),
|
||||
'egg_shards_available': self.options.egg_shards_available.value,
|
||||
'required_bosses': self.options.required_bosses.current_key,
|
||||
'MC35': bool(self.options.send_defeated_mobs.value),
|
||||
'death_link': bool(self.options.death_link.value),
|
||||
'starting_items': json.dumps(self.options.starting_items.value),
|
||||
'race': self.multiworld.is_race,
|
||||
}
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
item_class = ItemClassification.filler
|
||||
if name in Constants.item_info["progression_items"]:
|
||||
item_class = ItemClassification.progression
|
||||
elif name in Constants.item_info["useful_items"]:
|
||||
item_class = ItemClassification.useful
|
||||
elif name in Constants.item_info["trap_items"]:
|
||||
item_class = ItemClassification.trap
|
||||
|
||||
return MinecraftItem(name, item_class, self.item_name_to_id.get(name, None), self.player)
|
||||
|
||||
def create_event(self, region_name: str, event_name: str) -> None:
|
||||
region = self.multiworld.get_region(region_name, self.player)
|
||||
loc = MinecraftLocation(self.player, event_name, None, region)
|
||||
loc.place_locked_item(self.create_event_item(event_name))
|
||||
region.locations.append(loc)
|
||||
|
||||
def create_event_item(self, name: str) -> Item:
|
||||
item = self.create_item(name)
|
||||
item.classification = ItemClassification.progression
|
||||
return item
|
||||
|
||||
def create_regions(self) -> None:
|
||||
# Create regions
|
||||
for region_name, exits in Constants.region_info["regions"]:
|
||||
r = Region(region_name, self.player, self.multiworld)
|
||||
for exit_name in exits:
|
||||
r.exits.append(Entrance(self.player, exit_name, r))
|
||||
self.multiworld.regions.append(r)
|
||||
|
||||
# Bind mandatory connections
|
||||
for entr_name, region_name in Constants.region_info["mandatory_connections"]:
|
||||
e = self.multiworld.get_entrance(entr_name, self.player)
|
||||
r = self.multiworld.get_region(region_name, self.player)
|
||||
e.connect(r)
|
||||
|
||||
# Add locations
|
||||
for region_name, locations in Constants.location_info["locations_by_region"].items():
|
||||
region = self.multiworld.get_region(region_name, self.player)
|
||||
for loc_name in locations:
|
||||
loc = MinecraftLocation(self.player, loc_name,
|
||||
self.location_name_to_id.get(loc_name, None), region)
|
||||
region.locations.append(loc)
|
||||
|
||||
# Add events
|
||||
self.create_event("Nether Fortress", "Blaze Rods")
|
||||
self.create_event("The End", "Ender Dragon")
|
||||
self.create_event("Nether Fortress", "Wither")
|
||||
|
||||
# Shuffle the connections
|
||||
shuffle_structures(self)
|
||||
|
||||
def create_items(self) -> None:
|
||||
self.multiworld.itempool += build_item_pool(self)
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
data = self._get_mc_data()
|
||||
filename = f"{self.multiworld.get_out_file_name_base(self.player)}.apmc"
|
||||
with open(os.path.join(output_directory, filename), 'wb') as f:
|
||||
f.write(b64encode(bytes(json.dumps(data), 'utf-8')))
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
return self._get_mc_data()
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return get_junk_item_names(self.random, 1)[0]
|
||||
|
||||
|
||||
class MinecraftLocation(Location):
|
||||
game = "Minecraft"
|
||||
|
||||
class MinecraftItem(Item):
|
||||
game = "Minecraft"
|
||||
|
||||
|
||||
def mc_update_output(raw_data, server, port):
|
||||
data = json.loads(b64decode(raw_data))
|
||||
data['server'] = server
|
||||
data['port'] = port
|
||||
return b64encode(bytes(json.dumps(data), 'utf-8'))
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"hard": [
|
||||
"Very Very Frightening",
|
||||
"A Furious Cocktail",
|
||||
"Two by Two",
|
||||
"Two Birds, One Arrow",
|
||||
"Arbalistic",
|
||||
"Monsters Hunted",
|
||||
"Beaconator",
|
||||
"A Balanced Diet",
|
||||
"Uneasy Alliance",
|
||||
"Cover Me in Debris",
|
||||
"A Complete Catalogue",
|
||||
"Surge Protector",
|
||||
"Sound of Music",
|
||||
"Star Trader",
|
||||
"When the Squad Hops into Town",
|
||||
"With Our Powers Combined!"
|
||||
],
|
||||
"unreasonable": [
|
||||
"How Did We Get Here?",
|
||||
"Adventuring Time"
|
||||
],
|
||||
"ender_dragon": [
|
||||
"Free the End",
|
||||
"The Next Generation",
|
||||
"The End... Again...",
|
||||
"You Need a Mint",
|
||||
"Monsters Hunted",
|
||||
"Is It a Plane?"
|
||||
],
|
||||
"wither": [
|
||||
"Withering Heights",
|
||||
"Bring Home the Beacon",
|
||||
"Beaconator",
|
||||
"A Furious Cocktail",
|
||||
"How Did We Get Here?",
|
||||
"Monsters Hunted"
|
||||
]
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
{
|
||||
"all_items": [
|
||||
"Archery",
|
||||
"Progressive Resource Crafting",
|
||||
"Resource Blocks",
|
||||
"Brewing",
|
||||
"Enchanting",
|
||||
"Bucket",
|
||||
"Flint and Steel",
|
||||
"Bed",
|
||||
"Bottles",
|
||||
"Shield",
|
||||
"Fishing Rod",
|
||||
"Campfire",
|
||||
"Progressive Weapons",
|
||||
"Progressive Tools",
|
||||
"Progressive Armor",
|
||||
"8 Netherite Scrap",
|
||||
"8 Emeralds",
|
||||
"4 Emeralds",
|
||||
"Channeling Book",
|
||||
"Silk Touch Book",
|
||||
"Sharpness III Book",
|
||||
"Piercing IV Book",
|
||||
"Looting III Book",
|
||||
"Infinity Book",
|
||||
"4 Diamond Ore",
|
||||
"16 Iron Ore",
|
||||
"500 XP",
|
||||
"100 XP",
|
||||
"50 XP",
|
||||
"3 Ender Pearls",
|
||||
"4 Lapis Lazuli",
|
||||
"16 Porkchops",
|
||||
"8 Gold Ore",
|
||||
"Rotten Flesh",
|
||||
"Single Arrow",
|
||||
"32 Arrows",
|
||||
"Saddle",
|
||||
"Structure Compass (Village)",
|
||||
"Structure Compass (Pillager Outpost)",
|
||||
"Structure Compass (Nether Fortress)",
|
||||
"Structure Compass (Bastion Remnant)",
|
||||
"Structure Compass (End City)",
|
||||
"Shulker Box",
|
||||
"Dragon Egg Shard",
|
||||
"Spyglass",
|
||||
"Lead",
|
||||
"Bee Trap"
|
||||
],
|
||||
"progression_items": [
|
||||
"Archery",
|
||||
"Progressive Resource Crafting",
|
||||
"Resource Blocks",
|
||||
"Brewing",
|
||||
"Enchanting",
|
||||
"Bucket",
|
||||
"Flint and Steel",
|
||||
"Bed",
|
||||
"Bottles",
|
||||
"Shield",
|
||||
"Fishing Rod",
|
||||
"Campfire",
|
||||
"Progressive Weapons",
|
||||
"Progressive Tools",
|
||||
"Progressive Armor",
|
||||
"8 Netherite Scrap",
|
||||
"Channeling Book",
|
||||
"Silk Touch Book",
|
||||
"Piercing IV Book",
|
||||
"3 Ender Pearls",
|
||||
"Saddle",
|
||||
"Structure Compass (Village)",
|
||||
"Structure Compass (Pillager Outpost)",
|
||||
"Structure Compass (Nether Fortress)",
|
||||
"Structure Compass (Bastion Remnant)",
|
||||
"Structure Compass (End City)",
|
||||
"Dragon Egg Shard",
|
||||
"Spyglass",
|
||||
"Lead"
|
||||
],
|
||||
"useful_items": [
|
||||
"Sharpness III Book",
|
||||
"Looting III Book",
|
||||
"Infinity Book"
|
||||
],
|
||||
"trap_items": [
|
||||
"Bee Trap"
|
||||
],
|
||||
|
||||
"required_pool": {
|
||||
"Archery": 1,
|
||||
"Progressive Resource Crafting": 2,
|
||||
"Brewing": 1,
|
||||
"Enchanting": 1,
|
||||
"Bucket": 1,
|
||||
"Flint and Steel": 1,
|
||||
"Bed": 1,
|
||||
"Bottles": 1,
|
||||
"Shield": 1,
|
||||
"Fishing Rod": 1,
|
||||
"Campfire": 1,
|
||||
"Progressive Weapons": 3,
|
||||
"Progressive Tools": 3,
|
||||
"Progressive Armor": 2,
|
||||
"8 Netherite Scrap": 2,
|
||||
"Channeling Book": 1,
|
||||
"Silk Touch Book": 1,
|
||||
"Sharpness III Book": 1,
|
||||
"Piercing IV Book": 1,
|
||||
"Looting III Book": 1,
|
||||
"Infinity Book": 1,
|
||||
"3 Ender Pearls": 4,
|
||||
"Saddle": 1,
|
||||
"Spyglass": 1,
|
||||
"Lead": 1
|
||||
},
|
||||
"junk_weights": {
|
||||
"4 Emeralds": 2,
|
||||
"4 Diamond Ore": 1,
|
||||
"16 Iron Ore": 1,
|
||||
"50 XP": 4,
|
||||
"16 Porkchops": 2,
|
||||
"8 Gold Ore": 1,
|
||||
"Rotten Flesh": 1,
|
||||
"32 Arrows": 1
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
{
|
||||
"all_locations": [
|
||||
"Who is Cutting Onions?",
|
||||
"Oh Shiny",
|
||||
"Suit Up",
|
||||
"Very Very Frightening",
|
||||
"Hot Stuff",
|
||||
"Free the End",
|
||||
"A Furious Cocktail",
|
||||
"Best Friends Forever",
|
||||
"Bring Home the Beacon",
|
||||
"Not Today, Thank You",
|
||||
"Isn't It Iron Pick",
|
||||
"Local Brewery",
|
||||
"The Next Generation",
|
||||
"Fishy Business",
|
||||
"Hot Tourist Destinations",
|
||||
"This Boat Has Legs",
|
||||
"Sniper Duel",
|
||||
"Nether",
|
||||
"Great View From Up Here",
|
||||
"How Did We Get Here?",
|
||||
"Bullseye",
|
||||
"Spooky Scary Skeleton",
|
||||
"Two by Two",
|
||||
"Stone Age",
|
||||
"Two Birds, One Arrow",
|
||||
"We Need to Go Deeper",
|
||||
"Who's the Pillager Now?",
|
||||
"Getting an Upgrade",
|
||||
"Tactical Fishing",
|
||||
"Zombie Doctor",
|
||||
"The City at the End of the Game",
|
||||
"Ice Bucket Challenge",
|
||||
"Remote Getaway",
|
||||
"Into Fire",
|
||||
"War Pigs",
|
||||
"Take Aim",
|
||||
"Total Beelocation",
|
||||
"Arbalistic",
|
||||
"The End... Again...",
|
||||
"Acquire Hardware",
|
||||
"Not Quite \"Nine\" Lives",
|
||||
"Cover Me With Diamonds",
|
||||
"Sky's the Limit",
|
||||
"Hired Help",
|
||||
"Return to Sender",
|
||||
"Sweet Dreams",
|
||||
"You Need a Mint",
|
||||
"Adventure",
|
||||
"Monsters Hunted",
|
||||
"Enchanter",
|
||||
"Voluntary Exile",
|
||||
"Eye Spy",
|
||||
"The End",
|
||||
"Serious Dedication",
|
||||
"Postmortal",
|
||||
"Monster Hunter",
|
||||
"Adventuring Time",
|
||||
"A Seedy Place",
|
||||
"Those Were the Days",
|
||||
"Hero of the Village",
|
||||
"Hidden in the Depths",
|
||||
"Beaconator",
|
||||
"Withering Heights",
|
||||
"A Balanced Diet",
|
||||
"Subspace Bubble",
|
||||
"Husbandry",
|
||||
"Country Lode, Take Me Home",
|
||||
"Bee Our Guest",
|
||||
"What a Deal!",
|
||||
"Uneasy Alliance",
|
||||
"Diamonds!",
|
||||
"A Terrible Fortress",
|
||||
"A Throwaway Joke",
|
||||
"Minecraft",
|
||||
"Sticky Situation",
|
||||
"Ol' Betsy",
|
||||
"Cover Me in Debris",
|
||||
"The End?",
|
||||
"The Parrots and the Bats",
|
||||
"A Complete Catalogue",
|
||||
"Getting Wood",
|
||||
"Time to Mine!",
|
||||
"Hot Topic",
|
||||
"Bake Bread",
|
||||
"The Lie",
|
||||
"On a Rail",
|
||||
"Time to Strike!",
|
||||
"Cow Tipper",
|
||||
"When Pigs Fly",
|
||||
"Overkill",
|
||||
"Librarian",
|
||||
"Overpowered",
|
||||
"Wax On",
|
||||
"Wax Off",
|
||||
"The Cutest Predator",
|
||||
"The Healing Power of Friendship",
|
||||
"Is It a Bird?",
|
||||
"Is It a Balloon?",
|
||||
"Is It a Plane?",
|
||||
"Surge Protector",
|
||||
"Light as a Rabbit",
|
||||
"Glow and Behold!",
|
||||
"Whatever Floats Your Goat!",
|
||||
"Caves & Cliffs",
|
||||
"Feels like home",
|
||||
"Sound of Music",
|
||||
"Star Trader",
|
||||
"Birthday Song",
|
||||
"Bukkit Bukkit",
|
||||
"It Spreads",
|
||||
"Sneak 100",
|
||||
"When the Squad Hops into Town",
|
||||
"With Our Powers Combined!",
|
||||
"You've Got a Friend in Me"
|
||||
],
|
||||
"locations_by_region": {
|
||||
"Overworld": [
|
||||
"Who is Cutting Onions?",
|
||||
"Oh Shiny",
|
||||
"Suit Up",
|
||||
"Very Very Frightening",
|
||||
"Hot Stuff",
|
||||
"Best Friends Forever",
|
||||
"Not Today, Thank You",
|
||||
"Isn't It Iron Pick",
|
||||
"Fishy Business",
|
||||
"Sniper Duel",
|
||||
"Bullseye",
|
||||
"Stone Age",
|
||||
"Two Birds, One Arrow",
|
||||
"Getting an Upgrade",
|
||||
"Tactical Fishing",
|
||||
"Zombie Doctor",
|
||||
"Ice Bucket Challenge",
|
||||
"Take Aim",
|
||||
"Total Beelocation",
|
||||
"Arbalistic",
|
||||
"Acquire Hardware",
|
||||
"Cover Me With Diamonds",
|
||||
"Hired Help",
|
||||
"Sweet Dreams",
|
||||
"Adventure",
|
||||
"Monsters Hunted",
|
||||
"Enchanter",
|
||||
"Eye Spy",
|
||||
"Monster Hunter",
|
||||
"Adventuring Time",
|
||||
"A Seedy Place",
|
||||
"Husbandry",
|
||||
"Bee Our Guest",
|
||||
"Diamonds!",
|
||||
"A Throwaway Joke",
|
||||
"Minecraft",
|
||||
"Sticky Situation",
|
||||
"Ol' Betsy",
|
||||
"The Parrots and the Bats",
|
||||
"Getting Wood",
|
||||
"Time to Mine!",
|
||||
"Hot Topic",
|
||||
"Bake Bread",
|
||||
"The Lie",
|
||||
"On a Rail",
|
||||
"Time to Strike!",
|
||||
"Cow Tipper",
|
||||
"When Pigs Fly",
|
||||
"Librarian",
|
||||
"Wax On",
|
||||
"Wax Off",
|
||||
"The Cutest Predator",
|
||||
"The Healing Power of Friendship",
|
||||
"Is It a Bird?",
|
||||
"Surge Protector",
|
||||
"Light as a Rabbit",
|
||||
"Glow and Behold!",
|
||||
"Whatever Floats Your Goat!",
|
||||
"Caves & Cliffs",
|
||||
"Sound of Music",
|
||||
"Bukkit Bukkit",
|
||||
"It Spreads",
|
||||
"Sneak 100",
|
||||
"When the Squad Hops into Town"
|
||||
],
|
||||
"The Nether": [
|
||||
"Hot Tourist Destinations",
|
||||
"This Boat Has Legs",
|
||||
"Nether",
|
||||
"Two by Two",
|
||||
"We Need to Go Deeper",
|
||||
"Not Quite \"Nine\" Lives",
|
||||
"Return to Sender",
|
||||
"Serious Dedication",
|
||||
"Hidden in the Depths",
|
||||
"Subspace Bubble",
|
||||
"Country Lode, Take Me Home",
|
||||
"Uneasy Alliance",
|
||||
"Cover Me in Debris",
|
||||
"Is It a Balloon?",
|
||||
"Feels like home",
|
||||
"With Our Powers Combined!"
|
||||
],
|
||||
"The End": [
|
||||
"Free the End",
|
||||
"The Next Generation",
|
||||
"Remote Getaway",
|
||||
"The End... Again...",
|
||||
"You Need a Mint",
|
||||
"The End",
|
||||
"The End?",
|
||||
"Is It a Plane?"
|
||||
],
|
||||
"Village": [
|
||||
"Postmortal",
|
||||
"Hero of the Village",
|
||||
"A Balanced Diet",
|
||||
"What a Deal!",
|
||||
"A Complete Catalogue",
|
||||
"Star Trader"
|
||||
],
|
||||
"Nether Fortress": [
|
||||
"A Furious Cocktail",
|
||||
"Bring Home the Beacon",
|
||||
"Local Brewery",
|
||||
"How Did We Get Here?",
|
||||
"Spooky Scary Skeleton",
|
||||
"Into Fire",
|
||||
"Beaconator",
|
||||
"Withering Heights",
|
||||
"A Terrible Fortress",
|
||||
"Overkill"
|
||||
],
|
||||
"Pillager Outpost": [
|
||||
"Who's the Pillager Now?",
|
||||
"Voluntary Exile",
|
||||
"Birthday Song",
|
||||
"You've Got a Friend in Me"
|
||||
],
|
||||
"Bastion Remnant": [
|
||||
"War Pigs",
|
||||
"Those Were the Days",
|
||||
"Overpowered"
|
||||
],
|
||||
"End City": [
|
||||
"Great View From Up Here",
|
||||
"The City at the End of the Game",
|
||||
"Sky's the Limit"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"regions": [
|
||||
["Menu", ["New World"]],
|
||||
["Overworld", ["Nether Portal", "End Portal", "Overworld Structure 1", "Overworld Structure 2"]],
|
||||
["The Nether", ["Nether Structure 1", "Nether Structure 2"]],
|
||||
["The End", ["The End Structure"]],
|
||||
["Village", []],
|
||||
["Pillager Outpost", []],
|
||||
["Nether Fortress", []],
|
||||
["Bastion Remnant", []],
|
||||
["End City", []]
|
||||
],
|
||||
"mandatory_connections": [
|
||||
["New World", "Overworld"],
|
||||
["Nether Portal", "The Nether"],
|
||||
["End Portal", "The End"]
|
||||
],
|
||||
"default_connections": [
|
||||
["Overworld Structure 1", "Village"],
|
||||
["Overworld Structure 2", "Pillager Outpost"],
|
||||
["Nether Structure 1", "Nether Fortress"],
|
||||
["Nether Structure 2", "Bastion Remnant"],
|
||||
["The End Structure", "End City"]
|
||||
],
|
||||
"illegal_connections": {
|
||||
"Nether Fortress": ["The End Structure"]
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
# Minecraft
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
Some recipes are locked from being able to be crafted and shuffled into the item pool. It can also optionally change which
|
||||
structures appear in each dimension. Crafting recipes are re-learned when they are received from other players as item
|
||||
checks, and occasionally when completing your own achievements. See below for which recipes are shuffled.
|
||||
|
||||
## What is considered a location check in Minecraft?
|
||||
|
||||
Location checks are completed when the player completes various Minecraft achievements. Opening the advancements menu
|
||||
in-game by pressing "L" will display outstanding achievements.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
When the player receives an item in Minecraft, it either unlocks crafting recipes or puts items into the player's
|
||||
inventory directly.
|
||||
|
||||
## What is the victory condition?
|
||||
|
||||
Victory is achieved when the player kills the Ender Dragon, enters the portal in The End, and completes the credits
|
||||
sequence either by skipping it or watching it play out.
|
||||
|
||||
## Which recipes are locked?
|
||||
|
||||
* Archery
|
||||
* Bow
|
||||
* Arrow
|
||||
* Crossbow
|
||||
* Brewing
|
||||
* Blaze Powder
|
||||
* Brewing Stand
|
||||
* Enchanting
|
||||
* Enchanting Table
|
||||
* Bookshelf
|
||||
* Bucket
|
||||
* Flint & Steel
|
||||
* All Beds
|
||||
* Bottles
|
||||
* Shield
|
||||
* Fishing Rod
|
||||
* Fishing Rod
|
||||
* Carrot on a Stick
|
||||
* Warped Fungus on a Stick
|
||||
* Campfire
|
||||
* Campfire
|
||||
* Soul Campfire
|
||||
* Spyglass
|
||||
* Lead
|
||||
* Progressive Weapons
|
||||
* Tier I
|
||||
* Stone Sword
|
||||
* Stone Axe
|
||||
* Tier II
|
||||
* Iron Sword
|
||||
* Iron Axe
|
||||
* Tier III
|
||||
* Diamond Sword
|
||||
* Diamond Axe
|
||||
* Progessive Tools
|
||||
* Tier I
|
||||
* Stone Pickaxe
|
||||
* Stone Shovel
|
||||
* Stone Hoe
|
||||
* Tier II
|
||||
* Iron Pickaxe
|
||||
* Iron Shovel
|
||||
* Iron Hoe
|
||||
* Tier III
|
||||
* Diamond Pickaxe
|
||||
* Diamond Shovel
|
||||
* Diamond Hoe
|
||||
* Netherite Ingot
|
||||
* Progressive Armor
|
||||
* Tier I
|
||||
* Iron Helmet
|
||||
* Iron Chestplate
|
||||
* Iron Leggings
|
||||
* Iron Boots
|
||||
* Tier II
|
||||
* Diamond Helmet
|
||||
* Diamond Chestplate
|
||||
* Diamond Leggings
|
||||
* Diamond Boots
|
||||
* Progressive Resource Crafting
|
||||
* Tier I
|
||||
* Iron Ingot from Nuggets
|
||||
* Iron Nugget
|
||||
* Gold Ingot from Nuggets
|
||||
* Gold Nugget
|
||||
* Furnace
|
||||
* Blast Furnace
|
||||
* Tier II
|
||||
* Redstone
|
||||
* Redstone Block
|
||||
* Glowstone
|
||||
* Iron Ingot from Iron Block
|
||||
* Iron Block
|
||||
* Gold Ingot from Gold Block
|
||||
* Gold Block
|
||||
* Diamond
|
||||
* Diamond Block
|
||||
* Netherite Block
|
||||
* Netherite Ingot from Netherite Block
|
||||
* Anvil
|
||||
* Emerald
|
||||
* Emerald Block
|
||||
* Copper Block
|
||||
@@ -1,74 +0,0 @@
|
||||
# Minecraft Randomizer Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- Minecraft Java Edition from
|
||||
the [Minecraft Java Edition Store Page](https://www.minecraft.net/en-us/store/minecraft-java-edition)
|
||||
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
|
||||
## Configuring your YAML file
|
||||
|
||||
### What is a YAML file and why do I need one?
|
||||
|
||||
See the guide on setting up a basic YAML at the Archipelago setup
|
||||
guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Where do I get a YAML file?
|
||||
|
||||
You can customize your options by visiting the [Minecraft Player Options Page](/games/Minecraft/player-options)
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain Your Minecraft Data File
|
||||
|
||||
**Only one yaml file needs to be submitted per minecraft world regardless of how many players play on it.**
|
||||
|
||||
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
|
||||
the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
|
||||
files. Your data file should have a `.apmc` extension.
|
||||
|
||||
Double-click on your `.apmc` file to have the Minecraft client auto-launch the installed forge server. Make sure to
|
||||
leave this window open as this is your server console.
|
||||
|
||||
### Connect to the MultiServer
|
||||
|
||||
Open Minecraft, go to `Multiplayer > Direct Connection`, and join the `localhost` server address.
|
||||
|
||||
If you are using the website to host the game then it should auto-connect to the AP server without the need to `/connect`
|
||||
|
||||
otherwise once you are in game type `/connect <AP-Address> (Port) (Password)` where `<AP-Address>` is the address of the
|
||||
Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. Note that there is no colon between `<AP-Address>` and `(Port)`.
|
||||
`(Password)` is only required if the Archipelago server you are using has a password set.
|
||||
|
||||
### Play the game
|
||||
|
||||
When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a
|
||||
multiworld game! At this point any additional minecraft players may connect to your forge server. To start the game once
|
||||
everyone is ready use the command `/start`.
|
||||
|
||||
## Non-Windows Installation
|
||||
|
||||
The Minecraft Client will install forge and the mod for other operating systems but Java has to be provided by the
|
||||
user. Head to [minecraft_versions.json on the MC AP GitHub](https://raw.githubusercontent.com/KonoTyran/Minecraft_AP_Randomizer/master/versions/minecraft_versions.json)
|
||||
to see which java version is required. New installations will default to the topmost "release" version.
|
||||
- Install the matching Amazon Corretto JDK
|
||||
- see [Manual Installation Software Links](#manual-installation-software-links)
|
||||
- or package manager provided by your OS / distribution
|
||||
- Open your `host.yaml` and add the path to your Java below the `minecraft_options` key
|
||||
- ` java: "path/to/java-xx-amazon-corretto/bin/java"`
|
||||
- Run the Minecraft Client and select your .apmc file
|
||||
|
||||
## Full Manual Installation
|
||||
|
||||
It is highly recommended to ues the Archipelago installer to handle the installation of the forge server for you.
|
||||
Support will not be given for those wishing to manually install forge. For those of you who know how, and wish to do so,
|
||||
the following links are the versions of the software we use.
|
||||
|
||||
### Manual Installation Software Links
|
||||
|
||||
- [Minecraft Forge Download Page](https://files.minecraftforge.net/net/minecraftforge/forge/)
|
||||
- [Minecraft Archipelago Randomizer Mod Releases Page](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
- **DO NOT INSTALL THIS ON YOUR CLIENT**
|
||||
- [Amazon Corretto](https://docs.aws.amazon.com/corretto/)
|
||||
- pick the matching version and select "Downloads" on the left
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
# Guia instalación de Minecraft Randomizer
|
||||
|
||||
# Instalacion automatica para el huesped de partida
|
||||
|
||||
- descarga e instala [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) and activa el
|
||||
modulo `Minecraft Client`
|
||||
|
||||
## Software Requerido
|
||||
|
||||
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
|
||||
|
||||
## Configura tu fichero YAML
|
||||
|
||||
### Que es un fichero YAML y potque necesito uno?
|
||||
|
||||
Tu fichero YAML contiene un numero de opciones que proveen al generador con informacion sobre como debe generar tu
|
||||
juego. Cada jugador de un multiworld entregara u propio fichero YAML. Esto permite que cada jugador disfrute de una
|
||||
experiencia personalizada a su gusto y diferentes jugadores dentro del mismo multiworld pueden tener diferentes opciones
|
||||
|
||||
### Where do I get a YAML file?
|
||||
|
||||
Un fichero basico yaml para minecraft tendra este aspecto.
|
||||
|
||||
```yaml
|
||||
description: Basic Minecraft Yaml
|
||||
# Tu nombre en el juego. Espacios seran sustituidos por guinoes bajos y
|
||||
# hay un limite de 16 caracteres
|
||||
name: TuNombre
|
||||
game: Minecraft
|
||||
|
||||
# Opciones compartidas por todos los juegos:
|
||||
accessibility: full
|
||||
progression_balancing: 50
|
||||
# Opciones Especficicas para Minecraft
|
||||
|
||||
Minecraft:
|
||||
# Numero de logros requeridos (87 max) para que aparezca el Ender Dragon y completar el juego.
|
||||
advancement_goal: 50
|
||||
|
||||
# Numero de trozos de huevo de dragon a obtener (30 max) antes de que el Ender Dragon aparezca.
|
||||
egg_shards_required: 10
|
||||
|
||||
# Numero de huevos disponibles en la partida (30 max).
|
||||
egg_shards_available: 15
|
||||
|
||||
# Modifica el nivel de objetos logicamente requeridos para
|
||||
# explorar areas peligrosas y luchar contra jefes.
|
||||
combat_difficulty:
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
|
||||
# Si off, los logros que dependan de suerte o sean tediosos tendran objetos de apoyo, no necesarios para completar el juego.
|
||||
include_hard_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Si off, los logros muy dificiles tendran objetos de apoyo, no necesarios para completar el juego.
|
||||
# Solo afecta a How Did We Get Here? and Adventuring Time.
|
||||
include_insane_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Algunos logros requieren derrotar al Ender Dragon;
|
||||
# Si esto se queda en off, dichos logros no tendran objetos necesarios.
|
||||
include_postgame_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Permite el mezclado de villas, puesto, fortalezas, bastiones y ciudades de END.
|
||||
shuffle_structures:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Añade brujulas de estructura al juego,
|
||||
# apuntaran a la estructura correspondiente mas cercana.
|
||||
structure_compasses:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Reemplaza un porcentaje de objetos innecesarios por trampas abeja
|
||||
# las cuales crearan multiples abejas agresivas alrededor de los jugadores cuando se reciba.
|
||||
bee_traps:
|
||||
0: 1
|
||||
25: 0
|
||||
50: 0
|
||||
75: 0
|
||||
100: 0
|
||||
```
|
||||
|
||||
## Unirse a un juego MultiWorld
|
||||
|
||||
### Obten tu ficheros de datos Minecraft
|
||||
|
||||
**Solo un fichero yaml es necesario por mundo minecraft, sin importar el numero de jugadores que jueguen en el.**
|
||||
|
||||
Cuando te unes a un juego multiworld, se te pedirá que entregues tu fichero YAML a quien sea que hospede el juego
|
||||
multiworld (no confundir con hospedar el mundo minecraft). Una vez la generación acabe, el anfitrión te dará un enlace a
|
||||
tu fichero de datos o un zip con los ficheros de todos. Tu fichero de datos tiene una extensión `.apmc`.
|
||||
|
||||
Haz doble click en tu fichero `.apmc` para que se arranque el cliente de minecraft y el servidor forge se ejecute.
|
||||
|
||||
### Conectar al multiserver
|
||||
|
||||
Despues de poner tu fichero en el directorio `APData`, arranca el Forge server y asegurate que tienes el estado OP
|
||||
tecleando `/op TuUsuarioMinecraft` en la consola del servidor y entonces conectate con tu cliente Minecraft.
|
||||
|
||||
Una vez en juego introduce `/connect <AP-Address> (Port) (<Password>)` donde `<AP-Address>` es la dirección del
|
||||
servidor. `(Port)` solo es requerido si el servidor Archipelago no esta usando el puerto por defecto 38281.
|
||||
`(<Password>)`
|
||||
solo se necesita si el servidor Archipleago tiene un password activo.
|
||||
|
||||
### Jugar al juego
|
||||
|
||||
Cuando la consola te diga que te has unido a la sala, estas lista/o para empezar a jugar. Felicidades por unirte
|
||||
exitosamente a un juego multiworld! Llegados a este punto cualquier jugador adicional puede conectarse a tu servidor
|
||||
forge.
|
||||
|
||||
## Procedimiento de instalación manual
|
||||
|
||||
Solo es requerido si quieres usar una instalacion de forge por ti mismo, recomendamos usar el instalador de Archipelago
|
||||
|
||||
### Software Requerido
|
||||
|
||||
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
|
||||
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
**NO INSTALES ESTO EN TU CLIENTE MINECRAFT**
|
||||
|
||||
### Instalación de servidor dedicado
|
||||
|
||||
Solo una persona ha de realizar este proceso y hospedar un servidor dedicado para que los demas jueguen conectandose a
|
||||
él.
|
||||
|
||||
1. Descarga el instalador de **Minecraft Forge** 1.16.5 desde el enlace proporcionado, siempre asegurandose de bajar la
|
||||
version mas reciente.
|
||||
|
||||
2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elije **install server**.
|
||||
- En esta pagina elegiras ademas donde instalar el servidor, importante recordar esta localización en el siguiente
|
||||
paso.
|
||||
|
||||
3. Navega al directorio donde hayas instalado el servidor y abre `forge-1.16.5-xx.x.x.jar`
|
||||
- La primera vez que lances el servidor se cerrara (o no aparecerá nada en absoluto), debería haber un fichero nuevo
|
||||
en el directorio llamado `eula.txt`, el cual que contiene un enlace al EULA de minecraft, cambia la linea
|
||||
a `eula=true` para aceptar el EULA y poder utilizar el software de servidor.
|
||||
- Esto creara la estructura de directorios apropiada para el siguiente paso
|
||||
|
||||
4. Coloca el fichero `aprandomizer-x.x.x.jar` del segundo enlace en el directorio `mods`
|
||||
- Cuando se ejecute el servidor de nuevo, generara el directorio `APData` que se necesitara para jugar
|
||||
@@ -1,74 +0,0 @@
|
||||
# Guide de configuration du randomiseur Minecraft
|
||||
|
||||
## Logiciel requis
|
||||
|
||||
- Minecraft Java Edition à partir de
|
||||
la [page de la boutique Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
|
||||
- Archipelago depuis la [page des versions d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- (sélectionnez `Minecraft Client` lors de l'installation.)
|
||||
|
||||
## Configuration de votre fichier YAML
|
||||
|
||||
### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ?
|
||||
|
||||
Voir le guide sur la configuration d'un YAML de base lors de la configuration d'Archipelago
|
||||
guide : [Guide de configuration de base de Multiworld](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Où puis-je obtenir un fichier YAML ?
|
||||
|
||||
Vous pouvez personnaliser vos paramètres Minecraft en allant sur la [page des paramètres de joueur](/games/Minecraft/player-options)
|
||||
|
||||
## Rejoindre une partie MultiWorld
|
||||
|
||||
### Obtenez votre fichier de données Minecraft
|
||||
|
||||
**Un seul fichier yaml doit être soumis par monde minecraft, quel que soit le nombre de joueurs qui y jouent.**
|
||||
|
||||
Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à l'hébergeur. Une fois cela fait,
|
||||
l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun
|
||||
des dossiers. Votre fichier de données doit avoir une extension `.apmc`.
|
||||
|
||||
Double-cliquez sur votre fichier `.apmc` pour que le client Minecraft lance automatiquement le serveur forge installé. Assurez-vous de
|
||||
laissez cette fenêtre ouverte car il s'agit de votre console serveur.
|
||||
|
||||
### Connectez-vous au multiserveur
|
||||
|
||||
Ouvrez Minecraft, accédez à "Multijoueur> Connexion directe" et rejoignez l'adresse du serveur "localhost".
|
||||
|
||||
Si vous utilisez le site Web pour héberger le jeu, il devrait se connecter automatiquement au serveur AP sans avoir besoin de `/connect`
|
||||
|
||||
sinon, une fois que vous êtes dans le jeu, tapez `/connect <AP-Address> (Port) (Password)` où `<AP-Address>` est l'adresse du
|
||||
Serveur Archipelago. `(Port)` n'est requis que si le serveur Archipelago n'utilise pas le port par défaut 38281. Notez qu'il n'y a pas de deux-points entre `<AP-Address>` et `(Port)` mais un espace.
|
||||
`(Mot de passe)` n'est requis que si le serveur Archipelago que vous utilisez a un mot de passe défini.
|
||||
|
||||
### Jouer le jeu
|
||||
|
||||
Lorsque la console vous indique que vous avez rejoint la salle, vous êtes prêt. Félicitations pour avoir rejoint avec succès un
|
||||
jeu multimonde ! À ce stade, tous les joueurs minecraft supplémentaires peuvent se connecter à votre serveur forge. Pour commencer le jeu une fois
|
||||
que tout le monde est prêt utilisez la commande `/start`.
|
||||
|
||||
## Installation non Windows
|
||||
|
||||
Le client Minecraft installera forge et le mod pour d'autres systèmes d'exploitation, mais Java doit être fourni par l'
|
||||
utilisateur. Rendez-vous sur [minecraft_versions.json sur le MC AP GitHub](https://raw.githubusercontent.com/KonoTyran/Minecraft_AP_Randomizer/master/versions/minecraft_versions.json)
|
||||
pour voir quelle version de Java est requise. Les nouvelles installations utiliseront par défaut la version "release" la plus élevée.
|
||||
- Installez le JDK Amazon Corretto correspondant
|
||||
- voir les [Liens d'installation manuelle du logiciel](#manual-installation-software-links)
|
||||
- ou gestionnaire de paquets fourni par votre OS/distribution
|
||||
- Ouvrez votre `host.yaml` et ajoutez le chemin vers votre Java sous la clé `minecraft_options`
|
||||
- ` java : "chemin/vers/java-xx-amazon-corretto/bin/java"`
|
||||
- Exécutez le client Minecraft et sélectionnez votre fichier .apmc
|
||||
|
||||
## Installation manuelle complète
|
||||
|
||||
Il est fortement recommandé d'utiliser le programme d'installation d'Archipelago pour gérer l'installation du serveur forge pour vous.
|
||||
Le support ne sera pas fourni pour ceux qui souhaitent installer manuellement forge. Pour ceux d'entre vous qui savent comment faire et qui souhaitent le faire,
|
||||
les liens suivants sont les versions des logiciels que nous utilisons.
|
||||
|
||||
### Liens d'installation manuelle du logiciel
|
||||
|
||||
- [Page de téléchargement de Minecraft Forge] (https://files.minecraftforge.net/net/minecraftforge/forge/)
|
||||
- [Page des versions du mod Minecraft Archipelago Randomizer] (https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
- **NE PAS INSTALLER CECI SUR VOTRE CLIENT**
|
||||
- [Amazon Corretto](https://docs.aws.amazon.com/corretto/)
|
||||
- choisissez la version correspondante et sélectionnez "Téléchargements" sur la gauche
|
||||
@@ -1,132 +0,0 @@
|
||||
# Minecraft Randomizer Uppsättningsguide
|
||||
|
||||
## Nödvändig Mjukvara
|
||||
|
||||
### Server Värd
|
||||
|
||||
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
|
||||
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
|
||||
### Spelare
|
||||
|
||||
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
|
||||
|
||||
## Installationsprocedurer
|
||||
|
||||
### Tillägnad
|
||||
|
||||
Bara en person behöver göra denna uppsättning och vara värd för en server för alla andra spelare att koppla till.
|
||||
|
||||
1. Ladda ner 1.16.5 **Minecraft Forge** installeraren från länken ovanför och se till att ladda ner den senaste
|
||||
rekommenderade versionen.
|
||||
|
||||
2. Kör `forge-1.16.5-xx.x.x-installer.jar` filen och välj **installera server**.
|
||||
- På denna sida kommer du också välja vart du ska installera servern för att komma ihåg denna katalog. Detta är
|
||||
viktigt för nästa steg.
|
||||
|
||||
3. Navigera till vart du har installerat servern och öppna `forge-1.16.5-xx.x.x-installer.jar`
|
||||
- Under första serverstart så kommer den att stängas ner och fråga dig att acceptera Minecrafts EULA. En ny fil
|
||||
kommer skapas vid namn `eula.txt` som har en länk till Minecrafts EULA, och en linje som du behöver byta
|
||||
till `eula=true` för att acceptera Minecrafts EULA.
|
||||
- Detta kommer skapa de lämpliga katalogerna för dig att placera filerna i de följande steget.
|
||||
|
||||
4. Placera `aprandomizer-x.x.x.jar` länken ovanför i `mods` mappen som ligger ovanför installationen av din forge
|
||||
server.
|
||||
- Kör servern igen. Den kommer ladda up och generera den nödvändiga katalogen `APData` för när du är redo att spela!
|
||||
|
||||
### Grundläggande Spelaruppsättning
|
||||
|
||||
- Köp och installera Minecraft från länken ovanför.
|
||||
|
||||
**Du är klar**.
|
||||
|
||||
Andra spelare behöver endast ha en 'Vanilla' omodifierad version av Minecraft för att kunna spela!
|
||||
|
||||
### Avancerad Spelaruppsättning
|
||||
|
||||
***Detta är inte nödvändigt för att spela ett slumpmässigt Minecraftspel.***
|
||||
Dock så är det rekommenderat eftersom det hjälper att göra upplevelsen mer trevligt.
|
||||
|
||||
#### Rekommenderade Moddar
|
||||
|
||||
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
|
||||
|
||||
|
||||
1. Installera och Kör Minecraft från länken ovanför minst en gång.
|
||||
2. Kör `forge-1.16.5-xx.x.x-installer.jar` filen och välj **installera klient**.
|
||||
- Starta Minecraft Forge minst en gång för att skapa katalogerna som behövs för de nästa stegen.
|
||||
3. Navigera till din Minecraft installationskatalog och placera de önskade moddarna med `.jar` i `mods` -katalogen.
|
||||
- Standardinstallationskatalogerna är som följande;
|
||||
- Windows `%APPDATA%\.minecraft\mods`
|
||||
- macOS `~/Library/Application Support/minecraft/mods`
|
||||
- Linux `~/.minecraft/mods`
|
||||
|
||||
## Konfigurera Din YAML-fil
|
||||
|
||||
### Vad är en YAML-fil och varför behöver jag en?
|
||||
|
||||
Din YAML-fil behåller en uppsättning av konfigurationsalternativ som ger generatorn med information om hur den borde
|
||||
generera ditt spel. Varje spelare i en multivärld kommer behöva ge deras egen YAML-fil. Denna uppsättning tillåter varje
|
||||
spelare att an njuta av en upplevelse anpassade för deras smaker, och olika spelare i samma multivärld kan ha helt olika
|
||||
alternativ.
|
||||
|
||||
### Vart kan jag få tag i en YAML-fil?
|
||||
|
||||
En grundläggande Minecraft YAML kommer se ut så här.
|
||||
|
||||
```yaml
|
||||
description: Template Name
|
||||
# Ditt spelnamn. Mellanslag kommer bli omplacerad med understräck och det är en 16-karaktärsgräns.
|
||||
name: YourName
|
||||
game: Minecraft
|
||||
accessibility: full
|
||||
progression_balancing: 0
|
||||
advancement_goal:
|
||||
few: 0
|
||||
normal: 1
|
||||
many: 0
|
||||
combat_difficulty:
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
include_hard_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
include_insane_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
include_postgame_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
shuffle_structures:
|
||||
on: 1
|
||||
off: 0
|
||||
```
|
||||
|
||||
|
||||
## Gå med i ett Multivärld-spel
|
||||
|
||||
### Skaffa din Minecraft data-fil
|
||||
|
||||
**Endast en YAML-fil behöver användats per Minecraft-värld oavsett hur många spelare det är som spelar.**
|
||||
|
||||
När du går med it ett Multivärld spel så kommer du bli ombedd att lämna in din YAML-fil till personen som värdar. När
|
||||
detta är klart så kommer värden att ge dig antingen en länk till att ladda ner din data-fil, eller mer en zip-fil som
|
||||
innehåller allas data-filer. Din data-fil borde ha en `.apmc` -extension.
|
||||
|
||||
Lägg din data-fil i dina forge-servrar `APData` -mapp. Se till att ta bort alla tidigare data-filer som var i där förut.
|
||||
|
||||
### Koppla till Multiservern
|
||||
|
||||
Efter du har placerat din data-fil i `APData` -mappen, starta forge-servern och se till att you har OP-status genom att
|
||||
skriva `/op DittAnvändarnamn` i forger-serverns konsol innan du kopplar dig till din Minecraft klient. När du är inne i
|
||||
spelet, skriv `/connect <AP-Address> (<Lösenord>)` där `<AP-Address>` är addressen av
|
||||
Archipelago-servern. `(<Lösenord>)` är endast nödvändigt om Archipelago-servern som du använder har ett tillsatt
|
||||
lösenord.
|
||||
|
||||
### Spela spelet
|
||||
|
||||
När konsolen har informerat att du har gått med i rummet så är du redo att börja spela. Grattis att du har lykats med
|
||||
att gått med i ett Multivärld-spel! Vid detta tillfälle, alla ytterligare Minecraft-spelare må koppla in till din
|
||||
forge-server.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
requests >= 2.28.1 # used by client
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,60 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from .. import Constants
|
||||
|
||||
class TestDataLoad(unittest.TestCase):
|
||||
|
||||
def test_item_data(self):
|
||||
item_info = Constants.item_info
|
||||
|
||||
# All items in sub-tables are in all_items
|
||||
all_items: set = set(item_info['all_items'])
|
||||
assert set(item_info['progression_items']) <= all_items
|
||||
assert set(item_info['useful_items']) <= all_items
|
||||
assert set(item_info['trap_items']) <= all_items
|
||||
assert set(item_info['required_pool'].keys()) <= all_items
|
||||
assert set(item_info['junk_weights'].keys()) <= all_items
|
||||
|
||||
# No overlapping ids (because of bee trap stuff)
|
||||
all_ids: set = set(Constants.item_name_to_id.values())
|
||||
assert len(all_items) == len(all_ids)
|
||||
|
||||
def test_location_data(self):
|
||||
location_info = Constants.location_info
|
||||
exclusion_info = Constants.exclusion_info
|
||||
|
||||
# Every location has a region and every region's locations are in all_locations
|
||||
all_locations: set = set(location_info['all_locations'])
|
||||
all_locs_2: set = set()
|
||||
for v in location_info['locations_by_region'].values():
|
||||
all_locs_2.update(v)
|
||||
assert all_locations == all_locs_2
|
||||
|
||||
# All exclusions are locations
|
||||
for v in exclusion_info.values():
|
||||
assert set(v) <= all_locations
|
||||
|
||||
def test_region_data(self):
|
||||
region_info = Constants.region_info
|
||||
|
||||
# Every entrance and region in mandatory/default/illegal connections is a real entrance and region
|
||||
all_regions = set()
|
||||
all_entrances = set()
|
||||
for v in region_info['regions']:
|
||||
assert isinstance(v[0], str)
|
||||
assert isinstance(v[1], list)
|
||||
all_regions.add(v[0])
|
||||
all_entrances.update(v[1])
|
||||
|
||||
for v in region_info['mandatory_connections']:
|
||||
assert v[0] in all_entrances
|
||||
assert v[1] in all_regions
|
||||
|
||||
for v in region_info['default_connections']:
|
||||
assert v[0] in all_entrances
|
||||
assert v[1] in all_regions
|
||||
|
||||
for k, v in region_info['illegal_connections'].items():
|
||||
assert k in all_regions
|
||||
assert set(v) <= all_entrances
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
from . import MCTestBase
|
||||
|
||||
|
||||
class TestEntrances(MCTestBase):
|
||||
options = {
|
||||
"shuffle_structures": False,
|
||||
"structure_compasses": False
|
||||
}
|
||||
|
||||
def testPortals(self):
|
||||
self.run_entrance_tests([
|
||||
['Nether Portal', False, []],
|
||||
['Nether Portal', False, [], ['Flint and Steel']],
|
||||
['Nether Portal', False, [], ['Progressive Resource Crafting']],
|
||||
['Nether Portal', False, [], ['Progressive Tools']],
|
||||
['Nether Portal', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
['Nether Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket']],
|
||||
['Nether Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools']],
|
||||
|
||||
['End Portal', False, []],
|
||||
['End Portal', False, [], ['Brewing']],
|
||||
['End Portal', False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']],
|
||||
['End Portal', False, [], ['Flint and Steel']],
|
||||
['End Portal', False, [], ['Progressive Resource Crafting']],
|
||||
['End Portal', False, [], ['Progressive Tools']],
|
||||
['End Portal', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
['End Portal', False, [], ['Progressive Weapons']],
|
||||
['End Portal', False, [], ['Progressive Armor', 'Shield']],
|
||||
['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket',
|
||||
'Progressive Weapons', 'Progressive Armor',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket',
|
||||
'Progressive Weapons', 'Shield',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
'Progressive Weapons', 'Progressive Armor',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
'Progressive Weapons', 'Shield',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
])
|
||||
|
||||
def testStructures(self):
|
||||
self.run_entrance_tests([ # Structures 1 and 2 should be logically equivalent
|
||||
['Overworld Structure 1', False, []],
|
||||
['Overworld Structure 1', False, [], ['Progressive Weapons']],
|
||||
['Overworld Structure 1', False, [], ['Progressive Resource Crafting', 'Campfire']],
|
||||
['Overworld Structure 1', True, ['Progressive Weapons', 'Progressive Resource Crafting']],
|
||||
['Overworld Structure 1', True, ['Progressive Weapons', 'Campfire']],
|
||||
|
||||
['Overworld Structure 2', False, []],
|
||||
['Overworld Structure 2', False, [], ['Progressive Weapons']],
|
||||
['Overworld Structure 2', False, [], ['Progressive Resource Crafting', 'Campfire']],
|
||||
['Overworld Structure 2', True, ['Progressive Weapons', 'Progressive Resource Crafting']],
|
||||
['Overworld Structure 2', True, ['Progressive Weapons', 'Campfire']],
|
||||
|
||||
['Nether Structure 1', False, []],
|
||||
['Nether Structure 1', False, [], ['Flint and Steel']],
|
||||
['Nether Structure 1', False, [], ['Progressive Resource Crafting']],
|
||||
['Nether Structure 1', False, [], ['Progressive Tools']],
|
||||
['Nether Structure 1', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
['Nether Structure 1', False, [], ['Progressive Weapons']],
|
||||
['Nether Structure 1', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']],
|
||||
['Nether Structure 1', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']],
|
||||
|
||||
['Nether Structure 2', False, []],
|
||||
['Nether Structure 2', False, [], ['Flint and Steel']],
|
||||
['Nether Structure 2', False, [], ['Progressive Resource Crafting']],
|
||||
['Nether Structure 2', False, [], ['Progressive Tools']],
|
||||
['Nether Structure 2', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
['Nether Structure 2', False, [], ['Progressive Weapons']],
|
||||
['Nether Structure 2', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']],
|
||||
['Nether Structure 2', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']],
|
||||
|
||||
['The End Structure', False, []],
|
||||
['The End Structure', False, [], ['Brewing']],
|
||||
['The End Structure', False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']],
|
||||
['The End Structure', False, [], ['Flint and Steel']],
|
||||
['The End Structure', False, [], ['Progressive Resource Crafting']],
|
||||
['The End Structure', False, [], ['Progressive Tools']],
|
||||
['The End Structure', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
['The End Structure', False, [], ['Progressive Weapons']],
|
||||
['The End Structure', False, [], ['Progressive Armor', 'Shield']],
|
||||
['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket',
|
||||
'Progressive Weapons', 'Progressive Armor',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket',
|
||||
'Progressive Weapons', 'Shield',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
'Progressive Weapons', 'Progressive Armor',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
'Progressive Weapons', 'Shield',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
|
||||
])
|
||||
@@ -1,49 +0,0 @@
|
||||
from . import MCTestBase
|
||||
from ..Constants import region_info
|
||||
from .. import Options
|
||||
|
||||
from BaseClasses import ItemClassification
|
||||
|
||||
class AdvancementTestBase(MCTestBase):
|
||||
options = {
|
||||
"advancement_goal": Options.AdvancementGoal.range_end
|
||||
}
|
||||
# beatability test implicit
|
||||
|
||||
class ShardTestBase(MCTestBase):
|
||||
options = {
|
||||
"egg_shards_required": Options.EggShardsRequired.range_end,
|
||||
"egg_shards_available": Options.EggShardsAvailable.range_end
|
||||
}
|
||||
|
||||
# check that itempool is not overfilled with shards
|
||||
def test_itempool(self):
|
||||
assert len(self.multiworld.get_unfilled_locations()) == len(self.multiworld.itempool)
|
||||
|
||||
class CompassTestBase(MCTestBase):
|
||||
def test_compasses_in_pool(self):
|
||||
structures = [x[1] for x in region_info["default_connections"]]
|
||||
itempool_str = {item.name for item in self.multiworld.itempool}
|
||||
for struct in structures:
|
||||
assert f"Structure Compass ({struct})" in itempool_str
|
||||
|
||||
class NoBeeTestBase(MCTestBase):
|
||||
options = {
|
||||
"bee_traps": Options.BeeTraps.range_start
|
||||
}
|
||||
|
||||
# With no bees, there are no traps in the pool
|
||||
def test_bees(self):
|
||||
for item in self.multiworld.itempool:
|
||||
assert item.classification != ItemClassification.trap
|
||||
|
||||
|
||||
class AllBeeTestBase(MCTestBase):
|
||||
options = {
|
||||
"bee_traps": Options.BeeTraps.range_end
|
||||
}
|
||||
|
||||
# With max bees, there are no filler items, only bee traps
|
||||
def test_bees(self):
|
||||
for item in self.multiworld.itempool:
|
||||
assert item.classification != ItemClassification.filler
|
||||
@@ -1,33 +0,0 @@
|
||||
from test.bases import TestBase, WorldTestBase
|
||||
from .. import MinecraftWorld, MinecraftOptions
|
||||
|
||||
|
||||
class MCTestBase(WorldTestBase, TestBase):
|
||||
game = "Minecraft"
|
||||
player: int = 1
|
||||
|
||||
def _create_items(self, items, player):
|
||||
singleton = False
|
||||
if isinstance(items, str):
|
||||
items = [items]
|
||||
singleton = True
|
||||
ret = [self.multiworld.worlds[player].create_item(item) for item in items]
|
||||
if singleton:
|
||||
return ret[0]
|
||||
return ret
|
||||
|
||||
def _get_items(self, item_pool, all_except):
|
||||
if all_except and len(all_except) > 0:
|
||||
items = self.multiworld.itempool[:]
|
||||
items = [item for item in items if item.name not in all_except]
|
||||
items.extend(self._create_items(item_pool[0], 1))
|
||||
else:
|
||||
items = self._create_items(item_pool[0], 1)
|
||||
return self.get_state(items)
|
||||
|
||||
def _get_items_partial(self, item_pool, missing_item):
|
||||
new_items = item_pool[0].copy()
|
||||
new_items.remove(missing_item)
|
||||
items = self._create_items(new_items, 1)
|
||||
return self.get_state(items)
|
||||
|
||||
@@ -185,7 +185,7 @@ class TextArchive:
|
||||
# As far as I know, this should literally not be possible.
|
||||
# Every script I've looked at has dozens of unused indices, so finding 9 (8 plus one "ending" script)
|
||||
# should be no problem. We re-use these so we don't have to worry about an area getting tons of these
|
||||
raise AssertionError("Error in generation -- not enough room for progressive undernet in archive "+self.startOffset)
|
||||
raise AssertionError(f"Error in generation -- not enough room for progressive undernet in archive {self.startOffset} ({hex(self.startOffset)})")
|
||||
for i in range(9): # There are 8 progressive undernet ranks
|
||||
new_script_index = self.unused_indices[i]
|
||||
new_script = ArchiveScript(new_script_index, generate_progressive_undernet(i, self.unused_indices[i+1]))
|
||||
@@ -319,15 +319,16 @@ class MMBN3DeltaPatch(APDeltaPatch):
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options = Utils.get_options()
|
||||
if not file_name:
|
||||
bn3_options = options.get("mmbn3_options", None)
|
||||
from worlds.mmbn3 import MMBN3World
|
||||
bn3_options = MMBN3World.settings
|
||||
|
||||
if bn3_options is None:
|
||||
file_name = "Mega Man Battle Network 3 - Blue Version (USA).gba"
|
||||
else:
|
||||
file_name = bn3_options["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.local_path(file_name)
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
import settings
|
||||
import typing
|
||||
import threading
|
||||
|
||||
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification, Region, Entrance, \
|
||||
LocationProgressType
|
||||
@@ -16,7 +15,7 @@ from .Options import MMBN3Options
|
||||
from .Regions import regions, RegionName
|
||||
from .Names.ItemName import ItemName
|
||||
from .Names.LocationName import LocationName
|
||||
from worlds.generic.Rules import add_item_rule, add_rule
|
||||
from worlds.generic.Rules import add_item_rule, add_rule, forbid_item
|
||||
|
||||
|
||||
class MMBN3Settings(settings.Group):
|
||||
@@ -26,8 +25,15 @@ class MMBN3Settings(settings.Group):
|
||||
description = "MMBN3 ROM File"
|
||||
md5s = [MMBN3DeltaPatch.hash]
|
||||
|
||||
class RomStart(str):
|
||||
"""
|
||||
Set this to false to never autostart a rom (such as after patching),
|
||||
true for operating system default program
|
||||
Alternatively, a path to a program to open the .gba file with
|
||||
"""
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
rom_start: bool = True
|
||||
rom_start: RomStart | bool = True
|
||||
|
||||
|
||||
class MMBN3Web(WebWorld):
|
||||
@@ -203,134 +209,134 @@ class MMBN3World(World):
|
||||
|
||||
# Set WWW ID requirements
|
||||
def has_www_id(state): return state.has(ItemName.WWW_ID, self.player)
|
||||
add_rule(self.multiworld.get_location(LocationName.ACDC_1_PMD, self.player), has_www_id)
|
||||
add_rule(self.multiworld.get_location(LocationName.SciLab_1_WWW_BMD, self.player), has_www_id)
|
||||
add_rule(self.multiworld.get_location(LocationName.Yoka_1_WWW_BMD, self.player), has_www_id)
|
||||
add_rule(self.multiworld.get_location(LocationName.Undernet_1_WWW_BMD, self.player), has_www_id)
|
||||
add_rule(self.get_location(LocationName.ACDC_1_PMD), has_www_id)
|
||||
add_rule(self.get_location(LocationName.SciLab_1_WWW_BMD), has_www_id)
|
||||
add_rule(self.get_location(LocationName.Yoka_1_WWW_BMD), has_www_id)
|
||||
add_rule(self.get_location(LocationName.Undernet_1_WWW_BMD), has_www_id)
|
||||
|
||||
# Set Press Program requirements
|
||||
def has_press(state): return state.has(ItemName.Press, self.player)
|
||||
add_rule(self.multiworld.get_location(LocationName.Yoka_1_PMD, self.player), has_press)
|
||||
add_rule(self.multiworld.get_location(LocationName.Yoka_2_Upper_BMD, self.player), has_press)
|
||||
add_rule(self.multiworld.get_location(LocationName.Beach_2_East_BMD, self.player), has_press)
|
||||
add_rule(self.multiworld.get_location(LocationName.Hades_South_BMD, self.player), has_press)
|
||||
add_rule(self.multiworld.get_location(LocationName.Secret_3_BugFrag_BMD, self.player), has_press)
|
||||
add_rule(self.multiworld.get_location(LocationName.Secret_3_Island_BMD, self.player), has_press)
|
||||
add_rule(self.get_location(LocationName.Yoka_1_PMD), has_press)
|
||||
add_rule(self.get_location(LocationName.Yoka_2_Upper_BMD), has_press)
|
||||
add_rule(self.get_location(LocationName.Beach_2_East_BMD), has_press)
|
||||
add_rule(self.get_location(LocationName.Hades_South_BMD), has_press)
|
||||
add_rule(self.get_location(LocationName.Secret_3_BugFrag_BMD), has_press)
|
||||
add_rule(self.get_location(LocationName.Secret_3_Island_BMD), has_press)
|
||||
|
||||
# Set Purple Mystery Data Unlocker access
|
||||
def can_unlock(state): return state.can_reach_region(RegionName.SciLab_Overworld, self.player) or \
|
||||
state.can_reach_region(RegionName.SciLab_Cyberworld, self.player) or \
|
||||
state.can_reach_region(RegionName.Yoka_Cyberworld, self.player) or \
|
||||
state.has(ItemName.Unlocker, self.player, 8) # There are 8 PMDs that aren't in one of the above areas
|
||||
add_rule(self.multiworld.get_location(LocationName.ACDC_1_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Yoka_1_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Beach_1_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Undernet_7_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Mayls_HP_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.SciLab_Dads_Computer_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Zoo_Panda_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Beach_DNN_Security_Panel_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Beach_DNN_Main_Console_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Tamakos_HP_PMD, self.player), can_unlock)
|
||||
add_rule(self.get_location(LocationName.ACDC_1_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Yoka_1_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Beach_1_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Undernet_7_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Mayls_HP_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.SciLab_Dads_Computer_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Zoo_Panda_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Beach_DNN_Security_Panel_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Beach_DNN_Main_Console_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Tamakos_HP_PMD), can_unlock)
|
||||
|
||||
# Set Job additional area access
|
||||
self.multiworld.get_location(LocationName.Please_deliver_this, self.player).access_rule = \
|
||||
self.get_location(LocationName.Please_deliver_this).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.ACDC_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.ACDC_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.My_Navi_is_sick, self.player).access_rule =\
|
||||
self.get_location(LocationName.My_Navi_is_sick).access_rule =\
|
||||
lambda state: \
|
||||
state.has(ItemName.Recov30_star, self.player)
|
||||
self.multiworld.get_location(LocationName.Help_me_with_my_son, self.player).access_rule =\
|
||||
self.get_location(LocationName.Help_me_with_my_son).access_rule =\
|
||||
lambda state:\
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.ACDC_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Transmission_error, self.player).access_rule = \
|
||||
self.get_location(LocationName.Transmission_error).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Chip_Prices, self.player).access_rule = \
|
||||
self.get_location(LocationName.Chip_Prices).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.ACDC_Cyberworld, self.player) and \
|
||||
state.can_reach_region(RegionName.SciLab_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Im_broke, self.player).access_rule = \
|
||||
self.get_location(LocationName.Im_broke).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.Yoka_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Rare_chips_for_cheap, self.player).access_rule = \
|
||||
self.get_location(LocationName.Rare_chips_for_cheap).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.ACDC_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Be_my_boyfriend, self.player).access_rule =\
|
||||
self.get_location(LocationName.Be_my_boyfriend).access_rule =\
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Beach_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Will_you_deliver, self.player).access_rule=\
|
||||
self.get_location(LocationName.Will_you_deliver).access_rule=\
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.ACDC_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Somebody_please_help, self.player).access_rule = \
|
||||
self.get_location(LocationName.Somebody_please_help).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.ACDC_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Looking_for_condor, self.player).access_rule = \
|
||||
self.get_location(LocationName.Looking_for_condor).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.ACDC_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Help_with_rehab, self.player).access_rule = \
|
||||
self.get_location(LocationName.Help_with_rehab).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Help_with_rehab_bonus, self.player).access_rule = \
|
||||
self.get_location(LocationName.Help_with_rehab_bonus).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Old_Master, self.player).access_rule = \
|
||||
self.get_location(LocationName.Old_Master).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.ACDC_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Catching_gang_members, self.player).access_rule = \
|
||||
self.get_location(LocationName.Catching_gang_members).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Cyberworld, self.player) and \
|
||||
state.has(ItemName.Press, self.player)
|
||||
self.multiworld.get_location(LocationName.Please_adopt_a_virus, self.player).access_rule = \
|
||||
self.get_location(LocationName.Please_adopt_a_virus).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.SciLab_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Legendary_Tomes, self.player).access_rule = \
|
||||
self.get_location(LocationName.Legendary_Tomes).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.Undernet, self.player) and \
|
||||
state.can_reach_region(RegionName.Deep_Undernet, self.player) and \
|
||||
state.has_all({ItemName.Press, ItemName.Magnum1_A}, self.player)
|
||||
self.multiworld.get_location(LocationName.Legendary_Tomes_Treasure, self.player).access_rule = \
|
||||
self.get_location(LocationName.Legendary_Tomes_Treasure).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.ACDC_Overworld, self.player) and \
|
||||
state.can_reach_location(LocationName.Legendary_Tomes, self.player)
|
||||
self.multiworld.get_location(LocationName.Hide_and_seek_First_Child, self.player).access_rule = \
|
||||
self.get_location(LocationName.Hide_and_seek_First_Child).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Hide_and_seek_Second_Child, self.player).access_rule = \
|
||||
self.get_location(LocationName.Hide_and_seek_Second_Child).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Hide_and_seek_Third_Child, self.player).access_rule = \
|
||||
self.get_location(LocationName.Hide_and_seek_Third_Child).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Hide_and_seek_Fourth_Child, self.player).access_rule = \
|
||||
self.get_location(LocationName.Hide_and_seek_Fourth_Child).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Hide_and_seek_Completion, self.player).access_rule = \
|
||||
self.get_location(LocationName.Hide_and_seek_Completion).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Finding_the_blue_Navi, self.player).access_rule = \
|
||||
self.get_location(LocationName.Finding_the_blue_Navi).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Undernet, self.player)
|
||||
self.multiworld.get_location(LocationName.Give_your_support, self.player).access_rule = \
|
||||
self.get_location(LocationName.Give_your_support).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Stamp_collecting, self.player).access_rule = \
|
||||
self.get_location(LocationName.Stamp_collecting).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.ACDC_Cyberworld, self.player) and \
|
||||
state.can_reach_region(RegionName.SciLab_Cyberworld, self.player) and \
|
||||
state.can_reach_region(RegionName.Yoka_Cyberworld, self.player) and \
|
||||
state.can_reach_region(RegionName.Beach_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Help_with_a_will, self.player).access_rule = \
|
||||
self.get_location(LocationName.Help_with_a_will).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.ACDC_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.ACDC_Cyberworld, self.player) and \
|
||||
@@ -340,100 +346,115 @@ class MMBN3World(World):
|
||||
state.can_reach_region(RegionName.Undernet, self.player)
|
||||
|
||||
# Set Trade quests
|
||||
self.multiworld.get_location(LocationName.ACDC_SonicWav_W_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.ACDC_SonicWav_W_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.SonicWav_W, self.player)
|
||||
self.multiworld.get_location(LocationName.ACDC_Bubbler_C_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.ACDC_Bubbler_C_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.Bubbler_C, self.player)
|
||||
self.multiworld.get_location(LocationName.ACDC_Recov120_S_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.ACDC_Recov120_S_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.Recov120_S, self.player)
|
||||
self.multiworld.get_location(LocationName.SciLab_Shake1_S_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.SciLab_Shake1_S_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.Shake1_S, self.player)
|
||||
self.multiworld.get_location(LocationName.Yoka_FireSwrd_P_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.Yoka_FireSwrd_P_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.FireSwrd_P, self.player)
|
||||
self.multiworld.get_location(LocationName.Hospital_DynaWav_V_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.Hospital_DynaWav_V_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.DynaWave_V, self.player)
|
||||
self.multiworld.get_location(LocationName.Beach_DNN_WideSwrd_C_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.Beach_DNN_WideSwrd_C_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.WideSwrd_C, self.player)
|
||||
self.multiworld.get_location(LocationName.Beach_DNN_HoleMetr_H_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.Beach_DNN_HoleMetr_H_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.HoleMetr_H, self.player)
|
||||
self.multiworld.get_location(LocationName.Beach_DNN_Shadow_J_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.Beach_DNN_Shadow_J_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.Shadow_J, self.player)
|
||||
self.multiworld.get_location(LocationName.Hades_GrabBack_K_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.Hades_GrabBack_K_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.GrabBack_K, self.player)
|
||||
|
||||
# Set Number Traders
|
||||
|
||||
# The first 8 are considered cheap enough to grind for in ACDC. Protip: Try grinding in the tank
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_09, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_09).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_10, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_10).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_11, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_11).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_12, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_12).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_13, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_13).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_14, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_14).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_15, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_15).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_16, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_16).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_17, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_17).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_18, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_18).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_19, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_19).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_20, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_20).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_21, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_21).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_22, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_22).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_23, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_23).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_24, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_24).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_25, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_25).access_rule =\
|
||||
lambda state: self.explore_score(state) > 8
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_26, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_26).access_rule =\
|
||||
lambda state: self.explore_score(state) > 8
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_27, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_27).access_rule =\
|
||||
lambda state: self.explore_score(state) > 8
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_28, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_28).access_rule =\
|
||||
lambda state: self.explore_score(state) > 8
|
||||
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_29, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_29).access_rule =\
|
||||
lambda state: self.explore_score(state) > 10
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_30, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_30).access_rule =\
|
||||
lambda state: self.explore_score(state) > 10
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_31, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_31).access_rule =\
|
||||
lambda state: self.explore_score(state) > 10
|
||||
|
||||
#miscellaneous locations with extra requirements
|
||||
add_rule(self.multiworld.get_location(LocationName.Comedian, self.player),
|
||||
add_rule(self.get_location(LocationName.Comedian),
|
||||
lambda state: state.has(ItemName.Humor, self.player))
|
||||
add_rule(self.multiworld.get_location(LocationName.Villain, self.player),
|
||||
add_rule(self.get_location(LocationName.Villain),
|
||||
lambda state: state.has(ItemName.BlckMnd, self.player))
|
||||
def not_undernet(item): return item.code != item_table[ItemName.Progressive_Undernet_Rank].code or item.player != self.player
|
||||
self.multiworld.get_location(LocationName.WWW_1_Central_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_1_East_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_2_East_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_2_Northwest_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_3_East_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_3_North_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_4_Northwest_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_4_Central_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_Wall_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_Control_Room_1_Screen, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_Wilys_Desk, self.player).item_rule = not_undernet
|
||||
forbid_item(self.get_location(LocationName.WWW_1_Central_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_1_East_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_2_East_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_2_Northwest_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_3_East_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_3_North_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_4_Northwest_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_4_Central_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_Wall_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_Control_Room_1_Screen),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_Wilys_Desk),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
|
||||
# I have no fuckin clue why this specific location shits the bed on a progressive undernet rank.
|
||||
# If you ever figure it out I will buy you a pizza.
|
||||
forbid_item(self.get_location(LocationName.Chocolate_Shop_07),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
|
||||
# place "Victory" at "Final Boss" and set collection as win condition
|
||||
self.multiworld.get_location(LocationName.Alpha_Defeated, self.player) \
|
||||
self.get_location(LocationName.Alpha_Defeated) \
|
||||
.place_locked_item(self.create_event(ItemName.Victory))
|
||||
self.multiworld.completion_condition[self.player] = \
|
||||
lambda state: state.has(ItemName.Victory, self.player)
|
||||
|
||||
Binary file not shown.
@@ -641,4 +641,17 @@ SONG_DATA: Dict[str, SongData] = {
|
||||
"Save Yourself": SongData(2900765, "85-3", "Happy Otaku Pack Vol.20", True, 5, 7, 10),
|
||||
"Menace": SongData(2900766, "85-4", "Happy Otaku Pack Vol.20", True, 7, 9, 11),
|
||||
"Dangling": SongData(2900767, "85-5", "Happy Otaku Pack Vol.20", True, 6, 8, 10),
|
||||
"Inverted World": SongData(2900768, "86-0", "Aquaria Cruising Guide", True, 4, 6, 8),
|
||||
"Suito": SongData(2900769, "86-1", "Aquaria Cruising Guide", True, 6, 8, 11),
|
||||
"The Promised Land": SongData(2900770, "86-2", "Aquaria Cruising Guide", True, 4, 6, 9),
|
||||
"Alfheim's faith": SongData(2900771, "86-3", "Aquaria Cruising Guide", True, 6, 8, 11),
|
||||
"Heaven's Cage": SongData(2900772, "86-4", "Aquaria Cruising Guide", True, 5, 7, 10),
|
||||
"Broomstick adventure!": SongData(2900773, "86-5", "Aquaria Cruising Guide", True, 7, 9, 11),
|
||||
"Strong Nurse Buro-chan!": SongData(2900774, "43-61", "MD Plus Project", True, 5, 7, 9),
|
||||
"Cubism": SongData(2900775, "43-62", "MD Plus Project", False, 5, 7, 9),
|
||||
"Cubibibibism": SongData(2900776, "43-63", "MD Plus Project", False, 6, 8, 10),
|
||||
"LET'S TOAST!!": SongData(2900777, "43-64", "MD Plus Project", False, 6, 8, 10),
|
||||
"#YamiKawa": SongData(2900778, "43-65", "MD Plus Project", False, 5, 7, 10),
|
||||
"Rainy Step": SongData(2900779, "43-66", "MD Plus Project", False, 2, 5, 8),
|
||||
"OHOSHIKATSU": SongData(2900780, "43-67", "MD Plus Project", False, 5, 7, 10),
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ DATA_LOCATIONS = {
|
||||
"DexSanityFlag": (0x1A71, 19),
|
||||
"GameStatus": (0x1A84, 0x01),
|
||||
"Money": (0x141F, 3),
|
||||
"CurrentMap": (0x1436, 1),
|
||||
"ResetCheck": (0x0100, 4),
|
||||
# First and second Vermilion Gym trash can selection. Second is not used, so should always be 0.
|
||||
# First should never be above 0x0F. This is just before Event Flags.
|
||||
@@ -65,6 +66,7 @@ class PokemonRBClient(BizHawkClient):
|
||||
self.banking_command = None
|
||||
self.game_state = False
|
||||
self.last_death_link = 0
|
||||
self.current_map = 0
|
||||
|
||||
async def validate_rom(self, ctx):
|
||||
game_name = await read(ctx.bizhawk_ctx, [(0x134, 12, "ROM")])
|
||||
@@ -230,6 +232,10 @@ class PokemonRBClient(BizHawkClient):
|
||||
}])
|
||||
self.banking_command = None
|
||||
|
||||
if data["CurrentMap"][0] != self.current_map:
|
||||
await ctx.send_msgs([{"cmd": "Bounce", "slots": [ctx.slot], "data": {"currentMap": data["CurrentMap"][0]}}])
|
||||
self.current_map = data["CurrentMap"][0]
|
||||
|
||||
# VICTORY
|
||||
|
||||
if data["EventFlag"][280] & 1 and not ctx.finished_game:
|
||||
|
||||
@@ -15,7 +15,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
|
||||
|
||||
## Optional Software
|
||||
|
||||
- [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/coveleski/rb_tracker/releases/latest), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases)
|
||||
- [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/palex00/rb_tracker/releases/latest), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases)
|
||||
|
||||
|
||||
## Configuring BizHawk
|
||||
@@ -109,7 +109,7 @@ server uses password, type in the bottom textfield `/connect <address>:<port> [p
|
||||
|
||||
Pokémon Red and Blue has a fully functional map tracker that supports auto-tracking.
|
||||
|
||||
1. Download [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/coveleski/rb_tracker/releases/latest) and [PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
||||
1. Download [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/palex00/rb_tracker/releases/latest) and [PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
||||
2. Open PopTracker, and load the Pokémon Red and Blue pack.
|
||||
3. Click on the "AP" symbol at the top.
|
||||
4. Enter the AP address, slot name and password.
|
||||
|
||||
@@ -16,7 +16,7 @@ Al usar BizHawk, esta guía solo es aplicable en los sistemas de Windows y Linux
|
||||
|
||||
## Software Opcional
|
||||
|
||||
- [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/coveleski/rb_tracker/releases/latest), para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases)
|
||||
- [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/palex00/rb_tracker/releases/latest), para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases)
|
||||
|
||||
|
||||
## Configurando BizHawk
|
||||
@@ -114,7 +114,7 @@ presiona enter (si el servidor usa contraseña, escribe en el campo de texto inf
|
||||
|
||||
Pokémon Red and Blue tiene un mapa completamente funcional que soporta seguimiento automático.
|
||||
|
||||
1. Descarga el [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/coveleski/rb_tracker/releases/latest) y [PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
||||
1. Descarga el [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/palex00/rb_tracker/releases/latest) y [PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
||||
2. Abre PopTracker, y carga el pack de Pokémon Red and Blue.
|
||||
3. Haz clic en el símbolo "AP" en la parte superior.
|
||||
4. Ingresa la dirección de AP, nombre del slot y contraseña (si es que hay).
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
from typing import Dict, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
|
||||
|
||||
class RLItem(Item):
|
||||
game: str = "Rogue Legacy"
|
||||
|
||||
|
||||
class RLItemData(NamedTuple):
|
||||
category: str
|
||||
code: Optional[int] = None
|
||||
classification: ItemClassification = ItemClassification.filler
|
||||
max_quantity: int = 1
|
||||
weight: int = 1
|
||||
|
||||
|
||||
def get_items_by_category(category: str) -> Dict[str, RLItemData]:
|
||||
item_dict: Dict[str, RLItemData] = {}
|
||||
for name, data in item_table.items():
|
||||
if data.category == category:
|
||||
item_dict.setdefault(name, data)
|
||||
|
||||
return item_dict
|
||||
|
||||
|
||||
item_table: Dict[str, RLItemData] = {
|
||||
# Vendors
|
||||
"Blacksmith": RLItemData("Vendors", 90_000, ItemClassification.progression),
|
||||
"Enchantress": RLItemData("Vendors", 90_001, ItemClassification.progression),
|
||||
"Architect": RLItemData("Vendors", 90_002, ItemClassification.useful),
|
||||
|
||||
# Classes
|
||||
"Progressive Knights": RLItemData("Classes", 90_003, ItemClassification.useful, 2),
|
||||
"Progressive Mages": RLItemData("Classes", 90_004, ItemClassification.useful, 2),
|
||||
"Progressive Barbarians": RLItemData("Classes", 90_005, ItemClassification.useful, 2),
|
||||
"Progressive Knaves": RLItemData("Classes", 90_006, ItemClassification.useful, 2),
|
||||
"Progressive Shinobis": RLItemData("Classes", 90_007, ItemClassification.useful, 2),
|
||||
"Progressive Miners": RLItemData("Classes", 90_008, ItemClassification.useful, 2),
|
||||
"Progressive Liches": RLItemData("Classes", 90_009, ItemClassification.useful, 2),
|
||||
"Progressive Spellthieves": RLItemData("Classes", 90_010, ItemClassification.useful, 2),
|
||||
"Dragons": RLItemData("Classes", 90_096, ItemClassification.progression),
|
||||
"Traitors": RLItemData("Classes", 90_097, ItemClassification.useful),
|
||||
|
||||
# Skills
|
||||
"Health Up": RLItemData("Skills", 90_013, ItemClassification.progression_skip_balancing, 15),
|
||||
"Mana Up": RLItemData("Skills", 90_014, ItemClassification.progression_skip_balancing, 15),
|
||||
"Attack Up": RLItemData("Skills", 90_015, ItemClassification.progression_skip_balancing, 15),
|
||||
"Magic Damage Up": RLItemData("Skills", 90_016, ItemClassification.progression_skip_balancing, 15),
|
||||
"Armor Up": RLItemData("Skills", 90_017, ItemClassification.useful, 15),
|
||||
"Equip Up": RLItemData("Skills", 90_018, ItemClassification.useful, 5),
|
||||
"Crit Chance Up": RLItemData("Skills", 90_019, ItemClassification.useful, 5),
|
||||
"Crit Damage Up": RLItemData("Skills", 90_020, ItemClassification.useful, 5),
|
||||
"Down Strike Up": RLItemData("Skills", 90_021),
|
||||
"Gold Gain Up": RLItemData("Skills", 90_022),
|
||||
"Potion Efficiency Up": RLItemData("Skills", 90_023),
|
||||
"Invulnerability Time Up": RLItemData("Skills", 90_024),
|
||||
"Mana Cost Down": RLItemData("Skills", 90_025),
|
||||
"Death Defiance": RLItemData("Skills", 90_026, ItemClassification.useful),
|
||||
"Haggling": RLItemData("Skills", 90_027, ItemClassification.useful),
|
||||
"Randomize Children": RLItemData("Skills", 90_028, ItemClassification.useful),
|
||||
|
||||
# Blueprints
|
||||
"Progressive Blueprints": RLItemData("Blueprints", 90_055, ItemClassification.useful, 15),
|
||||
"Squire Blueprints": RLItemData("Blueprints", 90_040, ItemClassification.useful),
|
||||
"Silver Blueprints": RLItemData("Blueprints", 90_041, ItemClassification.useful),
|
||||
"Guardian Blueprints": RLItemData("Blueprints", 90_042, ItemClassification.useful),
|
||||
"Imperial Blueprints": RLItemData("Blueprints", 90_043, ItemClassification.useful),
|
||||
"Royal Blueprints": RLItemData("Blueprints", 90_044, ItemClassification.useful),
|
||||
"Knight Blueprints": RLItemData("Blueprints", 90_045, ItemClassification.useful),
|
||||
"Ranger Blueprints": RLItemData("Blueprints", 90_046, ItemClassification.useful),
|
||||
"Sky Blueprints": RLItemData("Blueprints", 90_047, ItemClassification.useful),
|
||||
"Dragon Blueprints": RLItemData("Blueprints", 90_048, ItemClassification.useful),
|
||||
"Slayer Blueprints": RLItemData("Blueprints", 90_049, ItemClassification.useful),
|
||||
"Blood Blueprints": RLItemData("Blueprints", 90_050, ItemClassification.useful),
|
||||
"Sage Blueprints": RLItemData("Blueprints", 90_051, ItemClassification.useful),
|
||||
"Retribution Blueprints": RLItemData("Blueprints", 90_052, ItemClassification.useful),
|
||||
"Holy Blueprints": RLItemData("Blueprints", 90_053, ItemClassification.useful),
|
||||
"Dark Blueprints": RLItemData("Blueprints", 90_054, ItemClassification.useful),
|
||||
|
||||
# Runes
|
||||
"Vault Runes": RLItemData("Runes", 90_060, ItemClassification.progression),
|
||||
"Sprint Runes": RLItemData("Runes", 90_061, ItemClassification.progression),
|
||||
"Vampire Runes": RLItemData("Runes", 90_062, ItemClassification.useful),
|
||||
"Sky Runes": RLItemData("Runes", 90_063, ItemClassification.progression),
|
||||
"Siphon Runes": RLItemData("Runes", 90_064, ItemClassification.useful),
|
||||
"Retaliation Runes": RLItemData("Runes", 90_065),
|
||||
"Bounty Runes": RLItemData("Runes", 90_066),
|
||||
"Haste Runes": RLItemData("Runes", 90_067),
|
||||
"Curse Runes": RLItemData("Runes", 90_068),
|
||||
"Grace Runes": RLItemData("Runes", 90_069),
|
||||
"Balance Runes": RLItemData("Runes", 90_070, ItemClassification.useful),
|
||||
|
||||
# Junk
|
||||
"Triple Stat Increase": RLItemData("Filler", 90_030, weight=6),
|
||||
"1000 Gold": RLItemData("Filler", 90_031, weight=3),
|
||||
"3000 Gold": RLItemData("Filler", 90_032, weight=2),
|
||||
"5000 Gold": RLItemData("Filler", 90_033, weight=1),
|
||||
}
|
||||
|
||||
event_item_table: Dict[str, RLItemData] = {
|
||||
"Defeat Khidr": RLItemData("Event", classification=ItemClassification.progression),
|
||||
"Defeat Alexander": RLItemData("Event", classification=ItemClassification.progression),
|
||||
"Defeat Ponce de Leon": RLItemData("Event", classification=ItemClassification.progression),
|
||||
"Defeat Herodotus": RLItemData("Event", classification=ItemClassification.progression),
|
||||
"Defeat Neo Khidr": RLItemData("Event", classification=ItemClassification.progression),
|
||||
"Defeat Alexander IV": RLItemData("Event", classification=ItemClassification.progression),
|
||||
"Defeat Ponce de Freon": RLItemData("Event", classification=ItemClassification.progression),
|
||||
"Defeat Astrodotus": RLItemData("Event", classification=ItemClassification.progression),
|
||||
"Defeat The Fountain": RLItemData("Event", classification=ItemClassification.progression),
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
from typing import Dict, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Location
|
||||
|
||||
|
||||
class RLLocation(Location):
|
||||
game: str = "Rogue Legacy"
|
||||
|
||||
|
||||
class RLLocationData(NamedTuple):
|
||||
category: str
|
||||
code: Optional[int] = None
|
||||
|
||||
|
||||
def get_locations_by_category(category: str) -> Dict[str, RLLocationData]:
|
||||
location_dict: Dict[str, RLLocationData] = {}
|
||||
for name, data in location_table.items():
|
||||
if data.category == category:
|
||||
location_dict.setdefault(name, data)
|
||||
|
||||
return location_dict
|
||||
|
||||
|
||||
location_table: Dict[str, RLLocationData] = {
|
||||
# Manor Renovation
|
||||
"Manor - Ground Road": RLLocationData("Manor", 91_000),
|
||||
"Manor - Main Base": RLLocationData("Manor", 91_001),
|
||||
"Manor - Main Bottom Window": RLLocationData("Manor", 91_002),
|
||||
"Manor - Main Top Window": RLLocationData("Manor", 91_003),
|
||||
"Manor - Main Rooftop": RLLocationData("Manor", 91_004),
|
||||
"Manor - Left Wing Base": RLLocationData("Manor", 91_005),
|
||||
"Manor - Left Wing Window": RLLocationData("Manor", 91_006),
|
||||
"Manor - Left Wing Rooftop": RLLocationData("Manor", 91_007),
|
||||
"Manor - Left Big Base": RLLocationData("Manor", 91_008),
|
||||
"Manor - Left Big Upper 1": RLLocationData("Manor", 91_009),
|
||||
"Manor - Left Big Upper 2": RLLocationData("Manor", 91_010),
|
||||
"Manor - Left Big Windows": RLLocationData("Manor", 91_011),
|
||||
"Manor - Left Big Rooftop": RLLocationData("Manor", 91_012),
|
||||
"Manor - Left Far Base": RLLocationData("Manor", 91_013),
|
||||
"Manor - Left Far Roof": RLLocationData("Manor", 91_014),
|
||||
"Manor - Left Extension": RLLocationData("Manor", 91_015),
|
||||
"Manor - Left Tree 1": RLLocationData("Manor", 91_016),
|
||||
"Manor - Left Tree 2": RLLocationData("Manor", 91_017),
|
||||
"Manor - Right Wing Base": RLLocationData("Manor", 91_018),
|
||||
"Manor - Right Wing Window": RLLocationData("Manor", 91_019),
|
||||
"Manor - Right Wing Rooftop": RLLocationData("Manor", 91_020),
|
||||
"Manor - Right Big Base": RLLocationData("Manor", 91_021),
|
||||
"Manor - Right Big Upper": RLLocationData("Manor", 91_022),
|
||||
"Manor - Right Big Rooftop": RLLocationData("Manor", 91_023),
|
||||
"Manor - Right High Base": RLLocationData("Manor", 91_024),
|
||||
"Manor - Right High Upper": RLLocationData("Manor", 91_025),
|
||||
"Manor - Right High Tower": RLLocationData("Manor", 91_026),
|
||||
"Manor - Right Extension": RLLocationData("Manor", 91_027),
|
||||
"Manor - Right Tree": RLLocationData("Manor", 91_028),
|
||||
"Manor - Observatory Base": RLLocationData("Manor", 91_029),
|
||||
"Manor - Observatory Telescope": RLLocationData("Manor", 91_030),
|
||||
|
||||
# Boss Rewards
|
||||
"Castle Hamson Boss Reward": RLLocationData("Boss", 91_100),
|
||||
"Forest Abkhazia Boss Reward": RLLocationData("Boss", 91_102),
|
||||
"The Maya Boss Reward": RLLocationData("Boss", 91_104),
|
||||
"Land of Darkness Boss Reward": RLLocationData("Boss", 91_106),
|
||||
|
||||
# Special Locations
|
||||
"Jukebox": RLLocationData("Special", 91_200),
|
||||
"Painting": RLLocationData("Special", 91_201),
|
||||
"Cheapskate Elf's Game": RLLocationData("Special", 91_202),
|
||||
"Carnival": RLLocationData("Special", 91_203),
|
||||
|
||||
# Diaries
|
||||
**{f"Diary {i+1}": RLLocationData("Diary", 91_300 + i) for i in range(0, 25)},
|
||||
|
||||
# Chests
|
||||
**{f"Castle Hamson - Chest {i+1}": RLLocationData("Chests", 91_600 + i) for i in range(0, 50)},
|
||||
**{f"Forest Abkhazia - Chest {i+1}": RLLocationData("Chests", 91_700 + i) for i in range(0, 50)},
|
||||
**{f"The Maya - Chest {i+1}": RLLocationData("Chests", 91_800 + i) for i in range(0, 50)},
|
||||
**{f"Land of Darkness - Chest {i+1}": RLLocationData("Chests", 91_900 + i) for i in range(0, 50)},
|
||||
**{f"Chest {i+1}": RLLocationData("Chests", 92_000 + i) for i in range(0, 200)},
|
||||
|
||||
# Fairy Chests
|
||||
**{f"Castle Hamson - Fairy Chest {i+1}": RLLocationData("Fairies", 91_400 + i) for i in range(0, 15)},
|
||||
**{f"Forest Abkhazia - Fairy Chest {i+1}": RLLocationData("Fairies", 91_450 + i) for i in range(0, 15)},
|
||||
**{f"The Maya - Fairy Chest {i+1}": RLLocationData("Fairies", 91_500 + i) for i in range(0, 15)},
|
||||
**{f"Land of Darkness - Fairy Chest {i+1}": RLLocationData("Fairies", 91_550 + i) for i in range(0, 15)},
|
||||
**{f"Fairy Chest {i+1}": RLLocationData("Fairies", 92_200 + i) for i in range(0, 60)},
|
||||
}
|
||||
|
||||
event_location_table: Dict[str, RLLocationData] = {
|
||||
"Castle Hamson Boss Room": RLLocationData("Event"),
|
||||
"Forest Abkhazia Boss Room": RLLocationData("Event"),
|
||||
"The Maya Boss Room": RLLocationData("Event"),
|
||||
"Land of Darkness Boss Room": RLLocationData("Event"),
|
||||
"Fountain Room": RLLocationData("Event"),
|
||||
}
|
||||
@@ -1,387 +0,0 @@
|
||||
from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionSet, PerGameCommonOptions
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class StartingGender(Choice):
|
||||
"""
|
||||
Determines the gender of your initial 'Sir Lee' character.
|
||||
"""
|
||||
display_name = "Starting Gender"
|
||||
option_sir = 0
|
||||
option_lady = 1
|
||||
alias_male = 0
|
||||
alias_female = 1
|
||||
default = "random"
|
||||
|
||||
|
||||
class StartingClass(Choice):
|
||||
"""
|
||||
Determines the starting class of your initial 'Sir Lee' character.
|
||||
"""
|
||||
display_name = "Starting Class"
|
||||
option_knight = 0
|
||||
option_mage = 1
|
||||
option_barbarian = 2
|
||||
option_knave = 3
|
||||
option_shinobi = 4
|
||||
option_miner = 5
|
||||
option_spellthief = 6
|
||||
option_lich = 7
|
||||
default = 0
|
||||
|
||||
|
||||
class NewGamePlus(Choice):
|
||||
"""
|
||||
Puts the castle in new game plus mode which vastly increases enemy level, but increases gold gain by 50%. Not
|
||||
recommended for those inexperienced to Rogue Legacy!
|
||||
"""
|
||||
display_name = "New Game Plus"
|
||||
option_normal = 0
|
||||
option_new_game_plus = 1
|
||||
option_new_game_plus_2 = 2
|
||||
alias_hard = 1
|
||||
alias_brutal = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class LevelScaling(Range):
|
||||
"""
|
||||
A percentage modifier for scaling enemy level as you continue throughout the castle. 100 means enemies will have
|
||||
100% level scaling (normal). Setting this too high will result in enemies with absurdly high levels, you have been
|
||||
warned.
|
||||
"""
|
||||
display_name = "Enemy Level Scaling Percentage"
|
||||
range_start = 1
|
||||
range_end = 300
|
||||
default = 100
|
||||
|
||||
|
||||
class FairyChestsPerZone(Range):
|
||||
"""
|
||||
Determines the number of Fairy Chests in a given zone that contain items. After these have been checked, only stat
|
||||
bonuses can be found in Fairy Chests.
|
||||
"""
|
||||
display_name = "Fairy Chests Per Zone"
|
||||
range_start = 0
|
||||
range_end = 15
|
||||
default = 1
|
||||
|
||||
|
||||
class ChestsPerZone(Range):
|
||||
"""
|
||||
Determines the number of Non-Fairy Chests in a given zone that contain items. After these have been checked, only
|
||||
gold or stat bonuses can be found in Chests.
|
||||
"""
|
||||
display_name = "Chests Per Zone"
|
||||
range_start = 20
|
||||
range_end = 50
|
||||
default = 20
|
||||
|
||||
|
||||
class UniversalFairyChests(Toggle):
|
||||
"""
|
||||
Determines if fairy chests should be combined into one pool instead of per zone, similar to Risk of Rain 2.
|
||||
"""
|
||||
display_name = "Universal Fairy Chests"
|
||||
|
||||
|
||||
class UniversalChests(Toggle):
|
||||
"""
|
||||
Determines if non-fairy chests should be combined into one pool instead of per zone, similar to Risk of Rain 2.
|
||||
"""
|
||||
display_name = "Universal Non-Fairy Chests"
|
||||
|
||||
|
||||
class Vendors(Choice):
|
||||
"""
|
||||
Determines where to place the Blacksmith and Enchantress unlocks in logic (or start with them unlocked).
|
||||
"""
|
||||
display_name = "Vendors"
|
||||
option_start_unlocked = 0
|
||||
option_early = 1
|
||||
option_normal = 2
|
||||
option_anywhere = 3
|
||||
default = 1
|
||||
|
||||
|
||||
class Architect(Choice):
|
||||
"""
|
||||
Determines where the Architect sits in the item pool.
|
||||
"""
|
||||
display_name = "Architect"
|
||||
option_start_unlocked = 0
|
||||
option_early = 1
|
||||
option_anywhere = 2
|
||||
option_disabled = 3
|
||||
alias_normal = 2
|
||||
default = 2
|
||||
|
||||
|
||||
class ArchitectFee(Range):
|
||||
"""
|
||||
Determines how large of a percentage the architect takes from the player when utilizing his services. 100 means he
|
||||
takes all your gold. 0 means his services are free.
|
||||
"""
|
||||
display_name = "Architect Fee Percentage"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 40
|
||||
|
||||
|
||||
class DisableCharon(Toggle):
|
||||
"""
|
||||
Prevents Charon from taking your money when you re-enter the castle. Also removes Haggling from the Item Pool.
|
||||
"""
|
||||
display_name = "Disable Charon"
|
||||
|
||||
|
||||
class RequirePurchasing(DefaultOnToggle):
|
||||
"""
|
||||
Determines where you will be required to purchase equipment and runes from the Blacksmith and Enchantress before
|
||||
equipping them. If you disable require purchasing, Manor Renovations are scaled to take this into account.
|
||||
"""
|
||||
display_name = "Require Purchasing"
|
||||
|
||||
|
||||
class ProgressiveBlueprints(Toggle):
|
||||
"""
|
||||
Instead of shuffling blueprints randomly into the pool, blueprint unlocks are progressively unlocked. You would get
|
||||
Squire first, then Knight, etc., until finally Dark.
|
||||
"""
|
||||
display_name = "Progressive Blueprints"
|
||||
|
||||
|
||||
class GoldGainMultiplier(Choice):
|
||||
"""
|
||||
Adjusts the multiplier for gaining gold from all sources.
|
||||
"""
|
||||
display_name = "Gold Gain Multiplier"
|
||||
option_normal = 0
|
||||
option_quarter = 1
|
||||
option_half = 2
|
||||
option_double = 3
|
||||
option_quadruple = 4
|
||||
default = 0
|
||||
|
||||
|
||||
class NumberOfChildren(Range):
|
||||
"""
|
||||
Determines the number of offspring you can choose from on the lineage screen after a death.
|
||||
"""
|
||||
display_name = "Number of Children"
|
||||
range_start = 1
|
||||
range_end = 5
|
||||
default = 3
|
||||
|
||||
|
||||
class AdditionalLadyNames(OptionSet):
|
||||
"""
|
||||
Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list
|
||||
of names your children can have. The first value will also be your initial character's name depending on Starting
|
||||
Gender.
|
||||
"""
|
||||
display_name = "Additional Lady Names"
|
||||
|
||||
class AdditionalSirNames(OptionSet):
|
||||
"""
|
||||
Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list
|
||||
of names your children can have. The first value will also be your initial character's name depending on Starting
|
||||
Gender.
|
||||
"""
|
||||
display_name = "Additional Sir Names"
|
||||
|
||||
|
||||
class AllowDefaultNames(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the default names defined in the vanilla game are allowed to be used. Warning: Your world will not
|
||||
generate if the number of Additional Names defined is less than the Number of Children value.
|
||||
"""
|
||||
display_name = "Allow Default Names"
|
||||
|
||||
|
||||
class CastleScaling(Range):
|
||||
"""
|
||||
Adjusts the scaling factor for how big a castle can be. Larger castles scale enemies quicker and also take longer
|
||||
to generate. 100 means normal castle size.
|
||||
"""
|
||||
display_name = "Castle Size Scaling Percentage"
|
||||
range_start = 50
|
||||
range_end = 300
|
||||
default = 100
|
||||
|
||||
|
||||
class ChallengeBossKhidr(Choice):
|
||||
"""
|
||||
Determines if Neo Khidr replaces Khidr in their boss room.
|
||||
"""
|
||||
display_name = "Khidr"
|
||||
option_vanilla = 0
|
||||
option_challenge = 1
|
||||
default = 0
|
||||
|
||||
|
||||
class ChallengeBossAlexander(Choice):
|
||||
"""
|
||||
Determines if Alexander the IV replaces Alexander in their boss room.
|
||||
"""
|
||||
display_name = "Alexander"
|
||||
option_vanilla = 0
|
||||
option_challenge = 1
|
||||
default = 0
|
||||
|
||||
|
||||
class ChallengeBossLeon(Choice):
|
||||
"""
|
||||
Determines if Ponce de Freon replaces Ponce de Leon in their boss room.
|
||||
"""
|
||||
display_name = "Ponce de Leon"
|
||||
option_vanilla = 0
|
||||
option_challenge = 1
|
||||
default = 0
|
||||
|
||||
|
||||
class ChallengeBossHerodotus(Choice):
|
||||
"""
|
||||
Determines if Astrodotus replaces Herodotus in their boss room.
|
||||
"""
|
||||
display_name = "Herodotus"
|
||||
option_vanilla = 0
|
||||
option_challenge = 1
|
||||
default = 0
|
||||
|
||||
|
||||
class HealthUpPool(Range):
|
||||
"""
|
||||
Determines the number of Health Ups in the item pool.
|
||||
"""
|
||||
display_name = "Health Up Pool"
|
||||
range_start = 0
|
||||
range_end = 15
|
||||
default = 15
|
||||
|
||||
|
||||
class ManaUpPool(Range):
|
||||
"""
|
||||
Determines the number of Mana Ups in the item pool.
|
||||
"""
|
||||
display_name = "Mana Up Pool"
|
||||
range_start = 0
|
||||
range_end = 15
|
||||
default = 15
|
||||
|
||||
|
||||
class AttackUpPool(Range):
|
||||
"""
|
||||
Determines the number of Attack Ups in the item pool.
|
||||
"""
|
||||
display_name = "Attack Up Pool"
|
||||
range_start = 0
|
||||
range_end = 15
|
||||
default = 15
|
||||
|
||||
|
||||
class MagicDamageUpPool(Range):
|
||||
"""
|
||||
Determines the number of Magic Damage Ups in the item pool.
|
||||
"""
|
||||
display_name = "Magic Damage Up Pool"
|
||||
range_start = 0
|
||||
range_end = 15
|
||||
default = 15
|
||||
|
||||
|
||||
class ArmorUpPool(Range):
|
||||
"""
|
||||
Determines the number of Armor Ups in the item pool.
|
||||
"""
|
||||
display_name = "Armor Up Pool"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 10
|
||||
|
||||
|
||||
class EquipUpPool(Range):
|
||||
"""
|
||||
Determines the number of Equip Ups in the item pool.
|
||||
"""
|
||||
display_name = "Equip Up Pool"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 10
|
||||
|
||||
|
||||
class CritChanceUpPool(Range):
|
||||
"""
|
||||
Determines the number of Crit Chance Ups in the item pool.
|
||||
"""
|
||||
display_name = "Crit Chance Up Pool"
|
||||
range_start = 0
|
||||
range_end = 5
|
||||
default = 5
|
||||
|
||||
|
||||
class CritDamageUpPool(Range):
|
||||
"""
|
||||
Determines the number of Crit Damage Ups in the item pool.
|
||||
"""
|
||||
display_name = "Crit Damage Up Pool"
|
||||
range_start = 0
|
||||
range_end = 5
|
||||
default = 5
|
||||
|
||||
|
||||
class FreeDiaryOnGeneration(DefaultOnToggle):
|
||||
"""
|
||||
Allows the player to get a free diary check every time they regenerate the castle in the starting room.
|
||||
"""
|
||||
display_name = "Free Diary On Generation"
|
||||
|
||||
|
||||
class AvailableClasses(OptionSet):
|
||||
"""
|
||||
List of classes that will be in the item pool to find. The upgraded form of the class will be added with it.
|
||||
The upgraded form of your starting class will be available regardless.
|
||||
"""
|
||||
display_name = "Available Classes"
|
||||
default = frozenset(
|
||||
{"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"}
|
||||
)
|
||||
valid_keys = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class RLOptions(PerGameCommonOptions):
|
||||
starting_gender: StartingGender
|
||||
starting_class: StartingClass
|
||||
available_classes: AvailableClasses
|
||||
new_game_plus: NewGamePlus
|
||||
fairy_chests_per_zone: FairyChestsPerZone
|
||||
chests_per_zone: ChestsPerZone
|
||||
universal_fairy_chests: UniversalFairyChests
|
||||
universal_chests: UniversalChests
|
||||
vendors: Vendors
|
||||
architect: Architect
|
||||
architect_fee: ArchitectFee
|
||||
disable_charon: DisableCharon
|
||||
require_purchasing: RequirePurchasing
|
||||
progressive_blueprints: ProgressiveBlueprints
|
||||
gold_gain_multiplier: GoldGainMultiplier
|
||||
number_of_children: NumberOfChildren
|
||||
free_diary_on_generation: FreeDiaryOnGeneration
|
||||
khidr: ChallengeBossKhidr
|
||||
alexander: ChallengeBossAlexander
|
||||
leon: ChallengeBossLeon
|
||||
herodotus: ChallengeBossHerodotus
|
||||
health_pool: HealthUpPool
|
||||
mana_pool: ManaUpPool
|
||||
attack_pool: AttackUpPool
|
||||
magic_damage_pool: MagicDamageUpPool
|
||||
armor_pool: ArmorUpPool
|
||||
equip_pool: EquipUpPool
|
||||
crit_chance_pool: CritChanceUpPool
|
||||
crit_damage_pool: CritDamageUpPool
|
||||
allow_default_names: AllowDefaultNames
|
||||
additional_lady_names: AdditionalLadyNames
|
||||
additional_sir_names: AdditionalSirNames
|
||||
death_link: DeathLink
|
||||
@@ -1,61 +0,0 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from .Options import Architect, GoldGainMultiplier, Vendors
|
||||
|
||||
rl_options_presets: Dict[str, Dict[str, Any]] = {
|
||||
# Example preset using only literal values.
|
||||
"Unknown Fate": {
|
||||
"progression_balancing": "random",
|
||||
"accessibility": "random",
|
||||
"starting_gender": "random",
|
||||
"starting_class": "random",
|
||||
"new_game_plus": "random",
|
||||
"fairy_chests_per_zone": "random",
|
||||
"chests_per_zone": "random",
|
||||
"universal_fairy_chests": "random",
|
||||
"universal_chests": "random",
|
||||
"vendors": "random",
|
||||
"architect": "random",
|
||||
"architect_fee": "random",
|
||||
"disable_charon": "random",
|
||||
"require_purchasing": "random",
|
||||
"progressive_blueprints": "random",
|
||||
"gold_gain_multiplier": "random",
|
||||
"number_of_children": "random",
|
||||
"free_diary_on_generation": "random",
|
||||
"khidr": "random",
|
||||
"alexander": "random",
|
||||
"leon": "random",
|
||||
"herodotus": "random",
|
||||
"health_pool": "random",
|
||||
"mana_pool": "random",
|
||||
"attack_pool": "random",
|
||||
"magic_damage_pool": "random",
|
||||
"armor_pool": "random",
|
||||
"equip_pool": "random",
|
||||
"crit_chance_pool": "random",
|
||||
"crit_damage_pool": "random",
|
||||
"allow_default_names": True,
|
||||
"death_link": "random",
|
||||
},
|
||||
# A preset I actually use, using some literal values and some from the option itself.
|
||||
"Limited Potential": {
|
||||
"progression_balancing": "disabled",
|
||||
"fairy_chests_per_zone": 2,
|
||||
"starting_class": "random",
|
||||
"chests_per_zone": 30,
|
||||
"vendors": Vendors.option_normal,
|
||||
"architect": Architect.option_disabled,
|
||||
"gold_gain_multiplier": GoldGainMultiplier.option_half,
|
||||
"number_of_children": 2,
|
||||
"free_diary_on_generation": False,
|
||||
"health_pool": 10,
|
||||
"mana_pool": 10,
|
||||
"attack_pool": 10,
|
||||
"magic_damage_pool": 10,
|
||||
"armor_pool": 5,
|
||||
"equip_pool": 10,
|
||||
"crit_chance_pool": 5,
|
||||
"crit_damage_pool": 5,
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import MultiWorld, Region, Entrance
|
||||
from .Locations import RLLocation, location_table, get_locations_by_category
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import RLWorld
|
||||
|
||||
|
||||
class RLRegionData(NamedTuple):
|
||||
locations: Optional[List[str]]
|
||||
region_exits: Optional[List[str]]
|
||||
|
||||
|
||||
def create_regions(world: "RLWorld"):
|
||||
regions: Dict[str, RLRegionData] = {
|
||||
"Menu": RLRegionData(None, ["Castle Hamson"]),
|
||||
"The Manor": RLRegionData([], []),
|
||||
"Castle Hamson": RLRegionData([], ["Forest Abkhazia", "The Maya", "Land of Darkness",
|
||||
"The Fountain Room", "The Manor"]),
|
||||
"Forest Abkhazia": RLRegionData([], []),
|
||||
"The Maya": RLRegionData([], []),
|
||||
"Land of Darkness": RLRegionData([], []),
|
||||
"The Fountain Room": RLRegionData([], None),
|
||||
}
|
||||
|
||||
# Artificially stagger diary spheres for progression.
|
||||
for diary in range(0, 25):
|
||||
region: str
|
||||
if 0 <= diary < 6:
|
||||
region = "Castle Hamson"
|
||||
elif 6 <= diary < 12:
|
||||
region = "Forest Abkhazia"
|
||||
elif 12 <= diary < 18:
|
||||
region = "The Maya"
|
||||
elif 18 <= diary < 24:
|
||||
region = "Land of Darkness"
|
||||
else:
|
||||
region = "The Fountain Room"
|
||||
regions[region].locations.append(f"Diary {diary + 1}")
|
||||
|
||||
# Manor & Special
|
||||
for manor in get_locations_by_category("Manor").keys():
|
||||
regions["The Manor"].locations.append(manor)
|
||||
for special in get_locations_by_category("Special").keys():
|
||||
regions["Castle Hamson"].locations.append(special)
|
||||
|
||||
# Boss Rewards
|
||||
regions["Castle Hamson"].locations.append("Castle Hamson Boss Reward")
|
||||
regions["Forest Abkhazia"].locations.append("Forest Abkhazia Boss Reward")
|
||||
regions["The Maya"].locations.append("The Maya Boss Reward")
|
||||
regions["Land of Darkness"].locations.append("Land of Darkness Boss Reward")
|
||||
|
||||
# Events
|
||||
regions["Castle Hamson"].locations.append("Castle Hamson Boss Room")
|
||||
regions["Forest Abkhazia"].locations.append("Forest Abkhazia Boss Room")
|
||||
regions["The Maya"].locations.append("The Maya Boss Room")
|
||||
regions["Land of Darkness"].locations.append("Land of Darkness Boss Room")
|
||||
regions["The Fountain Room"].locations.append("Fountain Room")
|
||||
|
||||
# Chests
|
||||
chests = int(world.options.chests_per_zone)
|
||||
for i in range(0, chests):
|
||||
if world.options.universal_chests:
|
||||
regions["Castle Hamson"].locations.append(f"Chest {i + 1}")
|
||||
regions["Forest Abkhazia"].locations.append(f"Chest {i + 1 + chests}")
|
||||
regions["The Maya"].locations.append(f"Chest {i + 1 + (chests * 2)}")
|
||||
regions["Land of Darkness"].locations.append(f"Chest {i + 1 + (chests * 3)}")
|
||||
else:
|
||||
regions["Castle Hamson"].locations.append(f"Castle Hamson - Chest {i + 1}")
|
||||
regions["Forest Abkhazia"].locations.append(f"Forest Abkhazia - Chest {i + 1}")
|
||||
regions["The Maya"].locations.append(f"The Maya - Chest {i + 1}")
|
||||
regions["Land of Darkness"].locations.append(f"Land of Darkness - Chest {i + 1}")
|
||||
|
||||
# Fairy Chests
|
||||
chests = int(world.options.fairy_chests_per_zone)
|
||||
for i in range(0, chests):
|
||||
if world.options.universal_fairy_chests:
|
||||
regions["Castle Hamson"].locations.append(f"Fairy Chest {i + 1}")
|
||||
regions["Forest Abkhazia"].locations.append(f"Fairy Chest {i + 1 + chests}")
|
||||
regions["The Maya"].locations.append(f"Fairy Chest {i + 1 + (chests * 2)}")
|
||||
regions["Land of Darkness"].locations.append(f"Fairy Chest {i + 1 + (chests * 3)}")
|
||||
else:
|
||||
regions["Castle Hamson"].locations.append(f"Castle Hamson - Fairy Chest {i + 1}")
|
||||
regions["Forest Abkhazia"].locations.append(f"Forest Abkhazia - Fairy Chest {i + 1}")
|
||||
regions["The Maya"].locations.append(f"The Maya - Fairy Chest {i + 1}")
|
||||
regions["Land of Darkness"].locations.append(f"Land of Darkness - Fairy Chest {i + 1}")
|
||||
|
||||
# Set up the regions correctly.
|
||||
for name, data in regions.items():
|
||||
world.multiworld.regions.append(create_region(world.multiworld, world.player, name, data))
|
||||
|
||||
world.get_entrance("Castle Hamson").connect(world.get_region("Castle Hamson"))
|
||||
world.get_entrance("The Manor").connect(world.get_region("The Manor"))
|
||||
world.get_entrance("Forest Abkhazia").connect(world.get_region("Forest Abkhazia"))
|
||||
world.get_entrance("The Maya").connect(world.get_region("The Maya"))
|
||||
world.get_entrance("Land of Darkness").connect(world.get_region("Land of Darkness"))
|
||||
world.get_entrance("The Fountain Room").connect(world.get_region("The Fountain Room"))
|
||||
|
||||
|
||||
def create_region(multiworld: MultiWorld, player: int, name: str, data: RLRegionData):
|
||||
region = Region(name, player, multiworld)
|
||||
if data.locations:
|
||||
for loc_name in data.locations:
|
||||
loc_data = location_table.get(loc_name)
|
||||
location = RLLocation(player, loc_name, loc_data.code if loc_data else None, region)
|
||||
region.locations.append(location)
|
||||
|
||||
if data.region_exits:
|
||||
for exit in data.region_exits:
|
||||
entrance = Entrance(player, exit, region)
|
||||
region.exits.append(entrance)
|
||||
|
||||
return region
|
||||
@@ -1,117 +0,0 @@
|
||||
from BaseClasses import CollectionState
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import RLWorld
|
||||
|
||||
|
||||
def get_upgrade_total(world: "RLWorld") -> int:
|
||||
return int(world.options.health_pool) + int(world.options.mana_pool) + \
|
||||
int(world.options.attack_pool) + int(world.options.magic_damage_pool)
|
||||
|
||||
|
||||
def get_upgrade_count(state: CollectionState, player: int) -> int:
|
||||
return state.count("Health Up", player) + state.count("Mana Up", player) + \
|
||||
state.count("Attack Up", player) + state.count("Magic Damage Up", player)
|
||||
|
||||
|
||||
def has_vendors(state: CollectionState, player: int) -> bool:
|
||||
return state.has_all({"Blacksmith", "Enchantress"}, player)
|
||||
|
||||
|
||||
def has_upgrade_amount(state: CollectionState, player: int, amount: int) -> bool:
|
||||
return get_upgrade_count(state, player) >= amount
|
||||
|
||||
|
||||
def has_upgrades_percentage(state: CollectionState, world: "RLWorld", percentage: float) -> bool:
|
||||
return has_upgrade_amount(state, world.player, round(get_upgrade_total(world) * (percentage / 100)))
|
||||
|
||||
|
||||
def has_movement_rune(state: CollectionState, player: int) -> bool:
|
||||
return state.has("Vault Runes", player) or state.has("Sprint Runes", player) or state.has("Sky Runes", player)
|
||||
|
||||
|
||||
def has_fairy_progression(state: CollectionState, player: int) -> bool:
|
||||
return state.has("Dragons", player) or (state.has("Enchantress", player) and has_movement_rune(state, player))
|
||||
|
||||
|
||||
def has_defeated_castle(state: CollectionState, player: int) -> bool:
|
||||
return state.has("Defeat Khidr", player) or state.has("Defeat Neo Khidr", player)
|
||||
|
||||
|
||||
def has_defeated_forest(state: CollectionState, player: int) -> bool:
|
||||
return state.has("Defeat Alexander", player) or state.has("Defeat Alexander IV", player)
|
||||
|
||||
|
||||
def has_defeated_tower(state: CollectionState, player: int) -> bool:
|
||||
return state.has("Defeat Ponce de Leon", player) or state.has("Defeat Ponce de Freon", player)
|
||||
|
||||
|
||||
def has_defeated_dungeon(state: CollectionState, player: int) -> bool:
|
||||
return state.has("Defeat Herodotus", player) or state.has("Defeat Astrodotus", player)
|
||||
|
||||
|
||||
def set_rules(world: "RLWorld", player: int):
|
||||
# If 'vendors' are 'normal', then expect it to show up in the first half(ish) of the spheres.
|
||||
if world.options.vendors == "normal":
|
||||
world.get_location("Forest Abkhazia Boss Reward").access_rule = \
|
||||
lambda state: has_vendors(state, player)
|
||||
|
||||
# Gate each manor location so everything isn't dumped into sphere 1.
|
||||
manor_rules = {
|
||||
"Defeat Khidr" if world.options.khidr == "vanilla" else "Defeat Neo Khidr": [
|
||||
"Manor - Left Wing Window",
|
||||
"Manor - Left Wing Rooftop",
|
||||
"Manor - Right Wing Window",
|
||||
"Manor - Right Wing Rooftop",
|
||||
"Manor - Left Big Base",
|
||||
"Manor - Right Big Base",
|
||||
"Manor - Left Tree 1",
|
||||
"Manor - Left Tree 2",
|
||||
"Manor - Right Tree",
|
||||
],
|
||||
"Defeat Alexander" if world.options.alexander == "vanilla" else "Defeat Alexander IV": [
|
||||
"Manor - Left Big Upper 1",
|
||||
"Manor - Left Big Upper 2",
|
||||
"Manor - Left Big Windows",
|
||||
"Manor - Left Big Rooftop",
|
||||
"Manor - Left Far Base",
|
||||
"Manor - Left Far Roof",
|
||||
"Manor - Left Extension",
|
||||
"Manor - Right Big Upper",
|
||||
"Manor - Right Big Rooftop",
|
||||
"Manor - Right Extension",
|
||||
],
|
||||
"Defeat Ponce de Leon" if world.options.leon == "vanilla" else "Defeat Ponce de Freon": [
|
||||
"Manor - Right High Base",
|
||||
"Manor - Right High Upper",
|
||||
"Manor - Right High Tower",
|
||||
"Manor - Observatory Base",
|
||||
"Manor - Observatory Telescope",
|
||||
]
|
||||
}
|
||||
|
||||
# Set rules for manor locations.
|
||||
for event, locations in manor_rules.items():
|
||||
for location in locations:
|
||||
world.get_location(location).access_rule = lambda state: state.has(event, player)
|
||||
|
||||
# Set rules for fairy chests to decrease headache of expectation to find non-movement fairy chests.
|
||||
for fairy_location in [location for location in world.multiworld.get_locations(player) if "Fairy" in location.name]:
|
||||
fairy_location.access_rule = lambda state: has_fairy_progression(state, player)
|
||||
|
||||
# Region rules.
|
||||
world.get_entrance("Forest Abkhazia").access_rule = \
|
||||
lambda state: has_upgrades_percentage(state, world, 12.5) and has_defeated_castle(state, player)
|
||||
|
||||
world.get_entrance("The Maya").access_rule = \
|
||||
lambda state: has_upgrades_percentage(state, world, 25) and has_defeated_forest(state, player)
|
||||
|
||||
world.get_entrance("Land of Darkness").access_rule = \
|
||||
lambda state: has_upgrades_percentage(state, world, 37.5) and has_defeated_tower(state, player)
|
||||
|
||||
world.get_entrance("The Fountain Room").access_rule = \
|
||||
lambda state: has_upgrades_percentage(state, world, 50) and has_defeated_dungeon(state, player)
|
||||
|
||||
# Win condition.
|
||||
world.multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player)
|
||||
@@ -1,243 +0,0 @@
|
||||
from typing import List
|
||||
|
||||
from BaseClasses import Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from .Items import RLItem, RLItemData, event_item_table, get_items_by_category, item_table
|
||||
from .Locations import RLLocation, location_table
|
||||
from .Options import RLOptions
|
||||
from .Presets import rl_options_presets
|
||||
from .Regions import create_regions
|
||||
from .Rules import set_rules
|
||||
|
||||
|
||||
class RLWeb(WebWorld):
|
||||
theme = "stone"
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Rogue Legacy Randomizer software on your computer. This guide covers single-player, "
|
||||
"multiworld, and related software.",
|
||||
"English",
|
||||
"rogue-legacy_en.md",
|
||||
"rogue-legacy/en",
|
||||
["Phar"]
|
||||
)]
|
||||
bug_report_page = "https://github.com/ThePhar/RogueLegacyRandomizer/issues/new?assignees=&labels=bug&template=" \
|
||||
"report-an-issue---.md&title=%5BIssue%5D"
|
||||
options_presets = rl_options_presets
|
||||
|
||||
|
||||
class RLWorld(World):
|
||||
"""
|
||||
Rogue Legacy is a genealogical rogue-"LITE" where anyone can be a hero. Each time you die, your child will succeed
|
||||
you. Every child is unique. One child might be colorblind, another might have vertigo-- they could even be a dwarf.
|
||||
But that's OK, because no one is perfect, and you don't have to be to succeed.
|
||||
"""
|
||||
game = "Rogue Legacy"
|
||||
options_dataclass = RLOptions
|
||||
options: RLOptions
|
||||
topology_present = True
|
||||
required_client_version = (0, 3, 5)
|
||||
web = RLWeb()
|
||||
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items() if data.code is not None}
|
||||
location_name_to_id = {name: data.code for name, data in location_table.items() if data.code is not None}
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
return self.options.as_dict(*[name for name in self.options_dataclass.type_hints.keys()])
|
||||
|
||||
def generate_early(self):
|
||||
# Check validation of names.
|
||||
additional_lady_names = len(self.options.additional_lady_names.value)
|
||||
additional_sir_names = len(self.options.additional_sir_names.value)
|
||||
if not self.options.allow_default_names:
|
||||
if additional_lady_names < int(self.options.number_of_children):
|
||||
raise Exception(
|
||||
f"allow_default_names is off, but not enough names are defined in additional_lady_names. "
|
||||
f"Expected {int(self.options.number_of_children)}, Got {additional_lady_names}")
|
||||
|
||||
if additional_sir_names < int(self.options.number_of_children):
|
||||
raise Exception(
|
||||
f"allow_default_names is off, but not enough names are defined in additional_sir_names. "
|
||||
f"Expected {int(self.options.number_of_children)}, Got {additional_sir_names}")
|
||||
|
||||
def create_items(self):
|
||||
item_pool: List[RLItem] = []
|
||||
total_locations = len(self.multiworld.get_unfilled_locations(self.player))
|
||||
for name, data in item_table.items():
|
||||
quantity = data.max_quantity
|
||||
|
||||
# Architect
|
||||
if name == "Architect":
|
||||
if self.options.architect == "disabled":
|
||||
continue
|
||||
if self.options.architect == "start_unlocked":
|
||||
self.multiworld.push_precollected(self.create_item(name))
|
||||
continue
|
||||
if self.options.architect == "early":
|
||||
self.multiworld.local_early_items[self.player]["Architect"] = 1
|
||||
|
||||
# Blacksmith and Enchantress
|
||||
if name == "Blacksmith" or name == "Enchantress":
|
||||
if self.options.vendors == "start_unlocked":
|
||||
self.multiworld.push_precollected(self.create_item(name))
|
||||
continue
|
||||
if self.options.vendors == "early":
|
||||
self.multiworld.local_early_items[self.player]["Blacksmith"] = 1
|
||||
self.multiworld.local_early_items[self.player]["Enchantress"] = 1
|
||||
|
||||
# Haggling
|
||||
if name == "Haggling" and self.options.disable_charon:
|
||||
continue
|
||||
|
||||
# Blueprints
|
||||
if data.category == "Blueprints":
|
||||
# No progressive blueprints if progressive_blueprints are disabled.
|
||||
if name == "Progressive Blueprints" and not self.options.progressive_blueprints:
|
||||
continue
|
||||
# No distinct blueprints if progressive_blueprints are enabled.
|
||||
elif name != "Progressive Blueprints" and self.options.progressive_blueprints:
|
||||
continue
|
||||
|
||||
# Classes
|
||||
if data.category == "Classes":
|
||||
if name == "Progressive Knights":
|
||||
if "Knight" not in self.options.available_classes:
|
||||
continue
|
||||
|
||||
if self.options.starting_class == "knight":
|
||||
quantity = 1
|
||||
if name == "Progressive Mages":
|
||||
if "Mage" not in self.options.available_classes:
|
||||
continue
|
||||
|
||||
if self.options.starting_class == "mage":
|
||||
quantity = 1
|
||||
if name == "Progressive Barbarians":
|
||||
if "Barbarian" not in self.options.available_classes:
|
||||
continue
|
||||
|
||||
if self.options.starting_class == "barbarian":
|
||||
quantity = 1
|
||||
if name == "Progressive Knaves":
|
||||
if "Knave" not in self.options.available_classes:
|
||||
continue
|
||||
|
||||
if self.options.starting_class == "knave":
|
||||
quantity = 1
|
||||
if name == "Progressive Miners":
|
||||
if "Miner" not in self.options.available_classes:
|
||||
continue
|
||||
|
||||
if self.options.starting_class == "miner":
|
||||
quantity = 1
|
||||
if name == "Progressive Shinobis":
|
||||
if "Shinobi" not in self.options.available_classes:
|
||||
continue
|
||||
|
||||
if self.options.starting_class == "shinobi":
|
||||
quantity = 1
|
||||
if name == "Progressive Liches":
|
||||
if "Lich" not in self.options.available_classes:
|
||||
continue
|
||||
|
||||
if self.options.starting_class == "lich":
|
||||
quantity = 1
|
||||
if name == "Progressive Spellthieves":
|
||||
if "Spellthief" not in self.options.available_classes:
|
||||
continue
|
||||
|
||||
if self.options.starting_class == "spellthief":
|
||||
quantity = 1
|
||||
if name == "Dragons":
|
||||
if "Dragon" not in self.options.available_classes:
|
||||
continue
|
||||
if name == "Traitors":
|
||||
if "Traitor" not in self.options.available_classes:
|
||||
continue
|
||||
|
||||
# Skills
|
||||
if name == "Health Up":
|
||||
quantity = self.options.health_pool.value
|
||||
elif name == "Mana Up":
|
||||
quantity = self.options.mana_pool.value
|
||||
elif name == "Attack Up":
|
||||
quantity = self.options.attack_pool.value
|
||||
elif name == "Magic Damage Up":
|
||||
quantity = self.options.magic_damage_pool.value
|
||||
elif name == "Armor Up":
|
||||
quantity = self.options.armor_pool.value
|
||||
elif name == "Equip Up":
|
||||
quantity = self.options.equip_pool.value
|
||||
elif name == "Crit Chance Up":
|
||||
quantity = self.options.crit_chance_pool.value
|
||||
elif name == "Crit Damage Up":
|
||||
quantity = self.options.crit_damage_pool.value
|
||||
|
||||
# Ignore filler, it will be added in a later stage.
|
||||
if data.category == "Filler":
|
||||
continue
|
||||
|
||||
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
||||
|
||||
# Fill any empty locations with filler items.
|
||||
while len(item_pool) < total_locations:
|
||||
item_pool.append(self.create_item(self.get_filler_item_name()))
|
||||
|
||||
self.multiworld.itempool += item_pool
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
fillers = get_items_by_category("Filler")
|
||||
weights = [data.weight for data in fillers.values()]
|
||||
return self.random.choices([filler for filler in fillers.keys()], weights, k=1)[0]
|
||||
|
||||
def create_item(self, name: str) -> RLItem:
|
||||
data = item_table[name]
|
||||
return RLItem(name, data.classification, data.code, self.player)
|
||||
|
||||
def create_event(self, name: str) -> RLItem:
|
||||
data = event_item_table[name]
|
||||
return RLItem(name, data.classification, data.code, self.player)
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self, self.player)
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self)
|
||||
self._place_events()
|
||||
|
||||
def _place_events(self):
|
||||
# Fountain
|
||||
self.multiworld.get_location("Fountain Room", self.player).place_locked_item(
|
||||
self.create_event("Defeat The Fountain"))
|
||||
|
||||
# Khidr / Neo Khidr
|
||||
if self.options.khidr == "vanilla":
|
||||
self.multiworld.get_location("Castle Hamson Boss Room", self.player).place_locked_item(
|
||||
self.create_event("Defeat Khidr"))
|
||||
else:
|
||||
self.multiworld.get_location("Castle Hamson Boss Room", self.player).place_locked_item(
|
||||
self.create_event("Defeat Neo Khidr"))
|
||||
|
||||
# Alexander / Alexander IV
|
||||
if self.options.alexander == "vanilla":
|
||||
self.multiworld.get_location("Forest Abkhazia Boss Room", self.player).place_locked_item(
|
||||
self.create_event("Defeat Alexander"))
|
||||
else:
|
||||
self.multiworld.get_location("Forest Abkhazia Boss Room", self.player).place_locked_item(
|
||||
self.create_event("Defeat Alexander IV"))
|
||||
|
||||
# Ponce de Leon / Ponce de Freon
|
||||
if self.options.leon == "vanilla":
|
||||
self.multiworld.get_location("The Maya Boss Room", self.player).place_locked_item(
|
||||
self.create_event("Defeat Ponce de Leon"))
|
||||
else:
|
||||
self.multiworld.get_location("The Maya Boss Room", self.player).place_locked_item(
|
||||
self.create_event("Defeat Ponce de Freon"))
|
||||
|
||||
# Herodotus / Astrodotus
|
||||
if self.options.herodotus == "vanilla":
|
||||
self.multiworld.get_location("Land of Darkness Boss Room", self.player).place_locked_item(
|
||||
self.create_event("Defeat Herodotus"))
|
||||
else:
|
||||
self.multiworld.get_location("Land of Darkness Boss Room", self.player).place_locked_item(
|
||||
self.create_event("Defeat Astrodotus"))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user