mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-29 18:03:27 -07:00
Compare commits
24 Commits
webhost_ot
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdbf72f148 | ||
|
|
2b46df90b4 | ||
|
|
88dc135960 | ||
|
|
95f696c04f | ||
|
|
96277fe9be | ||
|
|
a7a7879df4 | ||
|
|
773f3c4f08 | ||
|
|
139856a573 | ||
|
|
a1ed804267 | ||
|
|
2d58e7953c | ||
|
|
393ed51203 | ||
|
|
03c9d0717b | ||
|
|
5ca50cd8d3 | ||
|
|
36cf86f2e8 | ||
|
|
1705620c4f | ||
|
|
ffe4c6dd15 | ||
|
|
cf47cc67c0 | ||
|
|
645f25a94e | ||
|
|
74f41e3733 | ||
|
|
4276c6d6b0 | ||
|
|
116ab2286a | ||
|
|
fb45a2f87e | ||
|
|
2e5356ad05 | ||
|
|
8457ff3e4b |
@@ -87,7 +87,8 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
|
||||
seed = get_seed(args.seed)
|
||||
|
||||
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level, add_timestamp=args.log_time)
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level, add_timestamp=args.log_time)
|
||||
random.seed(seed)
|
||||
seed_name = get_seed_name(random)
|
||||
|
||||
|
||||
49
Launcher.py
49
Launcher.py
@@ -29,8 +29,8 @@ if __name__ == "__main__":
|
||||
|
||||
import settings
|
||||
import Utils
|
||||
from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename,
|
||||
user_path)
|
||||
from Utils import (env_cleared_lib_path, init_logging, is_frozen, is_linux, is_macos, is_windows, local_path,
|
||||
messagebox, open_filename, user_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_logging('Launcher')
|
||||
@@ -52,10 +52,7 @@ def open_host_yaml():
|
||||
webbrowser.open(file)
|
||||
return
|
||||
|
||||
env = os.environ
|
||||
if "LD_LIBRARY_PATH" in env:
|
||||
env = env.copy()
|
||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||
env = env_cleared_lib_path()
|
||||
subprocess.Popen([exe, file], env=env)
|
||||
|
||||
def open_patch():
|
||||
@@ -106,10 +103,7 @@ def open_folder(folder_path):
|
||||
return
|
||||
|
||||
if exe:
|
||||
env = os.environ
|
||||
if "LD_LIBRARY_PATH" in env:
|
||||
env = env.copy()
|
||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||
env = env_cleared_lib_path()
|
||||
subprocess.Popen([exe, folder_path], env=env)
|
||||
else:
|
||||
logging.warning(f"No file browser available to open {folder_path}")
|
||||
@@ -202,22 +196,32 @@ def get_exe(component: str | Component) -> Sequence[str] | None:
|
||||
return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None
|
||||
|
||||
|
||||
def launch(exe, in_terminal=False):
|
||||
def launch(exe: Sequence[str], in_terminal: bool = False) -> bool:
|
||||
"""Runs the given command/args in `exe` in a new process.
|
||||
|
||||
If `in_terminal` is True, it will attempt to run in a terminal window,
|
||||
and the return value will indicate whether one was found."""
|
||||
if in_terminal:
|
||||
if is_windows:
|
||||
# intentionally using a window title with a space so it gets quoted and treated as a title
|
||||
subprocess.Popen(["start", "Running Archipelago", *exe], shell=True)
|
||||
return
|
||||
return True
|
||||
elif is_linux:
|
||||
terminal = which('x-terminal-emulator') or which('gnome-terminal') or which('xterm')
|
||||
terminal = which("x-terminal-emulator") or which("konsole") or which("gnome-terminal") or which("xterm")
|
||||
if terminal:
|
||||
subprocess.Popen([terminal, '-e', shlex.join(exe)])
|
||||
return
|
||||
# Clear LD_LIB_PATH during terminal startup, but set it again when running command in case it's needed
|
||||
ld_lib_path = os.environ.get("LD_LIBRARY_PATH")
|
||||
lib_path_setter = f"env LD_LIBRARY_PATH={shlex.quote(ld_lib_path)} " if ld_lib_path else ""
|
||||
env = env_cleared_lib_path()
|
||||
|
||||
subprocess.Popen([terminal, "-e", lib_path_setter + shlex.join(exe)], env=env)
|
||||
return True
|
||||
elif is_macos:
|
||||
terminal = [which('open'), '-W', '-a', 'Terminal.app']
|
||||
terminal = [which("open"), "-W", "-a", "Terminal.app"]
|
||||
subprocess.Popen([*terminal, *exe])
|
||||
return
|
||||
return True
|
||||
subprocess.Popen(exe)
|
||||
return False
|
||||
|
||||
|
||||
def create_shortcut(button: Any, component: Component) -> None:
|
||||
@@ -406,12 +410,17 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
|
||||
@staticmethod
|
||||
def component_action(button):
|
||||
MDSnackbar(MDSnackbarText(text="Opening in a new window..."), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
open_text = "Opening in a new window..."
|
||||
if button.component.func:
|
||||
# Note: if we want to draw the Snackbar before running func, func needs to be wrapped in schedule_once
|
||||
button.component.func()
|
||||
else:
|
||||
launch(get_exe(button.component), button.component.cli)
|
||||
# if launch returns False, it started the process in background (not in a new terminal)
|
||||
if not launch(get_exe(button.component), button.component.cli) and button.component.cli:
|
||||
open_text = "Running in the background..."
|
||||
|
||||
MDSnackbar(MDSnackbarText(text=open_text), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
|
||||
def _on_drop_file(self, window: Window, filename: bytes, x: int, y: int) -> None:
|
||||
""" When a patch file is dropped into the window, run the associated component. """
|
||||
|
||||
33
Utils.py
33
Utils.py
@@ -22,7 +22,7 @@ from datetime import datetime, timezone
|
||||
|
||||
from settings import Settings, get_settings
|
||||
from time import sleep
|
||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
|
||||
from typing import BinaryIO, Coroutine, Mapping, Optional, Set, Dict, Any, Union, TypeGuard
|
||||
from yaml import load, load_all, dump
|
||||
from pathspec import PathSpec, GitIgnoreSpec
|
||||
from typing_extensions import deprecated
|
||||
@@ -236,10 +236,7 @@ def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
|
||||
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
|
||||
assert open_command, "Didn't find program for open_file! Please report this together with system details."
|
||||
|
||||
env = os.environ
|
||||
if "LD_LIBRARY_PATH" in env:
|
||||
env = env.copy()
|
||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||
env = env_cleared_lib_path()
|
||||
subprocess.call([open_command, filename], env=env)
|
||||
|
||||
|
||||
@@ -345,6 +342,9 @@ def persistent_load() -> Dict[str, Dict[str, Any]]:
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
storage = unsafe_parse_yaml(f.read())
|
||||
if "datapackage" in storage:
|
||||
del storage["datapackage"]
|
||||
logging.debug("Removed old datapackage from persistent storage")
|
||||
except Exception as e:
|
||||
logging.debug(f"Could not read store: {e}")
|
||||
if storage is None:
|
||||
@@ -369,11 +369,6 @@ def load_data_package_for_checksum(game: str, checksum: typing.Optional[str]) ->
|
||||
except Exception as e:
|
||||
logging.debug(f"Could not load data package: {e}")
|
||||
|
||||
# fall back to old cache
|
||||
cache = persistent_load().get("datapackage", {}).get("games", {}).get(game, {})
|
||||
if cache.get("checksum") == checksum:
|
||||
return cache
|
||||
|
||||
# cache does not match
|
||||
return {}
|
||||
|
||||
@@ -758,6 +753,19 @@ def is_kivy_running() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def env_cleared_lib_path() -> Mapping[str, str]:
|
||||
"""
|
||||
Creates a copy of the current environment vars with the LD_LIBRARY_PATH removed if set, as this can interfere when
|
||||
launching something in a subprocess.
|
||||
"""
|
||||
env = os.environ
|
||||
if "LD_LIBRARY_PATH" in env:
|
||||
env = env.copy()
|
||||
del env["LD_LIBRARY_PATH"]
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def _mp_open_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None:
|
||||
if is_kivy_running():
|
||||
raise RuntimeError("kivy should not be running in multiprocess")
|
||||
@@ -770,10 +778,7 @@ def _mp_save_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args:
|
||||
res.put(save_filename(*args))
|
||||
|
||||
def _run_for_stdout(*args: str):
|
||||
env = os.environ
|
||||
if "LD_LIBRARY_PATH" in env:
|
||||
env = env.copy()
|
||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||
env = env_cleared_lib_path()
|
||||
return subprocess.run(args, capture_output=True, text=True, env=env).stdout.split("\n", 1)[0] or None
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
flask>=3.1.1
|
||||
werkzeug>=3.1.3
|
||||
pony>=0.7.19; python_version <= '3.12'
|
||||
flask==3.1.3
|
||||
werkzeug==3.1.6
|
||||
pony==0.7.19; python_version <= '3.12'
|
||||
pony @ git+https://github.com/black-sliver/pony@7feb1221953b7fa4a6735466bf21a8b4d35e33ba#0.7.19; python_version >= '3.13'
|
||||
waitress>=3.0.2
|
||||
Flask-Caching>=2.3.0
|
||||
waitress==3.0.2
|
||||
Flask-Caching==2.3.1
|
||||
Flask-Compress==1.18 # pkg_resources can't resolve the "backports.zstd" dependency of >1.18, breaking ModuleUpdate.py
|
||||
Flask-Limiter>=3.12
|
||||
Flask-Cors>=6.0.2
|
||||
bokeh>=3.6.3
|
||||
markupsafe>=3.0.2
|
||||
setproctitle>=1.3.5
|
||||
mistune>=3.1.3
|
||||
docutils>=0.22.2
|
||||
Flask-Limiter==4.1.1
|
||||
Flask-Cors==6.0.2
|
||||
bokeh==3.8.2
|
||||
markupsafe==3.0.3
|
||||
setproctitle==1.3.7
|
||||
mistune==3.2.0
|
||||
docutils==0.22.4
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
<h1>Currently Supported Games</h1>
|
||||
<p>Below are the games that are currently included with the Archipelago software. To play a game that is not on
|
||||
this page, please refer to the <a href="/tutorial/Archipelago/setup/en#playing-with-custom-worlds">playing with
|
||||
custom worlds</a> section of the setup guide.</p>
|
||||
custom worlds</a> section of the setup guide and the
|
||||
<a href="{{ url_for("tutorial", game="Archipelago", file="other_en") }}">other games and tools guide</a>
|
||||
to find more.</p>
|
||||
<div class="js-only">
|
||||
<label for="game-search">Search for your game below!</label><br />
|
||||
<div class="page-controls">
|
||||
|
||||
@@ -20,11 +20,7 @@
|
||||
{% for file_name, file_data in tutorial_data.files.items() %}
|
||||
<li>
|
||||
<a href="{{ url_for("tutorial", game=world_name, file=file_name) }}">{{ file_data.language }}</a>
|
||||
by
|
||||
{% for author in file_data.authors %}
|
||||
{{ author }}
|
||||
{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
by {{ file_data.authors | join(", ") }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
# NewSoupVi is acting maintainer, but world belongs to core with the exception of the music
|
||||
/worlds/apquest/ @NewSoupVi
|
||||
|
||||
# Sudoku (APSudoku)
|
||||
/worlds/apsudoku/ @EmilyV99
|
||||
|
||||
# Aquaria
|
||||
/worlds/aquaria/ @tioui
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
colorama>=0.4.6
|
||||
websockets>=13.0.1,<14
|
||||
PyYAML>=6.0.3
|
||||
jellyfish>=1.2.1
|
||||
jinja2>=3.1.6
|
||||
schema>=0.7.8
|
||||
kivy>=2.3.1
|
||||
bsdiff4>=1.2.6
|
||||
platformdirs>=4.5.0
|
||||
certifi>=2025.11.12
|
||||
cython>=3.2.1
|
||||
cymem>=2.0.13
|
||||
orjson>=3.11.4
|
||||
typing_extensions>=4.15.0
|
||||
pyshortcuts>=1.9.6
|
||||
pathspec>=0.12.1
|
||||
colorama==0.4.6
|
||||
websockets==13.1 # ,<14
|
||||
PyYAML==6.0.3
|
||||
jellyfish==1.2.1
|
||||
jinja2==3.1.6
|
||||
schema==0.7.8
|
||||
kivy==2.3.1
|
||||
bsdiff4==1.2.6
|
||||
platformdirs==4.9.4
|
||||
certifi==2026.2.25
|
||||
cython==3.2.4
|
||||
cymem==2.0.13
|
||||
orjson==3.11.7
|
||||
typing_extensions==4.15.0
|
||||
pyshortcuts==1.9.7
|
||||
pathspec==1.0.4
|
||||
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
||||
kivymd>=2.0.1.dev0
|
||||
|
||||
# Legacy world dependencies that custom worlds rely on
|
||||
Pymem>=1.13.0
|
||||
Pymem==1.14.0
|
||||
|
||||
@@ -527,7 +527,7 @@ class Or(NestedRule[TWorld], game="Archipelago"):
|
||||
items[item] = 1
|
||||
elif isinstance(child, HasAnyCount.Resolved):
|
||||
for item, count in child.item_counts:
|
||||
if item not in items or items[item] < count:
|
||||
if item not in items or count < items[item]:
|
||||
items[item] = count
|
||||
else:
|
||||
clauses.append(child)
|
||||
|
||||
3
setup.py
3
setup.py
@@ -71,7 +71,6 @@ non_apworlds: set[str] = {
|
||||
"Ocarina of Time",
|
||||
"Overcooked! 2",
|
||||
"Raft",
|
||||
"Sudoku",
|
||||
"Super Mario 64",
|
||||
"VVVVVV",
|
||||
"Wargroove",
|
||||
@@ -658,7 +657,7 @@ cx_Freeze.setup(
|
||||
options={
|
||||
"build_exe": {
|
||||
"packages": ["worlds", "kivy", "cymem", "websockets", "kivymd"],
|
||||
"includes": [],
|
||||
"includes": ["rule_builder.cached_world"],
|
||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||
"pandas"],
|
||||
"zip_includes": [],
|
||||
|
||||
@@ -11,7 +11,7 @@ class TestImplemented(unittest.TestCase):
|
||||
def test_completion_condition(self):
|
||||
"""Ensure a completion condition is set that has requirements."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
if not world_type.hidden and game_name not in {"Sudoku"}:
|
||||
if not world_type.hidden:
|
||||
with self.subTest(game_name):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
self.assertFalse(multiworld.completion_condition[1](multiworld.state))
|
||||
@@ -59,7 +59,7 @@ class TestImplemented(unittest.TestCase):
|
||||
def test_prefill_items(self):
|
||||
"""Test that every world can reach every location from allstate before pre_fill."""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
if gamename not in ("Archipelago", "Sudoku", "Final Fantasy", "Test Game"):
|
||||
if gamename not in ("Archipelago", "Final Fantasy", "Test Game"):
|
||||
with self.subTest(gamename):
|
||||
multiworld = setup_solo_multiworld(world_type, ("generate_early", "create_regions", "create_items",
|
||||
"set_rules", "connect_entrances", "generate_basic"))
|
||||
|
||||
@@ -109,7 +109,7 @@ class TestOptions(unittest.TestCase):
|
||||
def test_option_set_keys_random(self):
|
||||
"""Tests that option sets do not contain 'random' and its variants as valid keys"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
if game_name not in ("Archipelago", "Sudoku", "Super Metroid"):
|
||||
if game_name not in ("Archipelago", "Super Metroid"):
|
||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||
if issubclass(option, OptionSet):
|
||||
with self.subTest(game=game_name, option=option_key):
|
||||
|
||||
@@ -233,6 +233,14 @@ class CachedRuleBuilderTestCase(RuleBuilderTestCase):
|
||||
Or(Has("A"), HasAny("B", "C"), HasAnyCount({"D": 1, "E": 1})),
|
||||
HasAny.Resolved(("A", "B", "C", "D", "E"), player=1),
|
||||
),
|
||||
(
|
||||
And(HasAllCounts({"A": 1, "B": 2}), HasAllCounts({"A": 2, "B": 2})),
|
||||
HasAllCounts.Resolved((("A", 2), ("B", 2)), player=1),
|
||||
),
|
||||
(
|
||||
Or(HasAnyCount({"A": 1, "B": 2}), HasAnyCount({"A": 2, "B": 2})),
|
||||
HasAnyCount.Resolved((("A", 1), ("B", 2)), player=1),
|
||||
),
|
||||
)
|
||||
)
|
||||
class TestSimplify(RuleBuilderTestCase):
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
maseya-z3pr>=1.0.0rc1
|
||||
xxtea>=3.0.0
|
||||
maseya-z3pr==1.0.0rc1
|
||||
xxtea==3.7.0
|
||||
|
||||
@@ -77,7 +77,7 @@ class APQuestGrid(GridLayout):
|
||||
parent_width, parent_height = self.parent.size
|
||||
|
||||
self_width_according_to_parent_height = parent_height * 12 / 11
|
||||
self_height_according_to_parent_width = parent_height * 11 / 12
|
||||
self_height_according_to_parent_width = parent_width * 11 / 12
|
||||
|
||||
if self_width_according_to_parent_height > parent_width:
|
||||
self.size = parent_width, self_height_according_to_parent_width
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import pkgutil
|
||||
from collections.abc import Buffer
|
||||
from enum import Enum
|
||||
from io import BytesIO
|
||||
from typing import Literal, NamedTuple, Protocol, cast
|
||||
|
||||
from kivy.uix.image import CoreImage
|
||||
from typing_extensions import Buffer
|
||||
|
||||
from CommonClient import logger
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import asyncio
|
||||
import pkgutil
|
||||
from asyncio import Task
|
||||
from collections.abc import Buffer
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from kivy import Config
|
||||
from kivy.core.audio import Sound, SoundLoader
|
||||
from typing_extensions import Buffer
|
||||
|
||||
from CommonClient import logger
|
||||
|
||||
@@ -85,7 +85,7 @@ class SoundManager:
|
||||
|
||||
def ensure_config(self) -> None:
|
||||
Config.adddefaultsection("APQuest")
|
||||
Config.setdefault("APQuest", "volume", 50)
|
||||
Config.setdefault("APQuest", "volume", 30)
|
||||
self.set_volume_percentage(Config.getint("APQuest", "volume"))
|
||||
|
||||
async def sound_manager_loop(self) -> None:
|
||||
@@ -149,6 +149,7 @@ class SoundManager:
|
||||
continue
|
||||
|
||||
if sound_name == audio_filename:
|
||||
sound.volume = self.volume_percentage / 100
|
||||
sound.play()
|
||||
self.update_background_music()
|
||||
higher_priority_sound_is_playing = True
|
||||
@@ -213,6 +214,7 @@ class SoundManager:
|
||||
# It ends up feeling better if this just always continues playing quietly after being started.
|
||||
# Even "fading in at a random spot" is better than restarting the song after a jingle / math trap.
|
||||
if self.game_started and song.state == "stop":
|
||||
song.volume = self.current_background_music_volume * self.volume_percentage / 100
|
||||
song.play()
|
||||
song.seek(0)
|
||||
continue
|
||||
@@ -228,6 +230,7 @@ class SoundManager:
|
||||
|
||||
if self.current_background_music_volume != 0:
|
||||
if song.state == "stop":
|
||||
song.volume = self.current_background_music_volume * self.volume_percentage / 100
|
||||
song.play()
|
||||
song.seek(0)
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
from typing import Dict
|
||||
|
||||
from BaseClasses import Tutorial
|
||||
from ..AutoWorld import WebWorld, World
|
||||
|
||||
class AP_SudokuWebWorld(WebWorld):
|
||||
options_page = False
|
||||
theme = 'partyTime'
|
||||
|
||||
setup_en = Tutorial(
|
||||
tutorial_name='Setup Guide',
|
||||
description='A guide to playing APSudoku',
|
||||
language='English',
|
||||
file_name='setup_en.md',
|
||||
link='setup/en',
|
||||
authors=['EmilyV']
|
||||
)
|
||||
|
||||
tutorials = [setup_en]
|
||||
|
||||
class AP_SudokuWorld(World):
|
||||
"""
|
||||
Play a little Sudoku while you're in BK mode to maybe get some useful hints
|
||||
"""
|
||||
game = "Sudoku"
|
||||
web = AP_SudokuWebWorld()
|
||||
|
||||
item_name_to_id: Dict[str, int] = {}
|
||||
location_name_to_id: Dict[str, int] = {}
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, multiworld):
|
||||
raise Exception("APSudoku cannot be used for generating worlds, the client can instead connect to any slot from any world")
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# APSudoku
|
||||
|
||||
## Hint Games
|
||||
|
||||
HintGames do not need to be added at the start of a seed, and do not create a 'slot'- instead, you connect the HintGame client to a different game's slot. By playing a HintGame, you can earn hints for the connected slot.
|
||||
|
||||
## What is this game?
|
||||
|
||||
Play Sudoku puzzles of varying difficulties, earning a hint for each puzzle correctly solved. Harder puzzles are more likely to grant a hint towards a Progression item, though otherwise what hint is granted is random.
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld.
|
||||
|
||||
By using the connected room's Admin Password on the Admin Panel tab, you can configure some settings at any time to affect the entire room. This allows disabling hints entirely, as well as altering the hint odds for each difficulty.
|
||||
@@ -1,55 +0,0 @@
|
||||
# APSudoku Setup Guide
|
||||
|
||||
## Required Software
|
||||
- [APSudoku](https://github.com/APSudoku/APSudoku)
|
||||
|
||||
## General Concept
|
||||
|
||||
This is a HintGame client, which can connect to any multiworld slot, allowing you to play Sudoku to unlock random hints for that slot's locations.
|
||||
|
||||
Does not need to be added at the start of a seed, as it does not create any slots of its own, nor does it have any YAML files.
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
### Windows / Linux
|
||||
Go to the latest release from the [github APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform.
|
||||
|
||||
### Web
|
||||
Go to the [github pages](apsudoku.github.io) or [itch.io](https://emilyv99.itch.io/apsudoku) site, and play in the browser.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Run the APSudoku executable.
|
||||
2. Under `Settings` → `Connection` at the top-right:
|
||||
- Enter the server address and port number
|
||||
- Enter the name of the slot you wish to connect to
|
||||
- Enter the room password (optional)
|
||||
- Select DeathLink related settings (optional)
|
||||
- Press `Connect`
|
||||
4. Under the `Sudoku` tab
|
||||
- Choose puzzle difficulty
|
||||
- Click `Start` to generate a puzzle
|
||||
5. Try to solve the Sudoku. Click `Check` when done
|
||||
- A correct solution rewards you with 1 hint for a location in the world you are connected to
|
||||
- An incorrect solution has no penalty, unless DeathLink is enabled (see below)
|
||||
|
||||
Info:
|
||||
- You can set various settings under `Settings` → `Sudoku`, and can change the colors used under `Settings` → `Theme`.
|
||||
- While connected, you can view the `Console` and `Hints` tabs for standard TextClient-like features
|
||||
- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md)
|
||||
- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted)
|
||||
- Click the various `?` buttons for information on controls/how to play
|
||||
|
||||
## Admin Settings
|
||||
|
||||
By using the connected room's Admin Password on the Admin Panel tab, you can configure some settings at any time to affect the entire room.
|
||||
|
||||
- You can disable APSudoku for the entire room, preventing any hints from being granted.
|
||||
- You can customize the reward weights for each difficulty, making progression hints more or less likely, and/or adding a chance to get "no hint" after a solve.
|
||||
|
||||
## DeathLink Support
|
||||
|
||||
If `DeathLink` is enabled when you click `Connect`:
|
||||
- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or if you quit a puzzle without solving it (including disconnecting).
|
||||
- Your life count is customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
|
||||
- On receiving a DeathLink from another player, your puzzle resets.
|
||||
@@ -271,7 +271,7 @@ item_table = {
|
||||
ItemNames.TRIDENT: ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head
|
||||
ItemNames.TURTLE_EGG: ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg
|
||||
ItemNames.JELLY_EGG: ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed
|
||||
ItemNames.URCHIN_COSTUME: ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume
|
||||
ItemNames.URCHIN_COSTUME: ItemData(698034, 1, ItemType.PROGRESSION, ItemGroup.COLLECTIBLE), # collectible_urchin_costume
|
||||
ItemNames.BABY_WALKER: ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker
|
||||
ItemNames.VEDHA_S_CURE_ALL: ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All
|
||||
ItemNames.ZUUNA_S_PEROGI: ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi
|
||||
@@ -384,8 +384,8 @@ four_gods_excludes = [ItemNames.ANEMONE, ItemNames.ARNASSI_STATUE, ItemNames.BIG
|
||||
ItemNames.MITHALAS_BANNER, ItemNames.MITHALAS_POT, ItemNames.MUTANT_COSTUME, ItemNames.SEED_BAG,
|
||||
ItemNames.KING_S_SKULL, ItemNames.SONG_PLANT_SPORE, ItemNames.STONE_HEAD, ItemNames.SUN_KEY,
|
||||
ItemNames.GIRL_COSTUME, ItemNames.ODD_CONTAINER, ItemNames.TRIDENT, ItemNames.TURTLE_EGG,
|
||||
ItemNames.JELLY_EGG, ItemNames.URCHIN_COSTUME, ItemNames.BABY_WALKER,
|
||||
ItemNames.RAINBOW_MUSHROOM, ItemNames.RAINBOW_MUSHROOM, ItemNames.RAINBOW_MUSHROOM,
|
||||
ItemNames.JELLY_EGG, ItemNames.BABY_WALKER, ItemNames.RAINBOW_MUSHROOM,
|
||||
ItemNames.RAINBOW_MUSHROOM, ItemNames.RAINBOW_MUSHROOM, ItemNames.FISH_OIL,
|
||||
ItemNames.LEAF_POULTICE, ItemNames.LEAF_POULTICE, ItemNames.LEAF_POULTICE,
|
||||
ItemNames.LEECHING_POULTICE, ItemNames.LEECHING_POULTICE, ItemNames.ARCANE_POULTICE,
|
||||
ItemNames.ROTTEN_MEAT, ItemNames.ROTTEN_MEAT, ItemNames.ROTTEN_MEAT, ItemNames.ROTTEN_MEAT,
|
||||
|
||||
@@ -37,7 +37,7 @@ def _has_li(state: CollectionState, player: int) -> bool:
|
||||
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
|
||||
ItemNames.BABY_BLASTER, ItemNames.URCHIN_COSTUME
|
||||
]
|
||||
|
||||
def _has_damaging_item(state: CollectionState, player: int, damaging_items:Iterable[str] = DAMAGING_ITEMS) -> bool:
|
||||
|
||||
@@ -76,7 +76,7 @@ class AquariaWorld(World):
|
||||
item_name_groups = {
|
||||
"Damage": {ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM,
|
||||
ItemNames.LI_AND_LI_SONG, ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA,
|
||||
ItemNames.BABY_BLASTER},
|
||||
ItemNames.BABY_BLASTER, ItemNames.URCHIN_COSTUME},
|
||||
"Light": {ItemNames.SUN_FORM, ItemNames.BABY_DUMBO}
|
||||
}
|
||||
"""Grouping item make it easier to find them"""
|
||||
|
||||
@@ -37,7 +37,7 @@ class FactorioWeb(WebWorld):
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["Berserker, Farrak Kilhn"]
|
||||
["Berserker", "Farrak Kilhn"]
|
||||
)]
|
||||
option_groups = option_groups
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ end
|
||||
data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
||||
data.raw["assembling-machine"]["assembling-machine-2"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
||||
data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes)
|
||||
data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes_off_when_no_fluid_recipe = data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes_off_when_no_fluid_recipe
|
||||
if mods["factory-levels"] then
|
||||
-- Factory-Levels allows the assembling machines to get faster (and depending on settings), more productive at crafting products, the more the
|
||||
-- assembling machine crafts the product. If the machine crafts enough, it may auto-upgrade to the next tier.
|
||||
|
||||
@@ -1 +1 @@
|
||||
factorio-rcon-py>=2.1.2
|
||||
factorio-rcon-py==2.1.3
|
||||
|
||||
@@ -26,7 +26,10 @@ class GenericWeb(WebWorld):
|
||||
'English', 'setup_en.md', 'setup/en', ['alwaysintreble'])
|
||||
triggers = Tutorial('Archipelago Triggers Guide', 'A guide to setting up and using triggers in your game settings.',
|
||||
'English', 'triggers_en.md', 'triggers/en', ['alwaysintreble'])
|
||||
tutorials = [setup, mac, commands, advanced_settings, triggers, plando]
|
||||
other_games = Tutorial('Other Games and Tools',
|
||||
'A guide to additional games and tools that can be used with Archipelago.',
|
||||
'English', 'other_en.md', 'other/en', ['Berserker'])
|
||||
tutorials = [setup, mac, commands, advanced_settings, triggers, plando, other_games]
|
||||
|
||||
|
||||
class GenericWorld(World):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Archipelago does not have a compiled release on macOS. However, it is possible to run from source code on macOS. This guide expects you to have some experience with running software from the terminal.
|
||||
## Prerequisite Software
|
||||
Here is a list of software to install and source code to download.
|
||||
1. Python 3.11 "universal2" or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/).
|
||||
1. Python 3.11.9 "universal2" or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/).
|
||||
**Python 3.14 is not supported yet.**
|
||||
2. Xcode from the [macOS App Store](https://apps.apple.com/us/app/xcode/id497799835).
|
||||
3. The source code from the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
|
||||
37
worlds/generic/docs/other_en.md
Normal file
37
worlds/generic/docs/other_en.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Other Games And Tools
|
||||
|
||||
This page provides information and links regarding various tools that may be of use with Archipelago, including additional playable games not supported by this website.
|
||||
|
||||
You should only download and use files from sources you trust; sources listed here are not officially vetted for safety, so use your own judgement and caution.
|
||||
|
||||
## Discord
|
||||
|
||||
Currently, Discord is the primary hub for Archipelago; whether it be finding people to play with, developing new game implementations, or finding new playable games.
|
||||
|
||||
The [Archipelago Official Discord](https://discord.gg/8Z65BR2) is the main hub, while the [Archipelago After Dark Discord](https://discord.gg/fqvNCCRsu4) houses additional games that may be unrated or 18+ in some territories.
|
||||
|
||||
The `#apworld-index` channels in each of these servers contain lists of playable games which should be easily downloadable and playable with an Archipelago installation.
|
||||
|
||||
## Wiki
|
||||
|
||||
The community-maintained [Archipelago Wiki](https://archipelago.miraheze.org/) has information on many games as well, and acts as a great discord-free source of information.
|
||||
|
||||
## Hint Games
|
||||
|
||||
Hint Games are a special type of game which are not included as part of the multiworld generation process. Instead, they can log in to an ongoing multiworld, connecting to a slot designated for any game. Rather than earning items for other games in the multiworld, a Hint Game will allow you to earn hints for the slot you are connected to.
|
||||
|
||||
Hint Games can be found from sources such as the Discord and the [Hint Game Category](https://archipelago.miraheze.org/wiki/Category:Hint_games) of the wiki, as detailed above.
|
||||
|
||||
## Notable Tools
|
||||
|
||||
### Options Creator
|
||||
|
||||
The Options Creator is included in the Archipelago installation, and is accessible from the Archipelago Launcher. Using this simple GUI tool, you can easily create randomization options for any installed `.apworld` - perfect when using custom worlds you've installed that don't have options pages on the website.
|
||||
|
||||
### PopTracker
|
||||
|
||||
[PopTracker](https://poptracker.github.io) is a popular tool in Randomizer communities, which many games support via custom PopTracker Packs. Many Archipelago packs include the ability to directly connect to your slot for auto-tracking capabilities. (Check each game's setup guide or Discord channel to see if it has PopTracker compatibility!)
|
||||
|
||||
### Universal Tracker
|
||||
|
||||
[Universal Tracker](https://github.com/FarisTheAncient/Archipelago/releases?q=Tracker) is a custom tracker client that uses your .yaml files from generation (as well as the .apworld files) to attempt to provide a view of what locations are currently in-logic or not, using the actual generation logic. Specific steps may need to be taken depending on the game, or the use of randomness in your yaml. Support for UT can be found in the [#universal-tracker](https://discord.com/channels/731205301247803413/1367270230635839539) channel of the Archipelago Official Discord.
|
||||
@@ -5,11 +5,11 @@ from enum import IntFlag
|
||||
from typing import Any, ClassVar, Dict, Iterator, List, Set, Tuple, Type
|
||||
|
||||
import settings
|
||||
from BaseClasses import Item, ItemClassification, Location, MultiWorld, Region, Tutorial
|
||||
from BaseClasses import CollectionRule, Item, ItemClassification, Location, MultiWorld, Region, Tutorial
|
||||
from Options import PerGameCommonOptions
|
||||
from Utils import __version__
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from worlds.generic.Rules import add_rule, CollectionRule, set_rule
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
from .Client import L2ACSNIClient # noqa: F401
|
||||
from .Items import ItemData, ItemType, l2ac_item_name_to_id, l2ac_item_table, L2ACItem, start_id as items_start_id
|
||||
from .Locations import l2ac_location_name_to_id, L2ACLocation
|
||||
|
||||
@@ -478,7 +478,7 @@ def space_zone_2_boss(state, player):
|
||||
|
||||
def space_zone_2_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Space Zone 2")
|
||||
reachable_coins = 12
|
||||
reachable_coins = 9
|
||||
if state.has_any(["Mushroom", "Fire Flower", "Carrot", "Space Physics"], player):
|
||||
reachable_coins += 15
|
||||
if state.has("Space Physics", player) or not auto_scroll:
|
||||
@@ -487,7 +487,7 @@ def space_zone_2_coins(state, player, coins):
|
||||
state.has("Mushroom", player) and state.has_any(["Fire Flower", "Carrot"], player))):
|
||||
reachable_coins += 3
|
||||
if state.has("Space Physics", player):
|
||||
reachable_coins += 79
|
||||
reachable_coins += 82
|
||||
if not auto_scroll:
|
||||
reachable_coins += 21
|
||||
return coins <= reachable_coins
|
||||
|
||||
@@ -192,7 +192,7 @@ class MessengerRules:
|
||||
or (self.has_dart(state) and self.has_wingsuit(state)),
|
||||
# Dark Cave
|
||||
"Dark Cave - Right -> Dark Cave - Left":
|
||||
lambda state: state.has("Candle", self.player) and self.has_dart(state),
|
||||
lambda state: state.has("Candle", self.player) and self.has_dart(state) and self.has_wingsuit(state),
|
||||
# Riviere Turquoise
|
||||
"Riviere Turquoise - Waterfall Shop -> Riviere Turquoise - Flower Flight Checkpoint":
|
||||
lambda state: self.has_dart(state) or (
|
||||
|
||||
@@ -15,6 +15,7 @@ class MuseDashCollections:
|
||||
"Default Music",
|
||||
"Budget Is Burning: Nano Core",
|
||||
"Budget Is Burning Vol.1",
|
||||
"Wuthering Waves Pioneer Podcast",
|
||||
]
|
||||
|
||||
MUSE_PLUS_DLC: str = "Muse Plus"
|
||||
@@ -40,6 +41,7 @@ class MuseDashCollections:
|
||||
"Heart Message feat. Aoi Tokimori Secret",
|
||||
"Meow Rock feat. Chun Ge, Yuan Shen",
|
||||
"Stra Stella Secret",
|
||||
"Musepyoi Legend",
|
||||
]
|
||||
|
||||
song_items = SONG_DATA
|
||||
|
||||
@@ -696,11 +696,20 @@ SONG_DATA: Dict[str, SongData] = {
|
||||
"Otsukimi Koete Otsukiai": SongData(2900820, "43-70", "MD Plus Project", True, 6, 8, 10),
|
||||
"Obenkyou Time": SongData(2900821, "43-71", "MD Plus Project", False, 6, 8, 11),
|
||||
"Retry Now": SongData(2900822, "43-72", "MD Plus Project", False, 3, 6, 9),
|
||||
"Master Bancho's Sushi Class ": SongData(2900823, "93-0", "Welcome to the Blue Hole!", False, None, None, None),
|
||||
"Master Bancho's Sushi Class": SongData(2900823, "93-0", "Welcome to the Blue Hole!", False, None, 7, None),
|
||||
"CHAOTiC BATTLE": SongData(2900824, "94-0", "Cosmic Radio 2025", False, 7, 9, 11),
|
||||
"FATAL GAME": SongData(2900825, "94-1", "Cosmic Radio 2025", False, 3, 6, 9),
|
||||
"Aria": SongData(2900826, "94-2", "Cosmic Radio 2025", False, 4, 6, 9),
|
||||
"+1 UNKNOWN -NUMBER": SongData(2900827, "94-3", "Cosmic Radio 2025", True, 4, 7, 10),
|
||||
"To the Beyond, from the Nameless Seaside": SongData(2900828, "94-4", "Cosmic Radio 2025", False, 5, 8, 10),
|
||||
"REK421": SongData(2900829, "94-5", "Cosmic Radio 2025", True, 7, 9, 11),
|
||||
}
|
||||
"Musepyoi Legend": SongData(2900830, "95-0", "Ay-Aye Horse", True, None, None, None),
|
||||
"Not Regret": SongData(2900831, "95-1", "Ay-Aye Horse", False, 7, 9, 11),
|
||||
"-Toryanna-": SongData(2900832, "95-2", "Ay-Aye Horse", True, 4, 6, 9),
|
||||
"Icecream Angels": SongData(2900833, "95-3", "Ay-Aye Horse", False, 3, 6, 9),
|
||||
"MEGA TSKR": SongData(2900834, "95-4", "Ay-Aye Horse", False, 4, 7, 10),
|
||||
"777 Vocal ver.": SongData(2900835, "95-5", "Ay-Aye Horse", False, 7, 9, 11),
|
||||
"Chasing Daylight": SongData(2900836, "96-0", "Wuthering Waves Pioneer Podcast", False, 3, 5, 8),
|
||||
"CATCH ME IF YOU CAN": SongData(2900837, "96-1", "Wuthering Waves Pioneer Podcast", False, 4, 6, 9),
|
||||
"RUNNING FOR YOUR LIFE": SongData(2900838, "96-2", "Wuthering Waves Pioneer Podcast", False, 2, 5, 8),
|
||||
}
|
||||
@@ -124,7 +124,8 @@ class MuseDashWorld(World):
|
||||
|
||||
self.starting_songs = [s for s in start_items if s in song_items]
|
||||
self.starting_songs = self.md_collection.filter_songs_to_dlc(self.starting_songs, dlc_songs)
|
||||
self.included_songs = [s for s in include_songs if s in song_items and s not in self.starting_songs]
|
||||
# Sort first for deterministic iteration order.
|
||||
self.included_songs = [s for s in sorted(include_songs) if s in song_items and s not in self.starting_songs]
|
||||
self.included_songs = self.md_collection.filter_songs_to_dlc(self.included_songs, dlc_songs)
|
||||
|
||||
# Making sure songs chosen for goal are allowed by DLC and remove the chosen from being added to the pool.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"game": "Muse Dash",
|
||||
"authors": ["DeamonHunter"],
|
||||
"world_version": "1.5.29",
|
||||
"world_version": "1.5.30",
|
||||
"minimum_ap_version": "0.6.3"
|
||||
}
|
||||
@@ -10,6 +10,7 @@ class DifficultyRanges(MuseDashTestBase):
|
||||
"PeroPero in the Universe",
|
||||
"umpopoff",
|
||||
"P E R O P E R O Brother Dance",
|
||||
"Master Bancho's Sushi Class",
|
||||
]
|
||||
|
||||
def test_all_difficulty_ranges(self) -> None:
|
||||
@@ -78,7 +79,7 @@ class DifficultyRanges(MuseDashTestBase):
|
||||
|
||||
# Some songs are weird and have less than the usual 3 difficulties.
|
||||
# So this override is to avoid failing on these songs.
|
||||
if song_name in ("umpopoff", "P E R O P E R O Brother Dance"):
|
||||
if song_name in ("umpopoff", "P E R O P E R O Brother Dance", "Master Bancho's Sushi Class"):
|
||||
self.assertTrue(song.easy is None and song.hard is not None and song.master is None,
|
||||
f"Song '{song_name}' difficulty not set when it should be.")
|
||||
else:
|
||||
|
||||
@@ -263,6 +263,14 @@ class PokemonEmeraldWorld(World):
|
||||
if self.options.hms == RandomizeHms.option_shuffle:
|
||||
self.options.local_items.value.update(self.item_name_groups["HM"])
|
||||
|
||||
# Manually enable Latios as a dexsanity location if we're doing legendary hunt (which confines Latios to
|
||||
# the roamer encounter), the player allows Latios as a valid legendary hunt target, and they didn't also
|
||||
# blacklist Latios to remove its dexsanity location
|
||||
if self.options.goal == Goal.option_legendary_hunt and self.options.dexsanity \
|
||||
and "Latios" in self.options.allowed_legendary_hunt_encounters.value \
|
||||
and emerald_data.constants["SPECIES_LATIOS"] not in self.blacklisted_wilds:
|
||||
self.allowed_dexsanity_species.add(emerald_data.constants["SPECIES_LATIOS"])
|
||||
|
||||
def create_regions(self) -> None:
|
||||
from .regions import create_regions
|
||||
all_regions = create_regions(self)
|
||||
|
||||
@@ -376,10 +376,10 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
|
||||
|
||||
# Actually create the new list of slots and encounter table
|
||||
new_slots: List[int] = []
|
||||
if encounter_type in enabled_encounters:
|
||||
world.allowed_dexsanity_species.update(table.slots)
|
||||
for species_id in table.slots:
|
||||
new_slots.append(species_old_to_new_map[species_id])
|
||||
if encounter_type in enabled_encounters:
|
||||
world.allowed_dexsanity_species.update(new_slots)
|
||||
|
||||
new_encounters[encounter_type] = EncounterTableData(new_slots, table.address)
|
||||
|
||||
|
||||
@@ -1559,7 +1559,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
# Legendary hunt prevents Latios from being a wild spawn so the roamer
|
||||
# can be tracked, and also guarantees that the roamer is a Latios.
|
||||
if world.options.goal == Goal.option_legendary_hunt and \
|
||||
data.constants["SPECIES_LATIOS"] not in world.blacklisted_wilds:
|
||||
data.constants["SPECIES_LATIOS"] in world.allowed_dexsanity_species:
|
||||
set_rule(
|
||||
get_location(f"Pokedex - Latios"),
|
||||
lambda state: state.has("EVENT_ENCOUNTER_LATIOS", world.player)
|
||||
|
||||
@@ -88,16 +88,19 @@ class SatisfactoryWorld(World):
|
||||
self.items.build_item_pool(self.random, precollected_items, number_of_locations)
|
||||
|
||||
def set_rules(self) -> None:
|
||||
resource_sink_goal: bool = "AWESOME Sink Points (total)" in self.options.goal_selection \
|
||||
or "AWESOME Sink Points (per minute)" in self.options.goal_selection
|
||||
|
||||
required_parts = set(self.game_logic.space_elevator_phases[self.options.final_elevator_phase.value - 1].keys())
|
||||
required_buildings = set()
|
||||
|
||||
if resource_sink_goal:
|
||||
required_parts.union(self.game_logic.buildings["AWESOME Sink"].inputs)
|
||||
if "Space Elevator Phase" in self.options.goal_selection:
|
||||
required_buildings.add("Space Elevator")
|
||||
|
||||
if "AWESOME Sink Points (total)" in self.options.goal_selection \
|
||||
or "AWESOME Sink Points (per minute)" in self.options.goal_selection:
|
||||
required_buildings.add("AWESOME Sink")
|
||||
|
||||
self.multiworld.completion_condition[self.player] = \
|
||||
lambda state: self.state_logic.can_produce_all(state, required_parts)
|
||||
lambda state: self.state_logic.can_produce_all(state, required_parts) \
|
||||
and self.state_logic.can_build_all(state, required_buildings)
|
||||
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
change = super().collect(state, item)
|
||||
@@ -244,14 +247,14 @@ class SatisfactoryWorld(World):
|
||||
or self.options.awesome_logic_placement.value == Placement.starting_inventory:
|
||||
locations_visible_from_start.update(range(1338700, 1338709)) # ids of shop locations 1 to 10
|
||||
|
||||
location_names_with_useful_items: Iterable[str] = [
|
||||
location.name
|
||||
for location in self.get_locations()
|
||||
if location.address in locations_visible_from_start and location.item \
|
||||
and location.item.flags & (ItemClassification.progression | ItemClassification.useful) > 0
|
||||
]
|
||||
location_names_with_useful_items: Iterable[str] = [
|
||||
location.name
|
||||
for location in self.get_locations()
|
||||
if location.address in locations_visible_from_start and location.item \
|
||||
and location.item.flags & (ItemClassification.progression | ItemClassification.useful) > 0
|
||||
]
|
||||
|
||||
self.options.start_location_hints.value.update(location_names_with_useful_items)
|
||||
self.options.start_location_hints.value.update(location_names_with_useful_items)
|
||||
|
||||
def push_precollected_by_name(self, item_name: str) -> None:
|
||||
item = self.create_item(item_name)
|
||||
|
||||
@@ -156,15 +156,17 @@ This page includes all data associated with all games.
|
||||
|
||||
## How do I join a MultiWorld game?
|
||||
|
||||
1. Run ArchipelagoStarcraft2Client.exe.
|
||||
1. Run ArchipelagoLauncher.exe.
|
||||
- macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step
|
||||
only.
|
||||
2. In the Archipelago tab, type `/connect [server IP]`.
|
||||
2. Search for the Starcraft 2 Client in the launcher to open the game-specific client
|
||||
- Alternatively, steps 1 and 2 can be combined by providing the `"Starcraft 2 Client"` launch argument to the launcher.
|
||||
3. In the Archipelago tab, type `/connect [server IP]`.
|
||||
- If you're running through the website, the server IP should be displayed near the top of the room page.
|
||||
- The server IP may also be typed into the top bar, and then clicking "Connect"
|
||||
3. Type your slot name from your YAML when prompted.
|
||||
4. If the server has a password, enter that when prompted.
|
||||
5. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see all the missions in your
|
||||
4. Type your slot name from your YAML when prompted.
|
||||
5. If the server has a password, enter that when prompted.
|
||||
6. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see all the missions in your
|
||||
world.
|
||||
|
||||
Unreachable missions will have greyed-out text. Completed missions (all locations collected) will have white text.
|
||||
@@ -173,7 +175,22 @@ Mission buttons will have a color corresponding to the faction you play as in th
|
||||
|
||||
Click on an available mission to start it.
|
||||
|
||||
## The game isn't launching when I try to start a mission.
|
||||
## Troubleshooting
|
||||
|
||||
### I can't connect to my seed.
|
||||
|
||||
Rooms on the Archipelago website go to sleep after two hours of inactivity; reload or refresh the room page
|
||||
to start them back up.
|
||||
When restarting the room, the connection port may change (the numbers after "archipelago.gg:"),
|
||||
make sure that is accurate.
|
||||
Your slot name should be displayed on the room page as well; make sure that exactly matches the slot name you
|
||||
type into your client, and note that it is case-sensitive.
|
||||
|
||||
If none of these things solve the problem, visit the [Discord](https://discord.com/invite/8Z65BR2) and check
|
||||
the #software-announcements channel to see if there's a listed outage, or visit the #starcraft-2 channel for
|
||||
tech support.
|
||||
|
||||
### The game isn't launching when I try to start a mission.
|
||||
|
||||
Usually, this is caused by the mod files not being downloaded.
|
||||
Make sure you have run `/download_data` in the Archipelago tab before playing.
|
||||
@@ -183,12 +200,12 @@ Make sure that you are running an up-to-date version of the client.
|
||||
Check the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) to
|
||||
look up what the latest version is (RC releases are not necessary; that stands for "Release Candidate").
|
||||
|
||||
If these things are in order, check the log file for issues (stored at `[Archipelago Directory]/logs/Starcraft2Client.txt`).
|
||||
If these things are in order, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client_<date>.txt`).
|
||||
If you can't figure out the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel
|
||||
for help.
|
||||
Please include a specific description of what's going wrong and attach your log file to your message.
|
||||
|
||||
## My keyboard shortcuts profile is not available when I play *StarCraft 2 Archipelago*.
|
||||
### My keyboard shortcuts profile is not available when I play *StarCraft 2 Archipelago*.
|
||||
|
||||
For your keyboard shortcuts profile to work in Archipelago, you need to copy your shortcuts file from
|
||||
`Documents/StarCraft II/Accounts/######/Hotkeys` to `Documents/StarCraft II/Hotkeys`.
|
||||
|
||||
@@ -249,7 +249,6 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
|
||||
LocationType.VICTORY,
|
||||
lambda state: (
|
||||
logic.terran_common_unit(state)
|
||||
and logic.terran_defense_rating(state, True) >= 2
|
||||
and (adv_tactics or logic.terran_basic_anti_air(state))
|
||||
),
|
||||
),
|
||||
@@ -271,10 +270,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
|
||||
"Third Group Rescued",
|
||||
SC2WOL_LOC_ID_OFFSET + 303,
|
||||
LocationType.VANILLA,
|
||||
lambda state: (
|
||||
logic.terran_common_unit(state)
|
||||
and logic.terran_defense_rating(state, True) >= 2
|
||||
),
|
||||
logic.terran_common_unit,
|
||||
),
|
||||
make_location_data(
|
||||
SC2Mission.ZERO_HOUR.mission_name,
|
||||
@@ -320,20 +316,14 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
|
||||
"Hold Just a Little Longer",
|
||||
SC2WOL_LOC_ID_OFFSET + 309,
|
||||
LocationType.EXTRA,
|
||||
lambda state: (
|
||||
logic.terran_common_unit(state)
|
||||
and logic.terran_defense_rating(state, True) >= 2
|
||||
),
|
||||
logic.terran_common_unit,
|
||||
),
|
||||
make_location_data(
|
||||
SC2Mission.ZERO_HOUR.mission_name,
|
||||
"Cavalry's on the Way",
|
||||
SC2WOL_LOC_ID_OFFSET + 310,
|
||||
LocationType.EXTRA,
|
||||
lambda state: (
|
||||
logic.terran_common_unit(state)
|
||||
and logic.terran_defense_rating(state, True) >= 2
|
||||
),
|
||||
logic.terran_common_unit,
|
||||
),
|
||||
make_location_data(
|
||||
SC2Mission.EVACUATION.mission_name,
|
||||
|
||||
@@ -182,7 +182,7 @@ class ValidInventory:
|
||||
del self.logical_inventory[item.name]
|
||||
item.filter_flags |= remove_flag
|
||||
return ""
|
||||
|
||||
|
||||
def remove_child_items(
|
||||
parent_item: StarcraftItem,
|
||||
remove_flag: ItemFilterFlags = ItemFilterFlags.FilterExcluded,
|
||||
@@ -247,13 +247,13 @@ class ValidInventory:
|
||||
|
||||
# Limit the maximum number of upgrades
|
||||
if max_upgrades_per_unit != -1:
|
||||
for group_name, group_items in group_to_item.items():
|
||||
self.world.random.shuffle(group_to_item[group])
|
||||
for group_items in group_to_item.values():
|
||||
self.world.random.shuffle(group_items)
|
||||
cull_items_over_maximum(group_items, max_upgrades_per_unit)
|
||||
|
||||
|
||||
# Requesting minimum upgrades for items that have already been locked/placed when minimum required
|
||||
if min_upgrades_per_unit != -1:
|
||||
for group_name, group_items in group_to_item.items():
|
||||
for group_items in group_to_item.values():
|
||||
self.world.random.shuffle(group_items)
|
||||
request_minimum_items(group_items, min_upgrades_per_unit)
|
||||
|
||||
@@ -349,7 +349,7 @@ class ValidInventory:
|
||||
ItemFilterFlags.Removed not in item.filter_flags
|
||||
and ((ItemFilterFlags.Unexcludable|ItemFilterFlags.Excluded) & item.filter_flags) != ItemFilterFlags.Excluded
|
||||
)
|
||||
|
||||
|
||||
# Actually remove culled items; we won't re-add them
|
||||
inventory = [
|
||||
item for item in inventory
|
||||
@@ -373,7 +373,7 @@ class ValidInventory:
|
||||
item for item in cullable_items
|
||||
if not ((ItemFilterFlags.Removed|ItemFilterFlags.Uncullable) & item.filter_flags)
|
||||
]
|
||||
|
||||
|
||||
# Handle too many requested
|
||||
if current_inventory_size - start_inventory_size > inventory_size - filler_amount:
|
||||
for item in inventory:
|
||||
@@ -414,7 +414,7 @@ class ValidInventory:
|
||||
removable_transport_hooks = [item for item in inventory_transport_hooks if not (ItemFilterFlags.Unexcludable & item.filter_flags)]
|
||||
if len(inventory_transport_hooks) > 1 and removable_transport_hooks:
|
||||
inventory.remove(removable_transport_hooks[0])
|
||||
|
||||
|
||||
# Weapon/Armour upgrades
|
||||
def exclude_wa(prefix: str) -> List[StarcraftItem]:
|
||||
return [
|
||||
@@ -439,7 +439,7 @@ class ValidInventory:
|
||||
inventory = exclude_wa(item_names.PROTOSS_GROUND_UPGRADE_PREFIX)
|
||||
if used_item_names.isdisjoint(item_groups.protoss_air_wa):
|
||||
inventory = exclude_wa(item_names.PROTOSS_AIR_UPGRADE_PREFIX)
|
||||
|
||||
|
||||
# Part 4: Last-ditch effort to reduce inventory size; upgrades can go in start inventory
|
||||
current_inventory_size = len(inventory)
|
||||
precollect_items = current_inventory_size - inventory_size - start_inventory_size - filler_amount
|
||||
@@ -453,7 +453,7 @@ class ValidInventory:
|
||||
for item in promotable[:precollect_items]:
|
||||
item.filter_flags |= ItemFilterFlags.StartInventory
|
||||
start_inventory_size += 1
|
||||
|
||||
|
||||
assert current_inventory_size - start_inventory_size <= inventory_size - filler_amount, (
|
||||
f"Couldn't reduce inventory to fit. target={inventory_size}, poolsize={current_inventory_size}, "
|
||||
f"start_inventory={starcraft_item}, filler_amount={filler_amount}"
|
||||
|
||||
@@ -129,7 +129,7 @@ def adjust_mission_pools(world: 'SC2World', pools: SC2MOGenMissionPools):
|
||||
if grant_story_tech == GrantStoryTech.option_grant:
|
||||
# Additional starter mission if player is granted story tech
|
||||
pools.move_mission(SC2Mission.ENEMY_WITHIN, Difficulty.EASY, Difficulty.STARTER)
|
||||
pools.move_mission(SC2Mission.THE_ESCAPE, Difficulty.MEDIUM, Difficulty.STARTER)
|
||||
pools.move_mission(SC2Mission.THE_ESCAPE, Difficulty.EASY, Difficulty.STARTER)
|
||||
pools.move_mission(SC2Mission.IN_THE_ENEMY_S_SHADOW, Difficulty.MEDIUM, Difficulty.STARTER)
|
||||
if not war_council_nerfs or grant_story_tech == GrantStoryTech.option_grant:
|
||||
pools.move_mission(SC2Mission.TEMPLAR_S_RETURN, Difficulty.MEDIUM, Difficulty.STARTER)
|
||||
|
||||
@@ -1660,11 +1660,11 @@ class SC2Logic:
|
||||
Created mainly for engine of destruction start, but works for other missions with no-build starts.
|
||||
"""
|
||||
return state.has_any((
|
||||
item_names.ZEALOT_WHIRLWIND,
|
||||
item_names.SENTRY_DOUBLE_SHIELD_RECHARGE,
|
||||
item_names.SLAYER_PHASE_BLINK,
|
||||
item_names.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES,
|
||||
item_names.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION,
|
||||
item_names.ZEALOT_WHIRLWIND,
|
||||
item_names.SENTRY_DOUBLE_SHIELD_RECHARGE,
|
||||
item_names.SLAYER_PHASE_BLINK,
|
||||
item_names.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES,
|
||||
item_names.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION,
|
||||
), self.player)
|
||||
|
||||
# Mission-specific rules
|
||||
|
||||
52
worlds/sc2/test/slow_tests.py
Normal file
52
worlds/sc2/test/slow_tests.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Slow-running tests that are run infrequently.
|
||||
Run this file explicitly with `python3 -m unittest worlds.sc2.test.slow_tests`
|
||||
"""
|
||||
from .test_base import Sc2SetupTestBase
|
||||
|
||||
from Fill import FillError
|
||||
from .. import mission_tables, options
|
||||
|
||||
|
||||
class LargeTests(Sc2SetupTestBase):
|
||||
def test_any_starter_mission_works(self) -> None:
|
||||
base_options = {
|
||||
options.OPTION_NAME[options.SelectedRaces]: list(options.SelectedRaces.valid_keys),
|
||||
options.OPTION_NAME[options.RequiredTactics]: options.RequiredTactics.option_standard,
|
||||
options.OPTION_NAME[options.MissionOrder]: options.MissionOrder.option_custom,
|
||||
options.OPTION_NAME[options.ExcludeOverpoweredItems]: True,
|
||||
# options.OPTION_NAME[options.ExtraLocations]: options.ExtraLocations.option_disabled,
|
||||
options.OPTION_NAME[options.VanillaLocations]: options.VanillaLocations.option_disabled,
|
||||
}
|
||||
missions_to_check = [
|
||||
mission for mission in mission_tables.SC2Mission
|
||||
if mission.pool == mission_tables.MissionPools.STARTER
|
||||
]
|
||||
failed_missions: list[tuple[mission_tables.SC2Mission, int]] = []
|
||||
NUM_ATTEMPTS = 3
|
||||
for mission in missions_to_check:
|
||||
for attempt in range(NUM_ATTEMPTS):
|
||||
mission_options = base_options | {
|
||||
options.OPTION_NAME[options.CustomMissionOrder]: {
|
||||
"Test Campaign": {
|
||||
"Test Layout": {
|
||||
"type": "hopscotch",
|
||||
"size": 25,
|
||||
"goal": True,
|
||||
"missions": [
|
||||
{"index": 0, "mission_pool": [mission.mission_name]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
try:
|
||||
self.generate_world(mission_options)
|
||||
self.fill_after_generation()
|
||||
assert self.multiworld.worlds[1].custom_mission_order.get_starting_missions()[0] == mission
|
||||
except FillError as ex:
|
||||
failed_missions.append((mission, self.multiworld.seed))
|
||||
if failed_missions:
|
||||
for failed_mission in failed_missions:
|
||||
print(failed_mission)
|
||||
self.assertFalse(failed_missions)
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import *
|
||||
from typing import Any, cast
|
||||
import unittest
|
||||
import random
|
||||
from argparse import Namespace
|
||||
@@ -6,18 +6,11 @@ from BaseClasses import MultiWorld, CollectionState, PlandoOptions
|
||||
from Generate import get_seed_name
|
||||
from worlds import AutoWorld
|
||||
from test.general import gen_steps, call_all
|
||||
from Fill import distribute_items_restrictive
|
||||
|
||||
from test.bases import WorldTestBase
|
||||
from .. import SC2World, SC2Campaign
|
||||
from .. import client
|
||||
from .. import options
|
||||
|
||||
class Sc2TestBase(WorldTestBase):
|
||||
game = client.SC2Context.game
|
||||
world: SC2World
|
||||
player: ClassVar[int] = 1
|
||||
skip_long_tests: bool = True
|
||||
|
||||
|
||||
class Sc2SetupTestBase(unittest.TestCase):
|
||||
"""
|
||||
@@ -37,10 +30,11 @@ class Sc2SetupTestBase(unittest.TestCase):
|
||||
PROTOSS_CAMPAIGNS = {
|
||||
'enabled_campaigns': {SC2Campaign.PROPHECY.campaign_name, SC2Campaign.PROLOGUE.campaign_name, SC2Campaign.LOTV.campaign_name,}
|
||||
}
|
||||
seed: Optional[int] = None
|
||||
seed: int | None = None
|
||||
game = SC2World.game
|
||||
player = 1
|
||||
def generate_world(self, options: Dict[str, Any]) -> None:
|
||||
|
||||
def generate_world(self, options: dict[str, Any]) -> None:
|
||||
self.multiworld = MultiWorld(1)
|
||||
self.multiworld.game[self.player] = self.game
|
||||
self.multiworld.player_name = {self.player: "Tester"}
|
||||
@@ -63,3 +57,11 @@ class Sc2SetupTestBase(unittest.TestCase):
|
||||
except Exception as ex:
|
||||
ex.add_note(f"Seed: {self.multiworld.seed}")
|
||||
raise
|
||||
|
||||
def fill_after_generation(self) -> None:
|
||||
assert self.multiworld
|
||||
try:
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
except Exception as ex:
|
||||
ex.add_note(f"Seed: {self.multiworld.seed}")
|
||||
raise
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
"""
|
||||
Unit tests for world generation
|
||||
"""
|
||||
from typing import *
|
||||
|
||||
from typing import Any
|
||||
from .test_base import Sc2SetupTestBase
|
||||
|
||||
from .. import mission_groups, mission_tables, options, locations, SC2Mission, SC2Campaign, SC2Race, unreleased_items, \
|
||||
RequiredTactics
|
||||
from .. import (
|
||||
mission_groups, mission_tables, options, locations,
|
||||
SC2Mission, SC2Campaign, SC2Race, unreleased_items,
|
||||
RequiredTactics,
|
||||
)
|
||||
from ..item import item_groups, item_tables, item_names
|
||||
from .. import get_all_missions, get_random_first_mission
|
||||
from ..options import EnabledCampaigns, NovaGhostOfAChanceVariant, MissionOrder, ExcludeOverpoweredItems, \
|
||||
VanillaItemsOnly, MaximumCampaignSize
|
||||
from ..options import (
|
||||
EnabledCampaigns, NovaGhostOfAChanceVariant, MissionOrder, ExcludeOverpoweredItems,
|
||||
VanillaItemsOnly, MaximumCampaignSize,
|
||||
)
|
||||
|
||||
|
||||
class TestItemFiltering(Sc2SetupTestBase):
|
||||
def test_explicit_locks_excludes_interact_and_set_flags(self):
|
||||
def test_explicit_locks_excludes_interact_and_set_flags(self) -> None:
|
||||
world_options = {
|
||||
**self.ALL_CAMPAIGNS,
|
||||
'locked_items': {
|
||||
@@ -46,7 +50,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
regen_biosteel_items = [x for x in itempool if x == item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL]
|
||||
self.assertEqual(len(regen_biosteel_items), 2)
|
||||
|
||||
def test_unexcludes_cancel_out_excludes(self):
|
||||
def test_unexcludes_cancel_out_excludes(self) -> None:
|
||||
world_options = {
|
||||
'grant_story_tech': options.GrantStoryTech.option_grant,
|
||||
'excluded_items': {
|
||||
@@ -121,7 +125,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
itempool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertNotIn(item_names.MARINE, itempool)
|
||||
|
||||
def test_excluding_groups_excludes_all_items_in_group(self):
|
||||
def test_excluding_groups_excludes_all_items_in_group(self) -> None:
|
||||
world_options = {
|
||||
'excluded_items': {
|
||||
item_groups.ItemGroupNames.BARRACKS_UNITS.lower(): -1,
|
||||
@@ -133,7 +137,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
for item_name in item_groups.barracks_units:
|
||||
self.assertNotIn(item_name, itempool)
|
||||
|
||||
def test_excluding_mission_groups_excludes_all_missions_in_group(self):
|
||||
def test_excluding_mission_groups_excludes_all_missions_in_group(self) -> None:
|
||||
world_options = {
|
||||
**self.ZERG_CAMPAIGNS,
|
||||
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
|
||||
@@ -164,7 +168,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
self.assertNotEqual(item_data.type, item_tables.TerranItemType.Nova_Gear)
|
||||
self.assertNotEqual(item_name, item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE)
|
||||
|
||||
def test_starter_unit_populates_start_inventory(self):
|
||||
def test_starter_unit_populates_start_inventory(self) -> None:
|
||||
world_options = {
|
||||
'enabled_campaigns': {
|
||||
SC2Campaign.WOL.campaign_name,
|
||||
@@ -308,7 +312,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
self.generate_world(world_options)
|
||||
world_items = [(item.name, item_tables.item_table[item.name]) for item in self.multiworld.itempool]
|
||||
self.assertTrue(world_items)
|
||||
occurrences: Dict[str, int] = {}
|
||||
occurrences: dict[str, int] = {}
|
||||
for item_name, _ in world_items:
|
||||
if item_name in item_groups.terran_progressive_items:
|
||||
if item_name in item_groups.nova_equipment:
|
||||
@@ -528,7 +532,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
Orbital command got replaced. The item is still there for backwards compatibility.
|
||||
It shouldn't be generated.
|
||||
"""
|
||||
world_options = {}
|
||||
world_options: dict[str, Any] = {}
|
||||
|
||||
self.generate_world(world_options)
|
||||
itempool = [item.name for item in self.multiworld.itempool]
|
||||
@@ -595,7 +599,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
self.assertIn(speedrun_location_name, all_location_names)
|
||||
self.assertNotIn(speedrun_location_name, world_location_names)
|
||||
|
||||
def test_nco_and_wol_picks_correct_starting_mission(self):
|
||||
def test_nco_and_wol_picks_correct_starting_mission(self) -> None:
|
||||
world_options = {
|
||||
'mission_order': MissionOrder.option_vanilla,
|
||||
'enabled_campaigns': {
|
||||
@@ -606,7 +610,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
self.generate_world(world_options)
|
||||
self.assertEqual(get_random_first_mission(self.world, self.world.custom_mission_order), mission_tables.SC2Mission.LIBERATION_DAY)
|
||||
|
||||
def test_excluding_mission_short_name_excludes_all_variants_of_mission(self):
|
||||
def test_excluding_mission_short_name_excludes_all_variants_of_mission(self) -> None:
|
||||
world_options = {
|
||||
'excluded_missions': [
|
||||
mission_tables.SC2Mission.ZERO_HOUR.mission_name.split(" (")[0]
|
||||
@@ -625,7 +629,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
self.assertNotIn(mission_tables.SC2Mission.ZERO_HOUR_Z, missions)
|
||||
self.assertNotIn(mission_tables.SC2Mission.ZERO_HOUR_P, missions)
|
||||
|
||||
def test_excluding_mission_variant_excludes_just_that_variant(self):
|
||||
def test_excluding_mission_variant_excludes_just_that_variant(self) -> None:
|
||||
world_options = {
|
||||
'excluded_missions': [
|
||||
mission_tables.SC2Mission.ZERO_HOUR.mission_name
|
||||
@@ -644,7 +648,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
self.assertIn(mission_tables.SC2Mission.ZERO_HOUR_Z, missions)
|
||||
self.assertIn(mission_tables.SC2Mission.ZERO_HOUR_P, missions)
|
||||
|
||||
def test_weapon_armor_upgrades(self):
|
||||
def test_weapon_armor_upgrades(self) -> None:
|
||||
world_options = {
|
||||
# Vanilla WoL with all missions
|
||||
'mission_order': options.MissionOrder.option_vanilla,
|
||||
@@ -682,7 +686,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
self.assertGreaterEqual(len(vehicle_weapon_items), 3)
|
||||
self.assertEqual(len(other_bundle_items), 0)
|
||||
|
||||
def test_weapon_armor_upgrades_with_bundles(self):
|
||||
def test_weapon_armor_upgrades_with_bundles(self) -> None:
|
||||
world_options = {
|
||||
# Vanilla WoL with all missions
|
||||
'mission_order': options.MissionOrder.option_vanilla,
|
||||
@@ -720,7 +724,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
self.assertGreaterEqual(len(vehicle_upgrade_items), 3)
|
||||
self.assertEqual(len(other_bundle_items), 0)
|
||||
|
||||
def test_weapon_armor_upgrades_all_in_air(self):
|
||||
def test_weapon_armor_upgrades_all_in_air(self) -> None:
|
||||
world_options = {
|
||||
# Vanilla WoL with all missions
|
||||
'mission_order': options.MissionOrder.option_vanilla,
|
||||
@@ -753,7 +757,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
self.assertGreaterEqual(len(vehicle_weapon_items), 3)
|
||||
self.assertGreaterEqual(len(ship_weapon_items), 3)
|
||||
|
||||
def test_weapon_armor_upgrades_generic_upgrade_missions(self):
|
||||
def test_weapon_armor_upgrades_generic_upgrade_missions(self) -> None:
|
||||
"""
|
||||
Tests the case when there aren't enough missions in order to get required weapon/armor upgrades
|
||||
for logic requirements.
|
||||
@@ -782,7 +786,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
# Under standard tactics you need to place L3 upgrades for available unit classes
|
||||
self.assertEqual(len(upgrade_items), 3)
|
||||
|
||||
def test_weapon_armor_upgrades_generic_upgrade_missions_no_logic(self):
|
||||
def test_weapon_armor_upgrades_generic_upgrade_missions_no_logic(self) -> None:
|
||||
"""
|
||||
Tests the case when there aren't enough missions in order to get required weapon/armor upgrades
|
||||
for logic requirements.
|
||||
@@ -813,7 +817,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
# No logic won't take the fallback to trigger
|
||||
self.assertEqual(len(upgrade_items), 0)
|
||||
|
||||
def test_weapon_armor_upgrades_generic_upgrade_missions_no_countermeasure_needed(self):
|
||||
def test_weapon_armor_upgrades_generic_upgrade_missions_no_countermeasure_needed(self) -> None:
|
||||
world_options = {
|
||||
# Vanilla WoL with all missions
|
||||
'mission_order': options.MissionOrder.option_vanilla,
|
||||
@@ -837,7 +841,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
# No additional starting inventory item placement is needed
|
||||
self.assertEqual(len(upgrade_items), 0)
|
||||
|
||||
def test_kerrigan_levels_per_mission_triggering_pre_fill(self):
|
||||
def test_kerrigan_levels_per_mission_triggering_pre_fill(self) -> None:
|
||||
world_options = {
|
||||
**self.ALL_CAMPAIGNS,
|
||||
'mission_order': options.MissionOrder.option_custom,
|
||||
@@ -878,7 +882,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
|
||||
self.assertGreater(len(kerrigan_1_stacks), 0)
|
||||
|
||||
def test_kerrigan_levels_per_mission_and_generic_upgrades_both_triggering_pre_fill(self):
|
||||
def test_kerrigan_levels_per_mission_and_generic_upgrades_both_triggering_pre_fill(self) -> None:
|
||||
world_options = {
|
||||
**self.ALL_CAMPAIGNS,
|
||||
'mission_order': options.MissionOrder.option_custom,
|
||||
@@ -925,7 +929,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
self.assertNotIn(item_names.KERRIGAN_LEVELS_70, itempool)
|
||||
self.assertNotIn(item_names.KERRIGAN_LEVELS_70, starting_inventory)
|
||||
|
||||
def test_locking_required_items(self):
|
||||
def test_locking_required_items(self) -> None:
|
||||
world_options = {
|
||||
**self.ALL_CAMPAIGNS,
|
||||
'mission_order': options.MissionOrder.option_custom,
|
||||
@@ -962,7 +966,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
self.assertIn(item_names.KERRIGAN_MEND, itempool)
|
||||
|
||||
|
||||
def test_fully_balanced_mission_races(self):
|
||||
def test_fully_balanced_mission_races(self) -> None:
|
||||
"""
|
||||
Tests whether fully balanced mission race balancing actually is fully balanced.
|
||||
"""
|
||||
@@ -1080,7 +1084,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
|
||||
self.generate_world(world_options)
|
||||
itempool = [item.name for item in self.multiworld.itempool]
|
||||
upgrade_item_counts: Dict[str, int] = {}
|
||||
upgrade_item_counts: dict[str, int] = {}
|
||||
for item_name in itempool:
|
||||
if item_tables.item_table[item_name].type in (
|
||||
item_tables.TerranItemType.Upgrade,
|
||||
@@ -1252,7 +1256,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
self.generate_world(world_options)
|
||||
itempool = [item.name for item in self.multiworld.itempool]
|
||||
|
||||
items_to_check: List[str] = unreleased_items
|
||||
items_to_check: list[str] = unreleased_items
|
||||
for item in items_to_check:
|
||||
self.assertNotIn(item, itempool)
|
||||
|
||||
@@ -1273,7 +1277,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
self.generate_world(world_options)
|
||||
itempool = [item.name for item in self.multiworld.itempool]
|
||||
|
||||
items_to_check: List[str] = unreleased_items
|
||||
items_to_check: list[str] = unreleased_items
|
||||
for item in items_to_check:
|
||||
self.assertIn(item, itempool)
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import unittest
|
||||
from .test_base import Sc2TestBase
|
||||
from .test_base import Sc2SetupTestBase
|
||||
from .. import mission_tables, SC2Campaign
|
||||
from .. import options
|
||||
from ..mission_order.layout_types import Grid
|
||||
|
||||
|
||||
class TestGridsizes(unittest.TestCase):
|
||||
def test_grid_sizes_meet_specs(self):
|
||||
self.assertTupleEqual((1, 2, 0), Grid.get_grid_dimensions(2))
|
||||
@@ -24,17 +25,17 @@ class TestGridsizes(unittest.TestCase):
|
||||
self.assertTupleEqual((5, 7, 2), Grid.get_grid_dimensions(33))
|
||||
|
||||
|
||||
class TestGridGeneration(Sc2TestBase):
|
||||
options = {
|
||||
"mission_order": options.MissionOrder.option_grid,
|
||||
"excluded_missions": [mission_tables.SC2Mission.ZERO_HOUR.mission_name,],
|
||||
"enabled_campaigns": {
|
||||
SC2Campaign.WOL.campaign_name,
|
||||
SC2Campaign.PROPHECY.campaign_name,
|
||||
}
|
||||
}
|
||||
|
||||
class TestGridGeneration(Sc2SetupTestBase):
|
||||
def test_size_matches_exclusions(self):
|
||||
world_options = {
|
||||
options.OPTION_NAME[options.MissionOrder]: options.MissionOrder.option_grid,
|
||||
options.OPTION_NAME[options.ExcludedMissions]: [mission_tables.SC2Mission.ZERO_HOUR.mission_name],
|
||||
options.OPTION_NAME[options.EnabledCampaigns]: {
|
||||
SC2Campaign.WOL.campaign_name,
|
||||
SC2Campaign.PROPHECY.campaign_name,
|
||||
}
|
||||
}
|
||||
self.generate_world(world_options)
|
||||
self.assertNotIn(mission_tables.SC2Mission.ZERO_HOUR.mission_name, self.multiworld.regions)
|
||||
# WoL has 29 missions. -1 for Zero Hour being excluded, +1 for the automatically-added menu location
|
||||
self.assertEqual(len(self.multiworld.regions), 29)
|
||||
|
||||
@@ -257,7 +257,7 @@ algorerhythm_bundle = BundleTemplate(CCRoom.bulletin_board, MemeBundleName.algor
|
||||
red_fish_items = [red_mullet, red_snapper, lava_eel, crimsonfish]
|
||||
blue_fish_items = [anchovy, tuna, sardine, bream, squid, ice_pip, albacore, blue_discus, midnight_squid, spook_fish, glacierfish]
|
||||
other_fish = [pufferfish, largemouth_bass, smallmouth_bass, rainbow_trout, walleye, perch, carp, catfish, pike, sunfish, herring, eel, octopus, sea_cucumber,
|
||||
super_cucumber, ghostfish, stonefish, sandfish, scorpion_carp, flounder, midnight_carp, tigerseye, bullhead, tilapia, chub, dorado, shad,
|
||||
super_cucumber, ghostfish, stonefish, sandfish, scorpion_carp, flounder, midnight_carp, bullhead, tilapia, chub, dorado, shad, tiger_trout,
|
||||
lingcod, halibut, slimejack, stingray, goby, blobfish, angler, legend, mutant_carp]
|
||||
dr_seuss_items = [other_fish, [fish.as_amount(2) for fish in other_fish], red_fish_items, blue_fish_items]
|
||||
dr_seuss_bundle = FixedPriceDeepBundleTemplate(CCRoom.crafts_room, MemeBundleName.dr_seuss, dr_seuss_items, 4, 4)
|
||||
|
||||
@@ -438,6 +438,8 @@ id,region,name,tags,content_packs
|
||||
906,Traveling Cart Sunday,Traveling Merchant Sunday Item 6,"TRAVELING_MERCHANT",
|
||||
907,Traveling Cart Sunday,Traveling Merchant Sunday Item 7,"TRAVELING_MERCHANT",
|
||||
908,Traveling Cart Sunday,Traveling Merchant Sunday Item 8,"TRAVELING_MERCHANT",
|
||||
909,Traveling Cart Sunday,Traveling Merchant Sunday Item 9,"TRAVELING_MERCHANT",
|
||||
910,Traveling Cart Sunday,Traveling Merchant Sunday Item 10,"TRAVELING_MERCHANT",
|
||||
911,Traveling Cart Monday,Traveling Merchant Monday Item 1,"MANDATORY,TRAVELING_MERCHANT",
|
||||
912,Traveling Cart Monday,Traveling Merchant Monday Item 2,"TRAVELING_MERCHANT",
|
||||
913,Traveling Cart Monday,Traveling Merchant Monday Item 3,"TRAVELING_MERCHANT",
|
||||
@@ -446,6 +448,8 @@ id,region,name,tags,content_packs
|
||||
916,Traveling Cart Monday,Traveling Merchant Monday Item 6,"TRAVELING_MERCHANT",
|
||||
917,Traveling Cart Monday,Traveling Merchant Monday Item 7,"TRAVELING_MERCHANT",
|
||||
918,Traveling Cart Monday,Traveling Merchant Monday Item 8,"TRAVELING_MERCHANT",
|
||||
919,Traveling Cart Monday,Traveling Merchant Monday Item 9,"TRAVELING_MERCHANT",
|
||||
920,Traveling Cart Monday,Traveling Merchant Monday Item 10,"TRAVELING_MERCHANT",
|
||||
921,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 1,"MANDATORY,TRAVELING_MERCHANT",
|
||||
922,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 2,"TRAVELING_MERCHANT",
|
||||
923,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 3,"TRAVELING_MERCHANT",
|
||||
@@ -454,6 +458,8 @@ id,region,name,tags,content_packs
|
||||
926,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 6,"TRAVELING_MERCHANT",
|
||||
927,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 7,"TRAVELING_MERCHANT",
|
||||
928,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 8,"TRAVELING_MERCHANT",
|
||||
929,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 9,"TRAVELING_MERCHANT",
|
||||
930,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 10,"TRAVELING_MERCHANT",
|
||||
931,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 1,"MANDATORY,TRAVELING_MERCHANT",
|
||||
932,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 2,"TRAVELING_MERCHANT",
|
||||
933,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 3,"TRAVELING_MERCHANT",
|
||||
@@ -462,6 +468,8 @@ id,region,name,tags,content_packs
|
||||
936,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 6,"TRAVELING_MERCHANT",
|
||||
937,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 7,"TRAVELING_MERCHANT",
|
||||
938,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 8,"TRAVELING_MERCHANT",
|
||||
939,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 9,"TRAVELING_MERCHANT",
|
||||
940,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 10,"TRAVELING_MERCHANT",
|
||||
941,Traveling Cart Thursday,Traveling Merchant Thursday Item 1,"MANDATORY,TRAVELING_MERCHANT",
|
||||
942,Traveling Cart Thursday,Traveling Merchant Thursday Item 2,"TRAVELING_MERCHANT",
|
||||
943,Traveling Cart Thursday,Traveling Merchant Thursday Item 3,"TRAVELING_MERCHANT",
|
||||
@@ -470,6 +478,8 @@ id,region,name,tags,content_packs
|
||||
946,Traveling Cart Thursday,Traveling Merchant Thursday Item 6,"TRAVELING_MERCHANT",
|
||||
947,Traveling Cart Thursday,Traveling Merchant Thursday Item 7,"TRAVELING_MERCHANT",
|
||||
948,Traveling Cart Thursday,Traveling Merchant Thursday Item 8,"TRAVELING_MERCHANT",
|
||||
949,Traveling Cart Thursday,Traveling Merchant Thursday Item 9,"TRAVELING_MERCHANT",
|
||||
950,Traveling Cart Thursday,Traveling Merchant Thursday Item 10,"TRAVELING_MERCHANT",
|
||||
951,Traveling Cart Friday,Traveling Merchant Friday Item 1,"MANDATORY,TRAVELING_MERCHANT",
|
||||
952,Traveling Cart Friday,Traveling Merchant Friday Item 2,"TRAVELING_MERCHANT",
|
||||
953,Traveling Cart Friday,Traveling Merchant Friday Item 3,"TRAVELING_MERCHANT",
|
||||
@@ -478,6 +488,8 @@ id,region,name,tags,content_packs
|
||||
956,Traveling Cart Friday,Traveling Merchant Friday Item 6,"TRAVELING_MERCHANT",
|
||||
957,Traveling Cart Friday,Traveling Merchant Friday Item 7,"TRAVELING_MERCHANT",
|
||||
958,Traveling Cart Friday,Traveling Merchant Friday Item 8,"TRAVELING_MERCHANT",
|
||||
959,Traveling Cart Friday,Traveling Merchant Friday Item 9,"TRAVELING_MERCHANT",
|
||||
960,Traveling Cart Friday,Traveling Merchant Friday Item 10,"TRAVELING_MERCHANT",
|
||||
961,Traveling Cart Saturday,Traveling Merchant Saturday Item 1,"MANDATORY,TRAVELING_MERCHANT",
|
||||
962,Traveling Cart Saturday,Traveling Merchant Saturday Item 2,"TRAVELING_MERCHANT",
|
||||
963,Traveling Cart Saturday,Traveling Merchant Saturday Item 3,"TRAVELING_MERCHANT",
|
||||
@@ -486,6 +498,8 @@ id,region,name,tags,content_packs
|
||||
966,Traveling Cart Saturday,Traveling Merchant Saturday Item 6,"TRAVELING_MERCHANT",
|
||||
967,Traveling Cart Saturday,Traveling Merchant Saturday Item 7,"TRAVELING_MERCHANT",
|
||||
968,Traveling Cart Saturday,Traveling Merchant Saturday Item 8,"TRAVELING_MERCHANT",
|
||||
969,Traveling Cart Saturday,Traveling Merchant Saturday Item 9,"TRAVELING_MERCHANT",
|
||||
970,Traveling Cart Saturday,Traveling Merchant Saturday Item 10,"TRAVELING_MERCHANT",
|
||||
1001,Fishing,Fishsanity: Carp,FISHSANITY,
|
||||
1002,Fishing,Fishsanity: Herring,FISHSANITY,
|
||||
1003,Fishing,Fishsanity: Smallmouth Bass,FISHSANITY,
|
||||
@@ -1182,7 +1196,7 @@ id,region,name,tags,content_packs
|
||||
2104,Fishing,Biome Balance,SPECIAL_ORDER_BOARD,
|
||||
2105,Haley's House,Rock Rejuvenation,SPECIAL_ORDER_BOARD,
|
||||
2106,Alex's House,Gifts for George,SPECIAL_ORDER_BOARD,
|
||||
2107,Museum,Fragments of the past,"GINGER_ISLAND,SPECIAL_ORDER_BOARD",
|
||||
2107,Museum,Fragments of the past,"SPECIAL_ORDER_BOARD",
|
||||
2108,Saloon,Gus' Famous Omelet,SPECIAL_ORDER_BOARD,
|
||||
2109,Farm,Crop Order,SPECIAL_ORDER_BOARD,
|
||||
2110,Railroad,Community Cleanup,SPECIAL_ORDER_BOARD,
|
||||
@@ -2227,7 +2241,7 @@ id,region,name,tags,content_packs
|
||||
3530,Farm,Craft Cookout Kit,"CRAFTSANITY,CRAFTSANITY_CRAFT",
|
||||
3531,Farm,Craft Fish Smoker,"CRAFTSANITY,CRAFTSANITY_CRAFT",
|
||||
3532,Farm,Craft Dehydrator,"CRAFTSANITY,CRAFTSANITY_CRAFT",
|
||||
3533,Farm,Craft Blue Grass Starter,"CRAFTSANITY,CRAFTSANITY_CRAFT,GINGER_ISLAND",
|
||||
3533,Farm,Craft Blue Grass Starter,"CRAFTSANITY,CRAFTSANITY_CRAFT,GINGER_ISLAND,REQUIRES_QI_ORDERS",
|
||||
3534,Farm,Craft Mystic Tree Seed,"CRAFTSANITY,CRAFTSANITY_CRAFT,REQUIRES_MASTERIES",
|
||||
3535,Farm,Craft Sonar Bobber,"CRAFTSANITY,CRAFTSANITY_CRAFT",
|
||||
3536,Farm,Craft Challenge Bait,"CRAFTSANITY,CRAFTSANITY_CRAFT,REQUIRES_MASTERIES",
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import csv
|
||||
import enum
|
||||
import logging
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from random import Random
|
||||
from typing import Optional, Dict, Protocol, List, Iterable
|
||||
@@ -16,7 +17,7 @@ from .mods.mod_data import ModNames
|
||||
from .options import ArcadeMachineLocations, SpecialOrderLocations, Museumsanity, \
|
||||
FestivalLocations, ElevatorProgression, BackpackProgression, FarmType
|
||||
from .options import StardewValleyOptions, Craftsanity, Chefsanity, Cooksanity, Shipsanity, Monstersanity
|
||||
from .options.options import BackpackSize, Moviesanity, Eatsanity, IncludeEndgameLocations, Friendsanity
|
||||
from .options.options import BackpackSize, Moviesanity, Eatsanity, IncludeEndgameLocations, Friendsanity, Fishsanity, SkillProgression, Cropsanity
|
||||
from .strings.ap_names.ap_option_names import WalnutsanityOptionName, SecretsanityOptionName, EatsanityOptionName, ChefsanityOptionName, StartWithoutOptionName
|
||||
from .strings.backpack_tiers import Backpack
|
||||
from .strings.goal_names import Goal
|
||||
@@ -665,19 +666,48 @@ def extend_endgame_locations(randomized_locations: List[LocationData], options:
|
||||
|
||||
def extend_filler_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
||||
days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
|
||||
i = 1
|
||||
while len(randomized_locations) < 90:
|
||||
location_name = f"Traveling Merchant Sunday Item {i}"
|
||||
while any(location.name == location_name for location in randomized_locations):
|
||||
i += 1
|
||||
location_name = f"Traveling Merchant Sunday Item {i}"
|
||||
number_locations_to_add_per_day = 0
|
||||
min_number_locations = 90 # Under 90 locations we can run out of rooms for the mandatory core items
|
||||
if len(randomized_locations) < min_number_locations:
|
||||
number_locations_to_add = min_number_locations - len(randomized_locations)
|
||||
number_locations_to_add_per_day += math.ceil(number_locations_to_add / 7)
|
||||
|
||||
# These settings generate a lot of empty locations, so they can absorb a lot of items
|
||||
filler_heavy_settings = [options.fishsanity != Fishsanity.option_none,
|
||||
options.shipsanity != Shipsanity.option_none,
|
||||
options.cooksanity != Cooksanity.option_none,
|
||||
options.craftsanity != Craftsanity.option_none,
|
||||
len(options.eatsanity.value) > 0,
|
||||
options.museumsanity == Museumsanity.option_all,
|
||||
options.quest_locations.value >= 0,
|
||||
options.bundle_per_room >= 2]
|
||||
# These settings generate orphan items and can cause too many items, if enabled without a complementary of the filler heavy settings
|
||||
orphan_settings = [len(options.chefsanity.value) > 0,
|
||||
options.friendsanity != Friendsanity.option_none,
|
||||
options.skill_progression == SkillProgression.option_progressive_with_masteries,
|
||||
options.cropsanity != Cropsanity.option_disabled,
|
||||
len(options.start_without.value) > 0,
|
||||
options.bundle_per_room <= -1,
|
||||
options.bundle_per_room <= -2]
|
||||
|
||||
enabled_filler_heavy_settings = len([val for val in filler_heavy_settings if val])
|
||||
enabled_orphan_settings = len([val for val in orphan_settings if val])
|
||||
if enabled_orphan_settings > enabled_filler_heavy_settings:
|
||||
number_locations_to_add_per_day += enabled_orphan_settings - enabled_filler_heavy_settings
|
||||
|
||||
if number_locations_to_add_per_day <= 0:
|
||||
return
|
||||
|
||||
existing_traveling_merchant_locations = [location.name for location in randomized_locations if location.name.startswith("Traveling Merchant Sunday Item ")]
|
||||
start_num_to_add = len(existing_traveling_merchant_locations) + 1
|
||||
|
||||
for i in range(start_num_to_add, start_num_to_add+number_locations_to_add_per_day):
|
||||
logger.debug(f"Player too few locations, adding Traveling Merchant Items #{i}")
|
||||
for day in days:
|
||||
location_name = f"Traveling Merchant {day} Item {i}"
|
||||
randomized_locations.append(location_table[location_name])
|
||||
|
||||
|
||||
|
||||
def create_locations(location_collector: StardewLocationCollector,
|
||||
bundle_rooms: List[BundleRoom],
|
||||
trash_bear_requests: Dict[str, List[str]],
|
||||
|
||||
@@ -297,7 +297,6 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
Material.stone: self.ability.can_mine_stone(),
|
||||
Material.wood: self.ability.can_chop_trees(),
|
||||
Meal.ice_cream: (self.season.has(Season.summer) & self.money.can_spend_at(Region.town, 250)) | self.money.can_spend_at(Region.oasis, 240),
|
||||
Meal.strange_bun: self.relationship.has_hearts(NPC.shane, 7) & self.has(Ingredient.wheat_flour) & self.has(Fish.periwinkle) & self.has(ArtisanGood.void_mayonnaise),
|
||||
MetalBar.copper: self.can_smelt(Ore.copper),
|
||||
MetalBar.gold: self.can_smelt(Ore.gold),
|
||||
MetalBar.iridium: self.can_smelt(Ore.iridium),
|
||||
@@ -313,7 +312,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
RetainingSoil.basic: self.money.can_spend_at(Region.pierre_store, 100),
|
||||
RetainingSoil.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
|
||||
SpecialItem.lucky_purple_shorts: self.special_items.has_purple_shorts(),
|
||||
SpecialItem.trimmed_purple_shorts: self.has(SpecialItem.lucky_purple_shorts) & self.has(Machine.sewing_machine),
|
||||
SpecialItem.trimmed_purple_shorts: self.has(SpecialItem.lucky_purple_shorts) & self.has(MetalBar.gold) & self.has(Machine.sewing_machine),
|
||||
SpecialItem.far_away_stone: self.special_items.has_far_away_stone(),
|
||||
SpecialItem.solid_gold_lewis: self.special_items.has_solid_gold_lewis(),
|
||||
SpecialItem.advanced_tv_remote: self.special_items.has_advanced_tv_remote(),
|
||||
|
||||
@@ -7,6 +7,13 @@ from ..items import Group, item_table
|
||||
from ..items.item_data import FILLER_GROUPS
|
||||
|
||||
|
||||
def get_real_item_count(multiworld):
|
||||
number_items = len([item for item in multiworld.itempool
|
||||
if all(filler_group not in item_table[item.name].groups for filler_group in FILLER_GROUPS) and Group.TRAP not in item_table[
|
||||
item.name].groups and (item.classification & ItemClassification.progression)])
|
||||
return number_items
|
||||
|
||||
|
||||
class TestLocationGeneration(SVTestBase):
|
||||
|
||||
def test_all_location_created_are_in_location_table(self):
|
||||
@@ -20,8 +27,7 @@ class TestMinLocationAndMaxItem(SVTestBase):
|
||||
def test_minimal_location_maximal_items_still_valid(self):
|
||||
valid_locations = self.get_real_locations()
|
||||
number_locations = len(valid_locations)
|
||||
number_items = len([item for item in self.multiworld.itempool
|
||||
if all(filler_group not in item_table[item.name].groups for filler_group in FILLER_GROUPS) and Group.TRAP not in item_table[item.name].groups])
|
||||
number_items = get_real_item_count(self.multiworld)
|
||||
print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND EXCLUDED]")
|
||||
self.assertGreaterEqual(number_locations, number_items)
|
||||
|
||||
@@ -32,8 +38,7 @@ class TestMinLocationAndMaxItemWithIsland(SVTestBase):
|
||||
def test_minimal_location_maximal_items_with_island_still_valid(self):
|
||||
valid_locations = self.get_real_locations()
|
||||
number_locations = len(valid_locations)
|
||||
number_items = len([item for item in self.multiworld.itempool
|
||||
if all(filler_group not in item_table[item.name].groups for filler_group in FILLER_GROUPS) and Group.TRAP not in item_table[item.name].groups and (item.classification & ItemClassification.progression)])
|
||||
number_items = get_real_item_count(self.multiworld)
|
||||
print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND INCLUDED]")
|
||||
self.assertGreaterEqual(number_locations, number_items)
|
||||
|
||||
@@ -99,3 +104,5 @@ class TestAllSanityWithModsSettingsHasAllExpectedLocations(SVTestBase):
|
||||
f"\n\tPlease update test_allsanity_with_mods_has_at_least_locations"
|
||||
f"\n\t\tExpected: {expected_locations}"
|
||||
f"\n\t\tActual: {number_locations}")
|
||||
|
||||
|
||||
|
||||
62
worlds/stardew_valley/test/long/TestNumberLocationsLong.py
Normal file
62
worlds/stardew_valley/test/long/TestNumberLocationsLong.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import unittest
|
||||
|
||||
from BaseClasses import ItemClassification
|
||||
from ..assertion import get_all_location_names
|
||||
from ..bases import skip_long_tests, SVTestCase, solo_multiworld
|
||||
from ..options.presets import setting_mins_and_maxes, allsanity_no_mods_7_x_x, get_minsanity_options, default_7_x_x
|
||||
from ...items import Group, item_table
|
||||
from ...items.item_data import FILLER_GROUPS
|
||||
|
||||
if skip_long_tests():
|
||||
raise unittest.SkipTest("Long tests disabled")
|
||||
|
||||
|
||||
def get_real_item_count(multiworld):
|
||||
number_items = len([item for item in multiworld.itempool
|
||||
if all(filler_group not in item_table[item.name].groups for filler_group in FILLER_GROUPS) and Group.TRAP not in item_table[
|
||||
item.name].groups and (item.classification & ItemClassification.progression)])
|
||||
return number_items
|
||||
|
||||
|
||||
class TestCountsPerSetting(SVTestCase):
|
||||
|
||||
def test_items_locations_counts_per_setting_with_ginger_island(self):
|
||||
option_mins_and_maxes = setting_mins_and_maxes()
|
||||
|
||||
for name in option_mins_and_maxes:
|
||||
values = option_mins_and_maxes[name]
|
||||
if not isinstance(values, list):
|
||||
continue
|
||||
with self.subTest(f"{name}"):
|
||||
highest_variance_items = -1
|
||||
highest_variance_locations = -1
|
||||
for preset in [allsanity_no_mods_7_x_x, default_7_x_x, get_minsanity_options]:
|
||||
lowest_items = 9999
|
||||
lowest_locations = 9999
|
||||
highest_items = -1
|
||||
highest_locations = -1
|
||||
for value in values:
|
||||
world_options = preset()
|
||||
world_options[name] = value
|
||||
with solo_multiworld(world_options, world_caching=False) as (multiworld, _):
|
||||
num_locations = len([loc for loc in get_all_location_names(multiworld) if not loc.startswith("Traveling Merchant")])
|
||||
num_items = get_real_item_count(multiworld)
|
||||
if num_items > highest_items:
|
||||
highest_items = num_items
|
||||
if num_items < lowest_items:
|
||||
lowest_items = num_items
|
||||
if num_locations > highest_locations:
|
||||
highest_locations = num_locations
|
||||
if num_locations < lowest_locations:
|
||||
lowest_locations = num_locations
|
||||
|
||||
variance_items = highest_items - lowest_items
|
||||
variance_locations = highest_locations - lowest_locations
|
||||
if variance_locations > highest_variance_locations:
|
||||
highest_variance_locations = variance_locations
|
||||
if variance_items > highest_variance_items:
|
||||
highest_variance_items = variance_items
|
||||
if highest_variance_locations > highest_variance_items:
|
||||
print(f"Options `{name}` can create up to {highest_variance_locations - highest_variance_items} filler ({highest_variance_locations} locations and up to {highest_variance_items} items)")
|
||||
if highest_variance_locations < highest_variance_items:
|
||||
print(f"Options `{name}` can create up to {highest_variance_items - highest_variance_locations} orphan ({highest_variance_locations} locations and up to {highest_variance_items} items)")
|
||||
@@ -292,3 +292,48 @@ def minimal_locations_maximal_items_with_island():
|
||||
min_max_options = minimal_locations_maximal_items()
|
||||
min_max_options.update({options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false})
|
||||
return min_max_options
|
||||
|
||||
|
||||
def setting_mins_and_maxes():
|
||||
low_orphan_options = {
|
||||
options.ArcadeMachineLocations.internal_name: [options.ArcadeMachineLocations.option_disabled, options.ArcadeMachineLocations.option_full_shuffling],
|
||||
options.BackpackProgression.internal_name: [options.BackpackProgression.option_vanilla, options.BackpackProgression.option_progressive],
|
||||
options.BackpackSize.internal_name: [options.BackpackSize.option_1, options.BackpackSize.option_12],
|
||||
options.Booksanity.internal_name: [options.Booksanity.option_none, options.Booksanity.option_power_skill, options.Booksanity.option_power, options.Booksanity.option_all],
|
||||
options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla_cheap,
|
||||
options.BundlePerRoom.internal_name: [options.BundlePerRoom.option_two_fewer, options.BundlePerRoom.option_four_extra],
|
||||
options.BundlePrice.internal_name: options.BundlePrice.option_normal,
|
||||
options.BundleRandomization.internal_name: options.BundleRandomization.option_remixed,
|
||||
options.Chefsanity.internal_name: [options.Chefsanity.preset_none, options.Chefsanity.preset_all],
|
||||
options.Cooksanity.internal_name: [options.Cooksanity.option_none, options.Cooksanity.option_all],
|
||||
options.Craftsanity.internal_name: [options.Craftsanity.option_none, options.Craftsanity.option_all],
|
||||
options.Cropsanity.internal_name: [options.Cropsanity.option_disabled, options.Cropsanity.option_enabled],
|
||||
options.Eatsanity.internal_name: [options.Eatsanity.preset_none, options.Eatsanity.preset_all],
|
||||
options.ElevatorProgression.internal_name: [options.ElevatorProgression.option_vanilla, options.ElevatorProgression.option_progressive],
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all,
|
||||
options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
|
||||
options.ExcludeGingerIsland.internal_name: [options.ExcludeGingerIsland.option_false, options.ExcludeGingerIsland.option_true],
|
||||
options.FarmType.internal_name: [options.FarmType.option_standard, options.FarmType.option_meadowlands],
|
||||
options.FestivalLocations.internal_name: [options.FestivalLocations.option_disabled, options.FestivalLocations.option_hard],
|
||||
options.Fishsanity.internal_name: [options.Fishsanity.option_none, options.Fishsanity.option_all],
|
||||
options.Friendsanity.internal_name: [options.Friendsanity.option_none, options.Friendsanity.option_all_with_marriage],
|
||||
options.FriendsanityHeartSize.internal_name: [1, 8],
|
||||
options.Goal.internal_name: options.Goal.option_allsanity,
|
||||
options.IncludeEndgameLocations.internal_name: [options.IncludeEndgameLocations.option_false, options.IncludeEndgameLocations.option_true],
|
||||
options.Mods.internal_name: frozenset(),
|
||||
options.Monstersanity.internal_name: [options.Monstersanity.option_none, options.Monstersanity.option_one_per_monster],
|
||||
options.Moviesanity.internal_name: [options.Moviesanity.option_none, options.Moviesanity.option_all_movies_and_all_loved_snacks],
|
||||
options.Museumsanity.internal_name: [options.Museumsanity.option_none, options.Museumsanity.option_all],
|
||||
options.NumberOfMovementBuffs.internal_name: [0, 12],
|
||||
options.QuestLocations.internal_name: [-1, 56],
|
||||
options.SeasonRandomization.internal_name: [options.SeasonRandomization.option_disabled, options.SeasonRandomization.option_randomized_not_winter],
|
||||
options.Secretsanity.internal_name: [options.Secretsanity.preset_none, options.Secretsanity.preset_all],
|
||||
options.Shipsanity.internal_name: [options.Shipsanity.option_none, options.Shipsanity.option_everything],
|
||||
options.SkillProgression.internal_name: [options.SkillProgression.option_vanilla, options.SkillProgression.option_progressive_with_masteries],
|
||||
options.SpecialOrderLocations.internal_name: [options.SpecialOrderLocations.option_vanilla, options.SpecialOrderLocations.option_board_qi],
|
||||
options.StartWithout.internal_name: [options.StartWithout.preset_none, options.StartWithout.preset_all],
|
||||
options.ToolProgression.internal_name: [options.ToolProgression.option_vanilla, options.ToolProgression.option_progressive],
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_medium,
|
||||
options.Walnutsanity.internal_name: [options.Walnutsanity.preset_none, options.Walnutsanity.preset_all],
|
||||
}
|
||||
return low_orphan_options
|
||||
|
||||
Reference in New Issue
Block a user