mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-23 10:03:20 -07:00
Merge remote-tracking branch 'remotes/upstream/main'
This commit is contained in:
2
.github/workflows/analyze-modified-files.yml
vendored
2
.github/workflows/analyze-modified-files.yml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
continue-on-error: false
|
||||
if: env.diff != '' && matrix.task == 'flake8'
|
||||
run: |
|
||||
flake8 --count --select=E9,F63,F7,F82 --show-source --statistics ${{ env.diff }}
|
||||
flake8 --count --select=E9,F63,F7,F82 --ignore F824 --show-source --statistics ${{ env.diff }}
|
||||
|
||||
- name: "flake8: Lint modified files"
|
||||
continue-on-error: true
|
||||
|
||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -99,8 +99,8 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 7 # keep for 7 days, should be enough
|
||||
|
||||
build-ubuntu2004:
|
||||
runs-on: ubuntu-20.04
|
||||
build-ubuntu2204:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
# - copy code below to release.yml -
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -29,8 +29,8 @@ jobs:
|
||||
# build-release-windows: # this is done by hand because of signing
|
||||
# build-release-macos: # LF volunteer
|
||||
|
||||
build-release-ubuntu2004:
|
||||
runs-on: ubuntu-20.04
|
||||
build-release-ubuntu2204:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
@@ -616,7 +616,7 @@ class MultiWorld():
|
||||
locations: Set[Location] = set()
|
||||
events: Set[Location] = set()
|
||||
for location in self.get_filled_locations():
|
||||
if type(location.item.code) is int:
|
||||
if type(location.item.code) is int and type(location.address) is int:
|
||||
locations.add(location)
|
||||
else:
|
||||
events.add(location)
|
||||
|
||||
6
Fill.py
6
Fill.py
@@ -75,9 +75,11 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
items_to_place.append(reachable_items[next_player].pop())
|
||||
|
||||
for item in items_to_place:
|
||||
for p, pool_item in enumerate(item_pool):
|
||||
# The items added into `reachable_items` are placed starting from the end of each deque in
|
||||
# `reachable_items`, so the items being placed are more likely to be found towards the end of `item_pool`.
|
||||
for p, pool_item in enumerate(reversed(item_pool), start=1):
|
||||
if pool_item is item:
|
||||
item_pool.pop(p)
|
||||
del item_pool[-p]
|
||||
break
|
||||
|
||||
maximum_exploration_state = sweep_from_pool(
|
||||
|
||||
18
Launcher.py
18
Launcher.py
@@ -8,9 +8,7 @@ Archipelago Launcher
|
||||
Scroll down to components= to add components to the launcher as well as setup.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import argparse
|
||||
import itertools
|
||||
import logging
|
||||
import multiprocessing
|
||||
import shlex
|
||||
@@ -132,7 +130,7 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
||||
from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogContentContainer, MDDialogSupportingText
|
||||
from kivymd.uix.divider import MDDivider
|
||||
|
||||
if client_component is None:
|
||||
if not client_component:
|
||||
run_component(text_client_component, *launch_args)
|
||||
return
|
||||
else:
|
||||
@@ -228,14 +226,13 @@ refresh_components: Optional[Callable[[], None]] = None
|
||||
|
||||
|
||||
def run_gui(path: str, args: Any) -> None:
|
||||
from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, MDButton, MDLabel, MDButtonText, ScrollBox, ApAsyncImage)
|
||||
from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, ScrollBox)
|
||||
from kivy.properties import ObjectProperty
|
||||
from kivy.core.window import Window
|
||||
from kivy.metrics import dp
|
||||
from kivymd.uix.button import MDIconButton
|
||||
from kivymd.uix.card import MDCard
|
||||
from kivymd.uix.menu import MDDropdownMenu
|
||||
from kivymd.uix.relativelayout import MDRelativeLayout
|
||||
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText
|
||||
|
||||
from kivy.lang.builder import Builder
|
||||
@@ -250,7 +247,6 @@ def run_gui(path: str, args: Any) -> None:
|
||||
self.image = image_path
|
||||
super().__init__(args, kwargs)
|
||||
|
||||
|
||||
class Launcher(ThemedApp):
|
||||
base_title: str = "Archipelago Launcher"
|
||||
top_screen: MDFloatLayout = ObjectProperty(None)
|
||||
@@ -337,6 +333,11 @@ def run_gui(path: str, args: Any) -> None:
|
||||
for card in cards:
|
||||
self.button_layout.layout.add_widget(card)
|
||||
|
||||
top = self.button_layout.children[0].y + self.button_layout.children[0].height \
|
||||
- self.button_layout.height
|
||||
scroll_percent = self.button_layout.convert_distance_to_scroll(0, top)
|
||||
self.button_layout.scroll_y = max(0, min(1, scroll_percent[1]))
|
||||
|
||||
def filter_clients(self, caller):
|
||||
self._refresh_components(caller.type)
|
||||
|
||||
@@ -358,6 +359,11 @@ def run_gui(path: str, args: Any) -> None:
|
||||
|
||||
self._refresh_components(self.current_filter)
|
||||
|
||||
# Uncomment to re-enable the Kivy console/live editor
|
||||
# Ctrl-E to enable it, make sure numlock/capslock is disabled
|
||||
# from kivy.modules.console import create_console
|
||||
# create_console(Window, self.top_screen)
|
||||
|
||||
return self.top_screen
|
||||
|
||||
def on_start(self):
|
||||
|
||||
@@ -26,6 +26,7 @@ import typing
|
||||
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
||||
server_loop)
|
||||
from NetUtils import ClientStatus
|
||||
from worlds.ladx import LinksAwakeningWorld
|
||||
from worlds.ladx.Common import BASE_ID as LABaseID
|
||||
from worlds.ladx.GpsTracker import GpsTracker
|
||||
from worlds.ladx.TrackerConsts import storage_key
|
||||
@@ -741,8 +742,8 @@ class LinksAwakeningContext(CommonContext):
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
def run_game(romfile: str) -> None:
|
||||
auto_start = typing.cast(typing.Union[bool, str],
|
||||
Utils.get_options()["ladx_options"].get("rom_start", True))
|
||||
auto_start = LinksAwakeningWorld.settings.rom_start
|
||||
|
||||
if auto_start is True:
|
||||
import webbrowser
|
||||
webbrowser.open(romfile)
|
||||
|
||||
@@ -28,6 +28,6 @@ def get_seeds():
|
||||
response.append({
|
||||
"seed_id": seed.id,
|
||||
"creation_time": seed.creation_time,
|
||||
"players": get_players(seed.slots),
|
||||
"players": get_players(seed),
|
||||
})
|
||||
return jsonify(response)
|
||||
|
||||
@@ -9,7 +9,7 @@ from threading import Event, Thread
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from pony.orm import db_session, select, commit
|
||||
from pony.orm import db_session, select, commit, PrimaryKey
|
||||
|
||||
from Utils import restricted_loads
|
||||
from .locker import Locker, AlreadyRunningException
|
||||
@@ -36,12 +36,21 @@ def handle_generation_failure(result: BaseException):
|
||||
logging.exception(e)
|
||||
|
||||
|
||||
def _mp_gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None, sid=None) -> PrimaryKey | None:
|
||||
from setproctitle import setproctitle
|
||||
|
||||
setproctitle(f"Generator ({sid})")
|
||||
res = gen_game(gen_options, meta=meta, owner=owner, sid=sid)
|
||||
setproctitle(f"Generator (idle)")
|
||||
return res
|
||||
|
||||
|
||||
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||
try:
|
||||
meta = json.loads(generation.meta)
|
||||
options = restricted_loads(generation.options)
|
||||
logging.info(f"Generating {generation.id} for {len(options)} players")
|
||||
pool.apply_async(gen_game, (options,),
|
||||
pool.apply_async(_mp_gen_game, (options,),
|
||||
{"meta": meta,
|
||||
"sid": generation.id,
|
||||
"owner": generation.owner},
|
||||
@@ -55,6 +64,10 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||
|
||||
|
||||
def init_generator(config: dict[str, Any]) -> None:
|
||||
from setproctitle import setproctitle
|
||||
|
||||
setproctitle("Generator (idle)")
|
||||
|
||||
try:
|
||||
import resource
|
||||
except ModuleNotFoundError:
|
||||
|
||||
@@ -227,6 +227,9 @@ def set_up_logging(room_id) -> logging.Logger:
|
||||
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
||||
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
|
||||
from setproctitle import setproctitle
|
||||
|
||||
setproctitle(name)
|
||||
Utils.init_logging(name)
|
||||
try:
|
||||
import resource
|
||||
@@ -247,8 +250,23 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
raise Exception("Worlds system should not be loaded in the custom server.")
|
||||
|
||||
import gc
|
||||
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
||||
del cert_file, cert_key_file, ponyconfig
|
||||
|
||||
if not cert_file:
|
||||
def get_ssl_context():
|
||||
return None
|
||||
else:
|
||||
load_date = None
|
||||
ssl_context = load_server_cert(cert_file, cert_key_file)
|
||||
|
||||
def get_ssl_context():
|
||||
nonlocal load_date, ssl_context
|
||||
today = datetime.date.today()
|
||||
if load_date != today:
|
||||
ssl_context = load_server_cert(cert_file, cert_key_file)
|
||||
load_date = today
|
||||
return ssl_context
|
||||
|
||||
del ponyconfig
|
||||
gc.collect() # free intermediate objects used during setup
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
@@ -263,12 +281,12 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
assert ctx.server is None
|
||||
try:
|
||||
ctx.server = websockets.serve(
|
||||
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=get_ssl_context())
|
||||
|
||||
await ctx.server
|
||||
except OSError: # likely port in use
|
||||
ctx.server = websockets.serve(
|
||||
functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
||||
functools.partial(server, ctx=ctx), ctx.host, 0, ssl=get_ssl_context())
|
||||
|
||||
await ctx.server
|
||||
port = 0
|
||||
|
||||
@@ -9,3 +9,4 @@ bokeh>=3.6.3
|
||||
markupsafe>=3.0.2
|
||||
Markdown>=3.7
|
||||
mdx-breakless-lists>=1.0.1
|
||||
setproctitle>=1.3.5
|
||||
|
||||
@@ -23,7 +23,6 @@ window.addEventListener('load', () => {
|
||||
showdown.setOption('strikethrough', true);
|
||||
showdown.setOption('literalMidWordUnderscores', true);
|
||||
gameInfo.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Reset the id of all header divs to something nicer
|
||||
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
|
||||
|
||||
@@ -6,6 +6,4 @@ window.addEventListener('load', () => {
|
||||
document.getElementById('file-input').addEventListener('change', () => {
|
||||
document.getElementById('host-game-form').submit();
|
||||
});
|
||||
|
||||
adjustFooterHeight();
|
||||
});
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
const adjustFooterHeight = () => {
|
||||
// If there is no footer on this page, do nothing
|
||||
const footer = document.getElementById('island-footer');
|
||||
if (!footer) { return; }
|
||||
|
||||
// If the body is taller than the window, also do nothing
|
||||
if (document.body.offsetHeight > window.innerHeight) {
|
||||
footer.style.marginTop = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a margin-top to the footer to position it at the bottom of the screen
|
||||
const sibling = footer.previousElementSibling;
|
||||
const margin = (window.innerHeight - sibling.offsetTop - sibling.offsetHeight - footer.offsetHeight);
|
||||
if (margin < 1) {
|
||||
footer.style.marginTop = '0';
|
||||
return;
|
||||
}
|
||||
footer.style.marginTop = `${margin}px`;
|
||||
};
|
||||
|
||||
const adjustHeaderWidth = () => {
|
||||
// If there is no header, do nothing
|
||||
const header = document.getElementById('base-header');
|
||||
if (!header) { return; }
|
||||
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.style.width = '100px';
|
||||
tempDiv.style.height = '100px';
|
||||
tempDiv.style.overflow = 'scroll';
|
||||
tempDiv.style.position = 'absolute';
|
||||
tempDiv.style.top = '-500px';
|
||||
document.body.appendChild(tempDiv);
|
||||
const scrollbarWidth = tempDiv.offsetWidth - tempDiv.clientWidth;
|
||||
document.body.removeChild(tempDiv);
|
||||
|
||||
const documentRoot = document.compatMode === 'BackCompat' ? document.body : document.documentElement;
|
||||
const margin = (documentRoot.scrollHeight > documentRoot.clientHeight) ? 0-scrollbarWidth : 0;
|
||||
document.getElementById('base-header-right').style.marginRight = `${margin}px`;
|
||||
};
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
window.addEventListener('resize', adjustFooterHeight);
|
||||
window.addEventListener('resize', adjustHeaderWidth);
|
||||
adjustFooterHeight();
|
||||
adjustHeaderWidth();
|
||||
});
|
||||
@@ -25,7 +25,6 @@ window.addEventListener('load', () => {
|
||||
showdown.setOption('literalMidWordUnderscores', true);
|
||||
showdown.setOption('disableForced4SpacesIndentedSublists', true);
|
||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
const title = document.querySelector('h1')
|
||||
if (title) {
|
||||
|
||||
@@ -36,6 +36,13 @@ html{
|
||||
|
||||
body{
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 110px);
|
||||
}
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
a{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Page Not Found (404)</title>
|
||||
@@ -13,5 +14,4 @@
|
||||
The page you're looking for doesn't exist.<br />
|
||||
<a href="/">Click here to return to safety.</a>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Upload Multidata</title>
|
||||
@@ -27,6 +28,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Archipelago</title>
|
||||
@@ -57,5 +58,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,26 +5,29 @@
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tooltip.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/cookieNotice.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/globalStyles.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/styleController.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/cookieNotice.js") }}"></script>
|
||||
{% block head %}
|
||||
<title>Archipelago</title>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages | unique %}
|
||||
<div class="user-message">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages | unique %}
|
||||
<div class="user-message">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
{% if show_footer %}
|
||||
{% include "islandFooter.html" %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Generation failed, please retry.</title>
|
||||
@@ -15,5 +16,4 @@
|
||||
{{ seed_error }}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Start Playing</title>
|
||||
@@ -26,6 +27,4 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
<div id="user-content-wrapper" class="markdown">
|
||||
<div id="user-content" class="grass-island">
|
||||
<h1>User Content</h1>
|
||||
Below is a list of all the content you have generated on this site. Rooms and seeds are listed separately.
|
||||
Below is a list of all the content you have generated on this site. Rooms and seeds are listed separately.<br/>
|
||||
Sessions can be saved or synced across devices using the <a href="{{url_for('show_session')}}">Sessions Page.</a>
|
||||
|
||||
<h2>Your Rooms</h2>
|
||||
{% if rooms %}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>View Seed {{ seed.id|suuid }}</title>
|
||||
@@ -50,5 +51,4 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Generation in Progress</title>
|
||||
<meta http-equiv="refresh" content="1">
|
||||
<noscript>
|
||||
<meta http-equiv="refresh" content="1">
|
||||
</noscript>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/waitSeed.css") }}"/>
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,5 +18,34 @@
|
||||
Waiting for game to generate, this page auto-refreshes to check.
|
||||
</div>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
<script>
|
||||
const waitSeedDiv = document.getElementById("wait-seed");
|
||||
async function checkStatus() {
|
||||
try {
|
||||
const response = await fetch("{{ url_for('api.wait_seed_api', seed=seed_id) }}");
|
||||
if (response.status !== 202) {
|
||||
// Seed is ready; reload page to load seed page.
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
waitSeedDiv.innerHTML = `
|
||||
<h1>Generation in Progress</h1>
|
||||
<p>${data.text}</p>
|
||||
`;
|
||||
|
||||
setTimeout(checkStatus, 1000); // Continue polling.
|
||||
} catch (error) {
|
||||
waitSeedDiv.innerHTML = `
|
||||
<h1>Progress Unknown</h1>
|
||||
<p>${error.message}<br />(Last checked: ${new Date().toLocaleTimeString()})</p>
|
||||
`;
|
||||
|
||||
setTimeout(checkStatus, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(checkStatus, 1000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -16,20 +16,30 @@
|
||||
orange: "FF7700" # Used for command echo
|
||||
# KivyMD theming parameters
|
||||
theme_style: "Dark" # Light/Dark
|
||||
primary_palette: "Green" # Many options
|
||||
dynamic_scheme_name: "TONAL_SPOT"
|
||||
primary_palette: "Lightsteelblue" # Many options
|
||||
dynamic_scheme_name: "VIBRANT"
|
||||
dynamic_scheme_contrast: 0.0
|
||||
<MDLabel>:
|
||||
color: self.theme_cls.primaryColor
|
||||
<BaseButton>:
|
||||
ripple_color: app.theme_cls.primaryColor
|
||||
ripple_duration_in_fast: 0.2
|
||||
<MDTabsItemBase>:
|
||||
ripple_color: app.theme_cls.primaryColor
|
||||
ripple_duration_in_fast: 0.2
|
||||
<TooltipLabel>:
|
||||
adaptive_height: True
|
||||
font_size: dp(20)
|
||||
theme_font_size: "Custom"
|
||||
font_size: "20dp"
|
||||
markup: True
|
||||
halign: "left"
|
||||
<SelectableLabel>:
|
||||
size_hint: 1, None
|
||||
theme_text_color: "Custom"
|
||||
text_color: 1, 1, 1, 1
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (.0, 0.9, .1, .3) if self.selected else self.theme_cls.surfaceContainerLowColor
|
||||
rgba: (self.theme_cls.primaryColor[0], self.theme_cls.primaryColor[1], self.theme_cls.primaryColor[2], .3) if self.selected else self.theme_cls.surfaceContainerLowestColor
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
@@ -153,9 +163,12 @@
|
||||
<ToolTip>:
|
||||
size: self.texture_size
|
||||
size_hint: None, None
|
||||
theme_font_size: "Custom"
|
||||
font_size: dp(18)
|
||||
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
|
||||
halign: "left"
|
||||
theme_text_color: "Custom"
|
||||
text_color: (1, 1, 1, 1)
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: 0.2, 0.2, 0.2, 1
|
||||
@@ -174,16 +187,34 @@
|
||||
rectangle: self.x-2, self.y-2, self.width+4, self.height+4
|
||||
<ServerToolTip>:
|
||||
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
|
||||
<AutocompleteHintInput>
|
||||
<AutocompleteHintInput>:
|
||||
size_hint_y: None
|
||||
height: dp(30)
|
||||
height: "30dp"
|
||||
multiline: False
|
||||
write_tab: False
|
||||
pos_hint: {"center_x": 0.5, "center_y": 0.5}
|
||||
<ConnectBarTextInput>:
|
||||
height: "30dp"
|
||||
multiline: False
|
||||
write_tab: False
|
||||
role: "medium"
|
||||
size_hint_y: None
|
||||
pos_hint: {"center_x": 0.5, "center_y": 0.5}
|
||||
<CommandPromptTextInput>:
|
||||
size_hint_y: None
|
||||
height: "30dp"
|
||||
multiline: False
|
||||
write_tab: False
|
||||
pos_hint: {"center_x": 0.5, "center_y": 0.5}
|
||||
<MessageBoxLabel>:
|
||||
theme_text_color: "Custom"
|
||||
text_color: 1, 1, 1, 1
|
||||
<ScrollBox>:
|
||||
layout: layout
|
||||
bar_width: "12dp"
|
||||
scroll_wheel_distance: 40
|
||||
do_scroll_x: False
|
||||
scroll_type: ['bars', 'content']
|
||||
|
||||
MDBoxLayout:
|
||||
id: layout
|
||||
|
||||
@@ -5,17 +5,18 @@
|
||||
size_hint: 1, None
|
||||
height: "75dp"
|
||||
context_button: context
|
||||
focus_behavior: False
|
||||
|
||||
MDRelativeLayout:
|
||||
ApAsyncImage:
|
||||
source: main.image
|
||||
size: (40, 40)
|
||||
size_hint_y: None
|
||||
size: (48, 48)
|
||||
size_hint: None, None
|
||||
pos_hint: {"center_x": 0.1, "center_y": 0.5}
|
||||
|
||||
MDLabel:
|
||||
text: main.component.display_name
|
||||
pos_hint:{"center_x": 0.5, "center_y": 0.85 if main.component.description else 0.65}
|
||||
pos_hint:{"center_x": 0.5, "center_y": 0.75 if main.component.description else 0.65}
|
||||
halign: "center"
|
||||
font_style: "Title"
|
||||
role: "medium"
|
||||
@@ -37,6 +38,7 @@
|
||||
pos_hint:{"center_x": 0.85, "center_y": 0.8}
|
||||
theme_text_color: "Custom"
|
||||
text_color: app.theme_cls.primaryColor
|
||||
detect_visible: False
|
||||
on_release: app.set_favorite(self)
|
||||
|
||||
MDIconButton:
|
||||
@@ -46,6 +48,7 @@
|
||||
pos_hint:{"center_x": 0.95, "center_y": 0.8}
|
||||
theme_text_color: "Custom"
|
||||
text_color: app.theme_cls.primaryColor
|
||||
detect_visible: False
|
||||
|
||||
MDButton:
|
||||
pos_hint:{"center_x": 0.9, "center_y": 0.25}
|
||||
@@ -53,7 +56,7 @@
|
||||
height: "25dp"
|
||||
component: main.component
|
||||
on_release: app.component_action(self)
|
||||
|
||||
detect_visible: False
|
||||
MDButtonText:
|
||||
text: "Open"
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ These are "nice to have" features for a client, but they are not strictly requir
|
||||
if possible.
|
||||
|
||||
* If your client appears in the Archipelago Launcher, you may define an icon for it that differentiates it from
|
||||
other clients. The icon size is 38x38 pixels, but it will accept larger images with downscaling.
|
||||
other clients. The icon size is 48x48 pixels, but smaller or larger images will scale to that size.
|
||||
|
||||
## World
|
||||
|
||||
@@ -109,6 +109,10 @@ subclass for webhost documentation and behaviors
|
||||
* A non-zero number of locations, added to your regions
|
||||
* A non-zero number of items **equal** to the number of locations, added to the multiworld itempool
|
||||
* In rare cases, there may be 0-location-0-item games, but this is extremely atypical.
|
||||
* A set
|
||||
[completion condition](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#L77) (aka "goal") for
|
||||
the player.
|
||||
* Use your player as the index (`multiworld.completion_condition[player]`) for your world's completion goal.
|
||||
|
||||
### Encouraged Features
|
||||
|
||||
@@ -142,11 +146,11 @@ workarounds or preferred methods which should be used instead:
|
||||
* If you need to place specific items, there are multiple ways to do so, but they should not be added to the
|
||||
multiworld itempool.
|
||||
* It is not allowed to use `eval` for most reasons, chiefly due to security concerns.
|
||||
* It is discouraged to use `yaml.load` directly due to security concerns.
|
||||
* When possible, use `Utils.yaml_load` instead, as this defaults to the safe loader.
|
||||
* It is discouraged to use PyYAML (i.e. `yaml.load`) directly due to security concerns.
|
||||
* When possible, use `Utils.parse_yaml` instead, as this defaults to the safe loader and the faster C parser.
|
||||
* When submitting regions or items to the multiworld (`multiworld.regions` and `multiworld.itempool` respectively),
|
||||
Do **not** use `=` as this will overwrite all elements for all games in the seed.
|
||||
* Instead, use `append`, `extend`, or `+=`.
|
||||
do **not** use `=` as this will overwrite all elements for all games in the seed.
|
||||
* Instead, use `append`, `extend`, or `+=`.
|
||||
|
||||
### Notable Caveats
|
||||
|
||||
|
||||
@@ -66,3 +66,22 @@ The reason entrance access rules using `location.can_reach` and `entrance.can_re
|
||||
We recognize it can feel like a trap since it will not alert you when you are missing an indirect condition, and that some games have very complex access rules.
|
||||
As of [PR #3682 (Core: Region handling customization)](https://github.com/ArchipelagoMW/Archipelago/pull/3682) being merged, it is possible for a world to opt out of indirect conditions entirely, instead using the system of checking each entrance whenever a region has been reached, although this does come with a performance cost.
|
||||
Opting out of using indirect conditions should only be used by games that *really* need it. For most games, it should be reasonable to know all entrance → region dependencies, making indirect conditions preferred because they are much faster.
|
||||
|
||||
---
|
||||
|
||||
### I uploaded the generated output of my world to the webhost and webhost is erroring on corrupted multidata
|
||||
|
||||
The error `Could not load multidata. File may be corrupted or incompatible.` occurs when uploading a locally generated
|
||||
file where there is an issue with the multidata contained within it. It may come with a description like
|
||||
`(No module named 'worlds.myworld')` or `(global 'worlds.myworld.names.ItemNames' is forbidden)`
|
||||
|
||||
Pickling is a way to compress python objects such that they can be decompressed and be used to rebuild the
|
||||
python objects. This means that if one of your custom class instances ends up in the multidata, the server would not
|
||||
be able to load that custom class to decompress the data, which can fail either because the custom class is unknown
|
||||
(because it cannot load your world module) or the class it's attempting to import to decompress is deemed unsafe.
|
||||
|
||||
Common situations where this can happen include:
|
||||
* Using Option instances directly in slot_data. Ex: using `options.option_name` instead of `options.option_name.value`.
|
||||
Also, consider using the `options.as_dict("option_name", "option_two")` helper.
|
||||
* Using enums as Location/Item names in the datapackage. When building out `location_name_to_id` and `item_name_to_id`,
|
||||
make sure that you are not using your enum class for either the names or ids in these mappings.
|
||||
|
||||
@@ -606,8 +606,8 @@ from .items import get_item_type
|
||||
|
||||
def set_rules(self) -> None:
|
||||
# For some worlds this step can be omitted if either a Logic mixin
|
||||
# (see below) is used, it's easier to apply the rules from data during
|
||||
# location generation or everything is in generate_basic
|
||||
# (see below) is used or it's easier to apply the rules from data during
|
||||
# location generation
|
||||
|
||||
# set a simple rule for an region
|
||||
set_rule(self.multiworld.get_entrance("Boss Door", self.player),
|
||||
|
||||
204
kvui.py
204
kvui.py
@@ -43,8 +43,8 @@ from kivy.core.image import ImageLoader, ImageLoaderBase, ImageData
|
||||
from kivy.base import ExceptionHandler, ExceptionManager
|
||||
from kivy.clock import Clock
|
||||
from kivy.factory import Factory
|
||||
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
|
||||
from kivy.metrics import dp
|
||||
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty, StringProperty
|
||||
from kivy.metrics import dp, sp
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.uix.layout import Layout
|
||||
from kivy.utils import escape_markup
|
||||
@@ -60,7 +60,7 @@ from kivymd.app import MDApp
|
||||
from kivymd.uix.gridlayout import MDGridLayout
|
||||
from kivymd.uix.floatlayout import MDFloatLayout
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
from kivymd.uix.tab.tab import MDTabsPrimary, MDTabsItem, MDTabsItemText, MDTabsCarousel
|
||||
from kivymd.uix.tab.tab import MDTabsSecondary, MDTabsItem, MDTabsItemText, MDTabsCarousel
|
||||
from kivymd.uix.menu import MDDropdownMenu
|
||||
from kivymd.uix.menu.menu import MDDropdownTextItem
|
||||
from kivymd.uix.dropdownitem import MDDropDownItem, MDDropDownItemText
|
||||
@@ -90,16 +90,16 @@ remove_between_brackets = re.compile(r"\[.*?]")
|
||||
class ThemedApp(MDApp):
|
||||
def set_colors(self):
|
||||
text_colors = KivyJSONtoTextParser.TextColors()
|
||||
self.theme_cls.theme_style = getattr(text_colors, "theme_style", "Dark")
|
||||
self.theme_cls.primary_palette = getattr(text_colors, "primary_palette", "Green")
|
||||
self.theme_cls.dynamic_scheme_name = getattr(text_colors, "dynamic_scheme_name", "TONAL_SPOT")
|
||||
self.theme_cls.dynamic_scheme_contrast = 0.0
|
||||
self.theme_cls.theme_style = text_colors.theme_style
|
||||
self.theme_cls.primary_palette = text_colors.primary_palette
|
||||
self.theme_cls.dynamic_scheme_name = text_colors.dynamic_scheme_name
|
||||
self.theme_cls.dynamic_scheme_contrast = text_colors.dynamic_scheme_contrast
|
||||
|
||||
|
||||
class ImageIcon(MDButtonIcon, AsyncImage):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(args, kwargs)
|
||||
self.image = AsyncImage(**kwargs)
|
||||
self.image = ApAsyncImage(**kwargs)
|
||||
self.add_widget(self.image)
|
||||
|
||||
def add_widget(self, widget, index=0, canvas=None):
|
||||
@@ -114,7 +114,7 @@ class ImageButton(MDIconButton):
|
||||
if val != "None":
|
||||
image_args[kwarg.replace("image_", "")] = val
|
||||
super().__init__()
|
||||
self.image = AsyncImage(**image_args)
|
||||
self.image = ApAsyncImage(**image_args)
|
||||
|
||||
def set_center(button, center):
|
||||
self.image.center_x = self.center_x
|
||||
@@ -166,6 +166,32 @@ class ToggleButton(MDButton, ToggleButtonBehavior):
|
||||
child.icon_color = self.theme_cls.primaryColor
|
||||
|
||||
|
||||
# thanks kivymd
|
||||
class ResizableTextField(MDTextField):
|
||||
"""
|
||||
Resizable MDTextField that manually overrides the builtin sizing.
|
||||
|
||||
Note that in order to use this, the sizing must be specified from within a .kv rule.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
# cursed rules override
|
||||
rules = Builder.match(self)
|
||||
textfield = next((rule for rule in rules if rule.name == f"<MDTextField>"), None)
|
||||
if textfield:
|
||||
subclasses = rules[rules.index(textfield) + 1:]
|
||||
for subclass in subclasses:
|
||||
height_rule = subclass.properties.get("height", None)
|
||||
if height_rule:
|
||||
height_rule.ignore_prev = True
|
||||
super().__init__(args, kwargs)
|
||||
|
||||
|
||||
def on_release(self: MDButton, *args):
|
||||
super(MDButton, self).on_release(args)
|
||||
self.on_leave()
|
||||
|
||||
MDButton.on_release = on_release
|
||||
|
||||
# I was surprised to find this didn't already exist in kivy :(
|
||||
class HoverBehavior(object):
|
||||
"""originally from https://stackoverflow.com/a/605348110"""
|
||||
@@ -266,11 +292,15 @@ class TooltipLabel(HovererableLabel, MDTooltip):
|
||||
self._tooltip = None
|
||||
|
||||
|
||||
class ServerLabel(HovererableLabel, MDTooltip):
|
||||
class ServerLabel(HoverBehavior, MDTooltip, MDBoxLayout):
|
||||
tooltip_display_delay = 0.1
|
||||
text: str = StringProperty("Server:")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HovererableLabel, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.add_widget(MDIcon(icon="information", font_size=sp(15)))
|
||||
self.add_widget(TooltipLabel(text=self.text, pos_hint={"center_x": 0.5, "center_y": 0.5},
|
||||
font_size=sp(15)))
|
||||
self._tooltip = ServerToolTip(text="Test")
|
||||
|
||||
def on_enter(self):
|
||||
@@ -383,7 +413,6 @@ class MarkupDropdownTextItem(MDDropdownTextItem):
|
||||
for child in self.children:
|
||||
if child.__class__ == MDLabel:
|
||||
child.markup = True
|
||||
print(self.text)
|
||||
# Currently, this only lets us do markup on text that does not have any icons
|
||||
# Create new TextItems as needed
|
||||
|
||||
@@ -461,14 +490,13 @@ class MarkupDropdown(MDDropdownMenu):
|
||||
self.menu.data = self._items
|
||||
|
||||
|
||||
class AutocompleteHintInput(MDTextField):
|
||||
class AutocompleteHintInput(ResizableTextField):
|
||||
min_chars = NumericProperty(3)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.dropdown = MarkupDropdown(caller=self, position="bottom", border_margin=dp(24), width=self.width)
|
||||
self.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
|
||||
self.dropdown = MarkupDropdown(caller=self, position="bottom", border_margin=dp(2), width=self.width)
|
||||
self.bind(on_text_validate=self.on_message)
|
||||
self.bind(width=lambda instance, x: setattr(self.dropdown, "width", x))
|
||||
|
||||
@@ -485,8 +513,11 @@ class AutocompleteHintInput(MDTextField):
|
||||
|
||||
def on_press(text):
|
||||
split_text = MarkupLabel(text=text).markup
|
||||
return self.dropdown.select("".join(text_frag for text_frag in split_text
|
||||
if not text_frag.startswith("[")))
|
||||
self.set_text(self, "".join(text_frag for text_frag in split_text
|
||||
if not text_frag.startswith("[")))
|
||||
self.dropdown.dismiss()
|
||||
self.focus = True
|
||||
|
||||
lowered = value.lower()
|
||||
for item_name in item_names:
|
||||
try:
|
||||
@@ -498,7 +529,7 @@ class AutocompleteHintInput(MDTextField):
|
||||
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
|
||||
self.dropdown.items.append({
|
||||
"text": text,
|
||||
"on_release": lambda: on_press(text),
|
||||
"on_release": lambda txt=text: on_press(txt),
|
||||
"markup": True
|
||||
})
|
||||
if not self.dropdown.parent:
|
||||
@@ -589,8 +620,7 @@ class HintLabel(RecycleDataViewBehavior, MDBoxLayout):
|
||||
if self.entrance_text != "Vanilla"
|
||||
else "", ". (", self.status_text.lower(), ")"))
|
||||
temp = MarkupLabel(text).markup
|
||||
text = "".join(
|
||||
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
|
||||
text = "".join(part for part in temp if not part.startswith("["))
|
||||
Clipboard.copy(escape_markup(text).replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
|
||||
return self.parent.select_with_touch(self.index, touch)
|
||||
else:
|
||||
@@ -621,7 +651,7 @@ class HintLabel(RecycleDataViewBehavior, MDBoxLayout):
|
||||
self.selected = is_selected
|
||||
|
||||
|
||||
class ConnectBarTextInput(MDTextField):
|
||||
class ConnectBarTextInput(ResizableTextField):
|
||||
def insert_text(self, substring, from_undo=False):
|
||||
s = substring.replace("\n", "").replace("\r", "")
|
||||
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
|
||||
@@ -631,14 +661,14 @@ def is_command_input(string: str) -> bool:
|
||||
return len(string) > 0 and string[0] in "/!"
|
||||
|
||||
|
||||
class CommandPromptTextInput(MDTextField):
|
||||
class CommandPromptTextInput(ResizableTextField):
|
||||
MAXIMUM_HISTORY_MESSAGES = 50
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self._command_history_index = -1
|
||||
self._command_history: typing.Deque[str] = deque(maxlen=CommandPromptTextInput.MAXIMUM_HISTORY_MESSAGES)
|
||||
|
||||
|
||||
def update_history(self, new_entry: str) -> None:
|
||||
self._command_history_index = -1
|
||||
if is_command_input(new_entry):
|
||||
@@ -665,7 +695,7 @@ class CommandPromptTextInput(MDTextField):
|
||||
self._change_to_history_text_if_available(self._command_history_index - 1)
|
||||
return True
|
||||
return super().keyboard_on_key_down(window, keycode, text, modifiers)
|
||||
|
||||
|
||||
def _change_to_history_text_if_available(self, new_index: int) -> None:
|
||||
if new_index < -1:
|
||||
return
|
||||
@@ -683,29 +713,61 @@ class MessageBox(Popup):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._label.refresh()
|
||||
self.size = self._label.texture.size
|
||||
if self.width + 50 > Window.width:
|
||||
self.text_size[0] = Window.width - 50
|
||||
self._label.refresh()
|
||||
self.size = self._label.texture.size
|
||||
|
||||
def __init__(self, title, text, error=False, **kwargs):
|
||||
label = MessageBox.MessageBoxLabel(text=text)
|
||||
separator_color = [217 / 255, 129 / 255, 122 / 255, 1.] if error else [47 / 255., 167 / 255., 212 / 255, 1.]
|
||||
super().__init__(title=title, content=label, size_hint=(None, None), width=max(100, int(label.width) + 40),
|
||||
super().__init__(title=title, content=label, size_hint=(0.5, None), width=max(100, int(label.width) + 40),
|
||||
separator_color=separator_color, **kwargs)
|
||||
self.height += max(0, label.height - 18)
|
||||
|
||||
|
||||
class ClientTabs(MDTabsPrimary):
|
||||
class ClientTabs(MDTabsSecondary):
|
||||
carousel: MDTabsCarousel
|
||||
lock_swiping = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.carousel = MDTabsCarousel(lock_swiping=True)
|
||||
super().__init__(*args, MDDivider(size_hint_y=None, height=dp(4)), self.carousel, **kwargs)
|
||||
self.carousel = MDTabsCarousel(lock_swiping=True, anim_move_duration=0.2)
|
||||
super().__init__(*args, MDDivider(size_hint_y=None, height=dp(1)), self.carousel, **kwargs)
|
||||
self.size_hint_y = 1
|
||||
|
||||
def _check_panel_height(self, *args):
|
||||
self.ids.tab_scroll.height = dp(38)
|
||||
|
||||
def update_indicator(
|
||||
self, x: float = 0.0, w: float = 0.0, instance: MDTabsItem = None
|
||||
) -> None:
|
||||
def update_indicator(*args):
|
||||
indicator_pos = (0, 0)
|
||||
indicator_size = (0, 0)
|
||||
|
||||
item_text_object = self._get_tab_item_text_icon_object()
|
||||
|
||||
if item_text_object:
|
||||
indicator_pos = (
|
||||
instance.x + dp(12),
|
||||
self.indicator.pos[1]
|
||||
if not self._tabs_carousel
|
||||
else self._tabs_carousel.height,
|
||||
)
|
||||
indicator_size = (
|
||||
instance.width - dp(24),
|
||||
self.indicator_height,
|
||||
)
|
||||
|
||||
Animation(
|
||||
pos=indicator_pos,
|
||||
size=indicator_size,
|
||||
d=0 if not self.indicator_anim else self.indicator_duration,
|
||||
t=self.indicator_transition,
|
||||
).start(self.indicator)
|
||||
|
||||
if not instance:
|
||||
self.indicator.pos = (x, self.indicator.pos[1])
|
||||
self.indicator.size = (w, self.indicator_height)
|
||||
else:
|
||||
Clock.schedule_once(update_indicator)
|
||||
|
||||
def remove_tab(self, tab, content=None):
|
||||
if content is None:
|
||||
content = tab.content
|
||||
@@ -714,6 +776,21 @@ class ClientTabs(MDTabsPrimary):
|
||||
self.on_size(self, self.size)
|
||||
|
||||
|
||||
class CommandButton(MDButton, MDTooltip):
|
||||
def __init__(self, *args, manager: "GameManager", **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.manager = manager
|
||||
self._tooltip = ToolTip(text="Test")
|
||||
|
||||
def on_enter(self):
|
||||
self._tooltip.text = self.manager.commandprocessor.get_help_text()
|
||||
self._tooltip.font_size = dp(20 - (len(self._tooltip.text) // 400)) # mostly guessing on the numbers here
|
||||
self.display_tooltip()
|
||||
|
||||
def on_leave(self):
|
||||
self.animation_tooltip_dismiss()
|
||||
|
||||
|
||||
class GameManager(ThemedApp):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago"),
|
||||
@@ -768,19 +845,19 @@ class GameManager(ThemedApp):
|
||||
|
||||
self.grid = MainLayout()
|
||||
self.grid.cols = 1
|
||||
self.connect_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70),
|
||||
self.connect_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(40),
|
||||
spacing=5, padding=(5, 10))
|
||||
# top part
|
||||
server_label = ServerLabel(halign="center")
|
||||
server_label = ServerLabel(width=dp(75))
|
||||
self.connect_layout.add_widget(server_label)
|
||||
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
|
||||
size_hint_y=None, role="medium",
|
||||
height=dp(70), multiline=False, write_tab=False)
|
||||
pos_hint={"center_x": 0.5, "center_y": 0.5})
|
||||
|
||||
def connect_bar_validate(sender):
|
||||
if not self.ctx.server:
|
||||
self.connect_button_action(sender)
|
||||
|
||||
self.server_connect_bar.height = dp(30)
|
||||
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
|
||||
self.connect_layout.add_widget(self.server_connect_bar)
|
||||
self.server_connect_button = MDButton(MDButtonText(text="Connect"), style="filled", size=(dp(100), dp(70)),
|
||||
@@ -793,7 +870,7 @@ class GameManager(ThemedApp):
|
||||
self.grid.add_widget(self.progressbar)
|
||||
|
||||
# middle part
|
||||
self.tabs = ClientTabs()
|
||||
self.tabs = ClientTabs(pos_hint={"center_x": 0.5, "center_y": 0.5})
|
||||
self.tabs.add_widget(MDTabsItem(MDTabsItemText(text="All" if len(self.logging_pairs) > 1 else "Archipelago")))
|
||||
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
|
||||
for logger_name, name in
|
||||
@@ -821,9 +898,10 @@ class GameManager(ThemedApp):
|
||||
self.grid.add_widget(self.main_area_container)
|
||||
|
||||
# bottom part
|
||||
bottom_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70), spacing=5, padding=(5, 10))
|
||||
info_button = MDButton(MDButtonText(text="Command:"), radius=5, style="filled", size=(dp(100), dp(70)),
|
||||
size_hint_x=None, size_hint_y=None, pos_hint={"center_y": 0.575})
|
||||
bottom_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(40), spacing=5, padding=(5, 10))
|
||||
info_button = CommandButton(MDButtonText(text="Command:", halign="left"), manager=self, radius=5,
|
||||
style="filled", size=(dp(100), dp(70)), size_hint_x=None, size_hint_y=None,
|
||||
pos_hint={"center_y": 0.575})
|
||||
info_button.bind(on_release=self.command_button_action)
|
||||
bottom_layout.add_widget(info_button)
|
||||
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
|
||||
@@ -844,15 +922,27 @@ class GameManager(ThemedApp):
|
||||
self.server_connect_bar.focus = True
|
||||
self.server_connect_bar.select_text(port_start if port_start > 0 else host_start, len(s))
|
||||
|
||||
# Uncomment to enable the kivy live editor console
|
||||
# Press Ctrl-E (with numlock/capslock) disabled to open
|
||||
# from kivy.core.window import Window
|
||||
# from kivy.modules import console
|
||||
# console.create_console(Window, self.container)
|
||||
|
||||
return self.container
|
||||
|
||||
def add_client_tab(self, title: str, content: Widget) -> Widget:
|
||||
def add_client_tab(self, title: str, content: Widget, index: int = -1) -> Widget:
|
||||
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content.
|
||||
Returns the new tab widget, with the provided content being placed on the tab as content."""
|
||||
new_tab = MDTabsItem(MDTabsItemText(text=title))
|
||||
new_tab.content = content
|
||||
self.tabs.add_widget(new_tab)
|
||||
self.tabs.carousel.add_widget(new_tab.content)
|
||||
if -1 < index <= len(self.tabs.carousel.slides):
|
||||
new_tab.bind(on_release=self.tabs.set_active_item)
|
||||
new_tab._tabs = self.tabs
|
||||
self.tabs.ids.container.add_widget(new_tab, index=index)
|
||||
self.tabs.carousel.add_widget(new_tab.content, index=len(self.tabs.carousel.slides) - index)
|
||||
else:
|
||||
self.tabs.add_widget(new_tab)
|
||||
self.tabs.carousel.add_widget(new_tab.content)
|
||||
return new_tab
|
||||
|
||||
def update_texts(self, dt):
|
||||
@@ -1002,8 +1092,9 @@ class HintLayout(MDBoxLayout):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
boxlayout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(55))
|
||||
boxlayout.add_widget(MDLabel(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(55)))
|
||||
boxlayout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(40))
|
||||
boxlayout.add_widget(MDLabel(text="New Hint:", size_hint_x=None, size_hint_y=None,
|
||||
height=dp(40), width=dp(75), halign="center", valign="center"))
|
||||
boxlayout.add_widget(AutocompleteHintInput())
|
||||
self.add_widget(boxlayout)
|
||||
|
||||
@@ -1013,7 +1104,7 @@ class HintLayout(MDBoxLayout):
|
||||
if fix_func:
|
||||
fix_func()
|
||||
|
||||
|
||||
|
||||
status_names: typing.Dict[HintStatus, str] = {
|
||||
HintStatus.HINT_FOUND: "Found",
|
||||
HintStatus.HINT_UNSPECIFIED: "Unspecified",
|
||||
@@ -1110,6 +1201,7 @@ class HintLog(MDRecycleView):
|
||||
|
||||
|
||||
class ApAsyncImage(AsyncImage):
|
||||
|
||||
def is_uri(self, filename: str) -> bool:
|
||||
if filename.startswith("ap:"):
|
||||
return True
|
||||
@@ -1155,7 +1247,23 @@ class E(ExceptionHandler):
|
||||
class KivyJSONtoTextParser(JSONtoTextParser):
|
||||
# dummy class to absorb kvlang definitions
|
||||
class TextColors(Widget):
|
||||
pass
|
||||
white: str = StringProperty("FFFFFF")
|
||||
black: str = StringProperty("000000")
|
||||
red: str = StringProperty("EE0000")
|
||||
green: str = StringProperty("00FF7F")
|
||||
yellow: str = StringProperty("FAFAD2")
|
||||
blue: str = StringProperty("6495ED")
|
||||
magenta: str = StringProperty("EE00EE")
|
||||
cyan: str = StringProperty("00EEEE")
|
||||
slateblue: str = StringProperty("6D8BE8")
|
||||
plum: str = StringProperty("AF99EF")
|
||||
salmon: str = StringProperty("FA8072")
|
||||
orange: str = StringProperty("FF7700")
|
||||
# KivyMD parameters
|
||||
theme_style: str = StringProperty("Dark")
|
||||
primary_palette: str = StringProperty("Lightsteelblue")
|
||||
dynamic_scheme_name: str = StringProperty("VIBRANT")
|
||||
dynamic_scheme_contrast: int = NumericProperty(0)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# we grab the color definitions from the .kv file, then overwrite the JSONtoTextParser default entries
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import List, TYPE_CHECKING, Dict, Any
|
||||
from schema import Schema, Optional
|
||||
from dataclasses import dataclass
|
||||
from worlds.AutoWorld import PerGameCommonOptions
|
||||
from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle, OptionGroup
|
||||
from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle, OptionGroup, StartInventoryPool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import HatInTimeWorld
|
||||
@@ -625,6 +625,8 @@ class ParadeTrapWeight(Range):
|
||||
|
||||
@dataclass
|
||||
class AHITOptions(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
|
||||
EndGoal: EndGoal
|
||||
ActRandomizer: ActRandomizer
|
||||
ActPlando: ActPlando
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld
|
||||
from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \
|
||||
calculate_yarn_costs, alps_hooks
|
||||
calculate_yarn_costs, alps_hooks, junk_weights
|
||||
from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region
|
||||
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \
|
||||
get_total_locations
|
||||
@@ -78,6 +78,9 @@ class HatInTimeWorld(World):
|
||||
self.nyakuza_thug_items: Dict[str, int] = {}
|
||||
self.badge_seller_count: int = 0
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.random.choices(list(junk_weights.keys()), weights=junk_weights.values(), k=1)[0]
|
||||
|
||||
def generate_early(self):
|
||||
adjust_options(self)
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ Boosts have logic associated with them in order to verify you can always reach t
|
||||
- I need to kill a unit with a slinger/archer/musketman or some other obsolete unit I can't build anymore, how can I do this?
|
||||
- Don't forget you can go into the Tech Tree and click on a Vanilla tech you've received in order to toggle it on/off. This is necessary in order to pursue some of the boosts if you receive techs in certain orders.
|
||||
- Something happened, and I'm not able to unlock the boost due to game rules!
|
||||
- A few scenarios you may worry about: "Found a religion", "Make an alliance with another player", "Develop an alliance to level 2", "Build a wonder from X Era", to name a few. Any boost that is "miss-able" has been flagged as an "Excluded" location and will not ever receive a progression item. For a list of how each boost is flagged, take a look [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/boosts.json).
|
||||
- A few scenarios you may worry about: "Found a religion", "Make an alliance with another player", "Develop an alliance to level 2", "Build a wonder from X Era", to name a few. Any boost that is "miss-able" has been flagged as an "Excluded" location and will not ever receive a progression item. For a list of how each boost is flagged, take a look [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/boosts.py).
|
||||
- I'm worried that my `PROGRESSIVE_ERA` item is going to be stuck in a boost I won't have time to complete before my maximum unlocked era ends!
|
||||
- The unpredictable timing of boosts and unlocking them can occasionally lead to scenarios where you'll have to first encounter a locked era defeat and then load a previous save. To help reduce the frequency of this, local `PROGRESSIVE_ERA` items will never be located at a boost check.
|
||||
- There's too many boosts, how will I know which one's I should focus on?!
|
||||
|
||||
@@ -14,22 +14,17 @@ The following are required in order to play Civ VI in Archipelago:
|
||||
|
||||
## Enabling the tuner
|
||||
|
||||
Depending on how you installed Civ 6 you will have to navigate to one of the following:
|
||||
|
||||
- `YOUR_USER/Documents/My Games/Sid Meier's Civilization VI/AppOptions.txt`
|
||||
- `YOUR_USER/AppData/Local/Firaxis Games/Sid Meier's Civilization VI/AppOptions.txt`
|
||||
|
||||
Once you have located your `AppOptions.txt`, do a search for `Enable FireTuner`. Set `EnableTuner` to `1` instead of `0`. **NOTE**: While this is active, achievements will be disabled.
|
||||
In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
|
||||
|
||||
## Mod Installation
|
||||
|
||||
1. Download and unzip the latest release of the mod from [GitHub](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
|
||||
|
||||
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`.
|
||||
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. If you use OneDrive, check if the folder is instead located in your OneDrive file structure.
|
||||
|
||||
3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. Associate the file with the Archipelago Launcher and double click it.
|
||||
|
||||
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder.
|
||||
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. If double clicking the `.apcivvi` file doesn't generate a folder, you can just rename it to a file ending with `.zip` and extract its contents to a new folder. To do this, right click the `.apcivvi` file and click "Rename", make sure it ends in `.zip`, then right click it again and select "Extract All".
|
||||
|
||||
5. Your finished mod folder should look something like this:
|
||||
|
||||
|
||||
@@ -930,7 +930,7 @@ location_tables: Dict[str, List[DS3LocationData]] = {
|
||||
"Great Swamp Ring", miniboss=True), # Giant Crab drop
|
||||
DS3LocationData("RS: Blue Sentinels - Horace", "Blue Sentinels",
|
||||
missable=True, npc=True), # Horace quest
|
||||
DS3LocationData("RS: Crystal Gem - stronghold, lizard", "Crystal Gem"),
|
||||
DS3LocationData("RS: Crystal Gem - stronghold, lizard", "Crystal Gem", lizard=True),
|
||||
DS3LocationData("RS: Fading Soul - woods by Crucifixion Woods bonfire", "Fading Soul",
|
||||
static='03,0:53300210::'),
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import unittest
|
||||
from typing import Dict
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import NamedRange
|
||||
from .option_names import options_to_include
|
||||
from .checks.world_checks import assert_can_win, assert_same_number_items_locations
|
||||
from . import DLCQuestTestBase, setup_dlc_quest_solo_multiworld
|
||||
from .checks.world_checks import assert_can_win, assert_same_number_items_locations
|
||||
from .option_names import options_to_include
|
||||
|
||||
|
||||
def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
@@ -38,6 +39,8 @@ class TestGenerateDynamicOptions(DLCQuestTestBase):
|
||||
basic_checks(self, multiworld)
|
||||
|
||||
def test_given_option_truple_when_generate_then_basic_checks(self):
|
||||
if self.skip_long_tests:
|
||||
raise unittest.SkipTest("Long tests disabled")
|
||||
num_options = len(options_to_include)
|
||||
for option1_index in range(0, num_options):
|
||||
for option2_index in range(option1_index + 1, num_options):
|
||||
@@ -59,6 +62,8 @@ class TestGenerateDynamicOptions(DLCQuestTestBase):
|
||||
basic_checks(self, multiworld)
|
||||
|
||||
def test_given_option_quartet_when_generate_then_basic_checks(self):
|
||||
if self.skip_long_tests:
|
||||
raise unittest.SkipTest("Long tests disabled")
|
||||
num_options = len(options_to_include)
|
||||
for option1_index in range(0, num_options):
|
||||
for option2_index in range(option1_index + 1, num_options):
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from typing import Dict, FrozenSet, Tuple, Any
|
||||
import os
|
||||
from argparse import Namespace
|
||||
from typing import ClassVar
|
||||
from typing import Dict, FrozenSet, Tuple, Any
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from test.bases import WorldTestBase
|
||||
from .. import DLCqworld
|
||||
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
|
||||
from worlds.AutoWorld import call_all
|
||||
from .. import DLCqworld
|
||||
|
||||
|
||||
class DLCQuestTestBase(WorldTestBase):
|
||||
game = "DLCQuest"
|
||||
world: DLCqworld
|
||||
player: ClassVar[int] = 1
|
||||
# Set False to run tests that take long
|
||||
skip_long_tests: bool = True
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
cls.skip_long_tests = not bool(os.environ.get("long"))
|
||||
|
||||
def world_setup(self, *args, **kwargs):
|
||||
super().world_setup(*args, **kwargs)
|
||||
|
||||
@@ -8,17 +8,20 @@ from schema import Schema, Optional, And, Or, SchemaError
|
||||
from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \
|
||||
StartInventoryPool, PerGameCommonOptions, OptionGroup
|
||||
|
||||
|
||||
# schema helpers
|
||||
class FloatRange:
|
||||
def __init__(self, low, high):
|
||||
self._low = low
|
||||
self._high = high
|
||||
|
||||
def validate(self, value):
|
||||
def validate(self, value) -> float:
|
||||
if not isinstance(value, (float, int)):
|
||||
raise SchemaError(f"should be instance of float or int, but was {value!r}")
|
||||
if not self._low <= value <= self._high:
|
||||
raise SchemaError(f"{value} is not between {self._low} and {self._high}")
|
||||
return float(value)
|
||||
|
||||
|
||||
LuaBool = Or(bool, And(int, lambda n: n in (0, 1)))
|
||||
|
||||
|
||||
@@ -450,7 +450,7 @@ class GrubHuntGoal(NamedRange):
|
||||
display_name = "Grub Hunt Goal"
|
||||
range_start = 1
|
||||
range_end = 46
|
||||
special_range_names = {"all": -1}
|
||||
special_range_names = {"all": -1, "forty_six": 46}
|
||||
default = 46
|
||||
|
||||
|
||||
|
||||
@@ -3,34 +3,34 @@
|
||||
## Required Software
|
||||
* Download and unzip the Lumafly Mod Manager from the [Lumafly website](https://themulhima.github.io/Lumafly/).
|
||||
* A legal copy of Hollow Knight.
|
||||
* Steam, Gog, and Xbox Game Pass versions of the game are supported.
|
||||
* Windows, Mac, and Linux (including Steam Deck) are supported.
|
||||
* Steam, Gog, and Xbox Game Pass versions of the game are supported.
|
||||
* Windows, Mac, and Linux (including Steam Deck) are supported.
|
||||
|
||||
## Installing the Archipelago Mod using Lumafly
|
||||
1. Launch Lumafly and ensure it locates your Hollow Knight installation directory.
|
||||
2. Install the Archipelago mods by doing either of the following:
|
||||
* Click one of the links below to allow Lumafly to install the mods. Lumafly will prompt for confirmation.
|
||||
* [Archipelago and dependencies only](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago)
|
||||
* [Archipelago with rando essentials](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago/Archipelago%20Map%20Mod/RecentItemsDisplay/DebugMod/RandoStats/Additional%20Timelines/CompassAlwaysOn/AdditionalMaps/)
|
||||
(includes Archipelago Map Mod, RecentItemsDisplay, DebugMod, RandoStats, AdditionalTimelines, CompassAlwaysOn,
|
||||
and AdditionalMaps).
|
||||
* Click the "Install" button near the "Archipelago" mod entry. If desired, also install "Archipelago Map Mod"
|
||||
to use as an in-game tracker.
|
||||
* Click one of the links below to allow Lumafly to install the mods. Lumafly will prompt for confirmation.
|
||||
* [Archipelago and dependencies only](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago)
|
||||
* [Archipelago with rando essentials](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago/Archipelago%20Map%20Mod/RecentItemsDisplay/DebugMod/RandoStats/Additional%20Timelines/CompassAlwaysOn/AdditionalMaps/)
|
||||
(includes Archipelago Map Mod, RecentItemsDisplay, DebugMod, RandoStats, AdditionalTimelines, CompassAlwaysOn,
|
||||
and AdditionalMaps).
|
||||
* Click the "Install" button near the "Archipelago" mod entry. If desired, also install "Archipelago Map Mod"
|
||||
to use as an in-game tracker.
|
||||
3. Launch the game, you're all set!
|
||||
|
||||
### What to do if Lumafly fails to find your installation directory
|
||||
1. Find the directory manually.
|
||||
* Xbox Game Pass:
|
||||
1. Enter the Xbox app and move your mouse over "Hollow Knight" on the left sidebar.
|
||||
2. Click the three points then click "Manage".
|
||||
3. Go to the "Files" tab and select "Browse...".
|
||||
4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
|
||||
* Steam:
|
||||
1. You likely put your Steam library in a non-standard place. If this is the case, you probably know where
|
||||
it is. Find your steam library and then find the Hollow Knight folder and copy the path.
|
||||
* Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight`
|
||||
* Linux/Steam Deck - ~/.local/share/Steam/steamapps/common/Hollow Knight
|
||||
* Mac - ~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app
|
||||
* Xbox Game Pass:
|
||||
1. Enter the Xbox app and move your mouse over "Hollow Knight" on the left sidebar.
|
||||
2. Click the three points then click "Manage".
|
||||
3. Go to the "Files" tab and select "Browse...".
|
||||
4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
|
||||
* Steam:
|
||||
1. You likely put your Steam library in a non-standard place. If this is the case, you probably know where
|
||||
it is. Find your steam library and then find the Hollow Knight folder and copy the path.
|
||||
* Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight`
|
||||
* Linux/Steam Deck - ~/.local/share/Steam/steamapps/common/Hollow Knight
|
||||
* Mac - ~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app
|
||||
2. Run Lumafly as an administrator and, when it asks you for the path, paste the path you copied.
|
||||
|
||||
## Configuring your YAML File
|
||||
@@ -49,9 +49,9 @@ website to generate a YAML using a graphical interface.
|
||||
4. Enter the correct settings for your Archipelago server.
|
||||
5. Hit **Start** to begin the game. The game will stall for a few seconds while it does all item placements.
|
||||
6. The game will immediately drop you into the randomized game.
|
||||
* If you are waiting for a countdown then wait for it to lapse before hitting Start.
|
||||
* Or hit Start then pause the game once you're in it.
|
||||
|
||||
* If you are waiting for a countdown then wait for it to lapse before hitting Start.
|
||||
* Or hit Start then pause the game once you're in it.
|
||||
|
||||
## Hints and other commands
|
||||
While playing in a multiworld, you can interact with the server using various commands listed in the
|
||||
[commands guide](/tutorial/Archipelago/commands/en). You can use the Archipelago Text Client to do this,
|
||||
|
||||
@@ -3,28 +3,28 @@
|
||||
## Programas obrigatórios
|
||||
* Baixe e extraia o Lumafly Mod Manager (gerenciador de mods Lumafly) do [Site Lumafly](https://themulhima.github.io/Lumafly/).
|
||||
* Uma cópia legal de Hollow Knight.
|
||||
* Versões Steam, Gog, e Xbox Game Pass do jogo são suportadas.
|
||||
* Windows, Mac, e Linux (incluindo Steam Deck) são suportados.
|
||||
* Versões Steam, Gog, e Xbox Game Pass do jogo são suportadas.
|
||||
* Windows, Mac, e Linux (incluindo Steam Deck) são suportados.
|
||||
|
||||
## Instalando o mod Archipelago Mod usando Lumafly
|
||||
1. Abra o Lumafly e confirme que ele localizou sua pasta de instalação do Hollow Knight.
|
||||
2. Clique em "Install (instalar)" perto da opção "Archipelago" mod.
|
||||
* Se quiser, instale também o "Archipelago Map Mod (mod do mapa do archipelago)" para usá-lo como rastreador dentro do jogo.
|
||||
* Se quiser, instale também o "Archipelago Map Mod (mod do mapa do archipelago)" para usá-lo como rastreador dentro do jogo.
|
||||
3. Abra o jogo, tudo preparado!
|
||||
|
||||
### O que fazer se o Lumafly falha em encontrar a sua pasta de instalação
|
||||
1. Encontre a pasta manualmente.
|
||||
* Xbox Game Pass:
|
||||
1. Entre no seu aplicativo Xbox e mova seu mouse em cima de "Hollow Knight" na sua barra da esquerda.
|
||||
2. Clique nos 3 pontos depois clique gerenciar.
|
||||
3. Vá nos arquivos e selecione procurar.
|
||||
4. Clique em "Hollow Knight", depois em "Content (Conteúdo)", depois clique na barra com o endereço e a copie.
|
||||
* Steam:
|
||||
1. Você provavelmente colocou sua biblioteca Steam num local não padrão. Se esse for o caso você provavelmente sabe onde está.
|
||||
. Encontre sua biblioteca Steam, depois encontre a pasta do Hollow Knight e copie seu endereço.
|
||||
* Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight`
|
||||
* Linux/Steam Deck - `~/.local/share/Steam/steamapps/common/Hollow Knight`
|
||||
* Mac - `~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app`
|
||||
* Xbox Game Pass:
|
||||
1. Entre no seu aplicativo Xbox e mova seu mouse em cima de "Hollow Knight" na sua barra da esquerda.
|
||||
2. Clique nos 3 pontos depois clique gerenciar.
|
||||
3. Vá nos arquivos e selecione procurar.
|
||||
4. Clique em "Hollow Knight", depois em "Content (Conteúdo)", depois clique na barra com o endereço e a copie.
|
||||
* Steam:
|
||||
1. Você provavelmente colocou sua biblioteca Steam num local não padrão. Se esse for o caso você provavelmente sabe onde está.
|
||||
Encontre sua biblioteca Steam, depois encontre a pasta do Hollow Knight e copie seu endereço.
|
||||
* Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight`
|
||||
* Linux/Steam Deck - `~/.local/share/Steam/steamapps/common/Hollow Knight`
|
||||
* Mac - `~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app`
|
||||
2. Rode o Lumafly como administrador e, quando ele perguntar pelo endereço do arquivo, cole o endereço do arquivo que você copiou.
|
||||
|
||||
## Configurando seu arquivo YAML
|
||||
@@ -43,9 +43,9 @@ para gerar o YAML usando a interface gráfica.
|
||||
4. Coloque as configurações corretas do seu servidor Archipelago.
|
||||
5. Aperte em **Começar**. O jogo vai travar por uns segundos enquanto ele coloca todos itens.
|
||||
6. O jogo vai te colocar imediatamente numa partida randomizada.
|
||||
* Se você está esperando uma contagem então espere ele cair antes de apertar começar.
|
||||
* Ou clique em começar e pause o jogo enquanto estiver nele.
|
||||
|
||||
* Se você está esperando uma contagem então espere ele cair antes de apertar começar.
|
||||
* Ou clique em começar e pause o jogo enquanto estiver nele.
|
||||
|
||||
## Dicas e outros comandos
|
||||
Enquanto jogar um multiworld, você pode interagir com o servidor usando vários comandos listados no
|
||||
[Guia de comandos](/tutorial/Archipelago/commands/en). Você pode usar o cliente de texto do Archipelago para isso,
|
||||
|
||||
@@ -174,7 +174,7 @@ class LingoWorld(World):
|
||||
"death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels",
|
||||
"enable_pilgrimage", "sunwarp_access", "mastery_achievements", "level_2_requirement", "location_checks",
|
||||
"early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps",
|
||||
"group_doors", "speed_boost_mode"
|
||||
"group_doors", "speed_boost_mode", "shuffle_postgame"
|
||||
]
|
||||
|
||||
slot_data = {
|
||||
|
||||
@@ -34,12 +34,32 @@ ITEMS_BY_GROUP: Dict[str, List[str]] = {}
|
||||
|
||||
TRAP_ITEMS: List[str] = ["Slowness Trap", "Iceland Trap", "Atbash Trap"]
|
||||
|
||||
PROGUSEFUL_ITEMS: List[str] = [
|
||||
"Crossroads - Roof Access",
|
||||
"Black",
|
||||
"Red",
|
||||
"Blue",
|
||||
"Yellow",
|
||||
"Purple",
|
||||
"Sunwarps",
|
||||
"Tenacious Entrance Panels",
|
||||
"The Tenacious - Black Palindromes (Panels)",
|
||||
"Hub Room - RAT (Panel)",
|
||||
"Outside The Wanderer - WANDERLUST (Panel)",
|
||||
"Orange Tower Panels"
|
||||
]
|
||||
|
||||
|
||||
def get_prog_item_classification(item_name: str):
|
||||
if item_name in PROGUSEFUL_ITEMS:
|
||||
return ItemClassification.progression | ItemClassification.useful
|
||||
else:
|
||||
return ItemClassification.progression
|
||||
|
||||
|
||||
def load_item_data():
|
||||
global ALL_ITEM_TABLE, ITEMS_BY_GROUP
|
||||
|
||||
for color in ["Black", "Red", "Blue", "Yellow", "Green", "Orange", "Gray", "Brown", "Purple"]:
|
||||
ALL_ITEM_TABLE[color] = ItemData(get_special_item_id(color), ItemClassification.progression,
|
||||
ALL_ITEM_TABLE[color] = ItemData(get_special_item_id(color), get_prog_item_classification(color),
|
||||
ItemType.COLOR, False, [])
|
||||
ITEMS_BY_GROUP.setdefault("Colors", []).append(color)
|
||||
|
||||
@@ -53,16 +73,16 @@ def load_item_data():
|
||||
door_groups.add(door.door_group)
|
||||
|
||||
ALL_ITEM_TABLE[door.item_name] = \
|
||||
ItemData(get_door_item_id(room_name, door_name), ItemClassification.progression, ItemType.NORMAL,
|
||||
door.has_doors, door.painting_ids)
|
||||
ItemData(get_door_item_id(room_name, door_name), get_prog_item_classification(door.item_name),
|
||||
ItemType.NORMAL, door.has_doors, door.painting_ids)
|
||||
ITEMS_BY_GROUP.setdefault("Doors", []).append(door.item_name)
|
||||
|
||||
if door.item_group is not None:
|
||||
ITEMS_BY_GROUP.setdefault(door.item_group, []).append(door.item_name)
|
||||
|
||||
for group in door_groups:
|
||||
ALL_ITEM_TABLE[group] = ItemData(get_door_group_item_id(group),
|
||||
ItemClassification.progression, ItemType.NORMAL, True, [])
|
||||
ALL_ITEM_TABLE[group] = ItemData(get_door_group_item_id(group), get_prog_item_classification(group),
|
||||
ItemType.NORMAL, True, [])
|
||||
ITEMS_BY_GROUP.setdefault("Doors", []).append(group)
|
||||
|
||||
panel_groups: Set[str] = set()
|
||||
@@ -72,11 +92,12 @@ def load_item_data():
|
||||
panel_groups.add(panel_door.panel_group)
|
||||
|
||||
ALL_ITEM_TABLE[panel_door.item_name] = ItemData(get_panel_door_item_id(room_name, panel_door_name),
|
||||
ItemClassification.progression, ItemType.NORMAL, False, [])
|
||||
get_prog_item_classification(panel_door.item_name),
|
||||
ItemType.NORMAL, False, [])
|
||||
ITEMS_BY_GROUP.setdefault("Panels", []).append(panel_door.item_name)
|
||||
|
||||
for group in panel_groups:
|
||||
ALL_ITEM_TABLE[group] = ItemData(get_panel_group_item_id(group), ItemClassification.progression,
|
||||
ALL_ITEM_TABLE[group] = ItemData(get_panel_group_item_id(group), get_prog_item_classification(group),
|
||||
ItemType.NORMAL, False, [])
|
||||
ITEMS_BY_GROUP.setdefault("Panels", []).append(group)
|
||||
|
||||
@@ -101,7 +122,7 @@ def load_item_data():
|
||||
|
||||
for item_name in PROGRESSIVE_ITEMS:
|
||||
ALL_ITEM_TABLE[item_name] = ItemData(get_progressive_item_id(item_name),
|
||||
ItemClassification.progression, ItemType.NORMAL, False, [])
|
||||
get_prog_item_classification(item_name), ItemType.NORMAL, False, [])
|
||||
|
||||
|
||||
# Initialize the item data at module scope.
|
||||
|
||||
@@ -35,8 +35,6 @@ LOCATIONS_BY_GROUP: Dict[str, List[str]] = {}
|
||||
|
||||
|
||||
def load_location_data():
|
||||
global ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP
|
||||
|
||||
for room_name, panels in PANELS_BY_ROOM.items():
|
||||
for panel_name, panel in panels.items():
|
||||
location_name = f"{room_name} - {panel_name}" if panel.location_name is None else panel.location_name
|
||||
|
||||
@@ -58,8 +58,7 @@ def hash_file(path):
|
||||
|
||||
|
||||
def load_static_data(ll1_path, ids_path):
|
||||
global PAINTING_EXITS, SPECIAL_ITEM_IDS, PANEL_LOCATION_IDS, DOOR_LOCATION_IDS, DOOR_ITEM_IDS, \
|
||||
DOOR_GROUP_ITEM_IDS, PROGRESSIVE_ITEM_IDS, PANEL_DOOR_ITEM_IDS, PANEL_GROUP_ITEM_IDS
|
||||
global PAINTING_EXITS
|
||||
|
||||
# Load in all item and location IDs. These are broken up into groups based on the type of item/location.
|
||||
with open(ids_path, "r") as file:
|
||||
@@ -128,7 +127,7 @@ def load_static_data(ll1_path, ids_path):
|
||||
|
||||
|
||||
def process_single_entrance(source_room: str, room_name: str, door_obj) -> RoomEntrance:
|
||||
global PAINTING_ENTRANCES, PAINTING_EXIT_ROOMS
|
||||
global PAINTING_ENTRANCES
|
||||
|
||||
entrance_type = EntranceType.NORMAL
|
||||
if "painting" in door_obj and door_obj["painting"]:
|
||||
@@ -175,8 +174,6 @@ def process_entrance(source_room, doors, room_obj):
|
||||
|
||||
|
||||
def process_panel_door(room_name, panel_door_name, panel_door_data):
|
||||
global PANEL_DOORS_BY_ROOM, PANEL_DOOR_BY_PANEL_BY_ROOM
|
||||
|
||||
panels: List[RoomAndPanel] = list()
|
||||
for panel in panel_door_data["panels"]:
|
||||
if isinstance(panel, dict):
|
||||
@@ -215,8 +212,6 @@ def process_panel_door(room_name, panel_door_name, panel_door_data):
|
||||
|
||||
|
||||
def process_panel(room_name, panel_name, panel_data):
|
||||
global PANELS_BY_ROOM
|
||||
|
||||
# required_room can either be a single room or a list of rooms.
|
||||
if "required_room" in panel_data:
|
||||
if isinstance(panel_data["required_room"], list):
|
||||
@@ -310,8 +305,6 @@ def process_panel(room_name, panel_name, panel_data):
|
||||
|
||||
|
||||
def process_door(room_name, door_name, door_data):
|
||||
global DOORS_BY_ROOM
|
||||
|
||||
# The item name associated with a door can be explicitly specified in the configuration. If it is not, it is
|
||||
# generated from the room and door name.
|
||||
if "item_name" in door_data:
|
||||
@@ -409,8 +402,6 @@ def process_door(room_name, door_name, door_data):
|
||||
|
||||
|
||||
def process_painting(room_name, painting_data):
|
||||
global PAINTINGS, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS
|
||||
|
||||
# Read in information about this painting and store it in an object.
|
||||
painting_id = painting_data["id"]
|
||||
|
||||
@@ -468,8 +459,6 @@ def process_painting(room_name, painting_data):
|
||||
|
||||
|
||||
def process_sunwarp(room_name, sunwarp_data):
|
||||
global SUNWARP_ENTRANCES, SUNWARP_EXITS
|
||||
|
||||
if sunwarp_data["direction"] == "enter":
|
||||
SUNWARP_ENTRANCES[sunwarp_data["dots"] - 1] = room_name
|
||||
else:
|
||||
@@ -477,8 +466,6 @@ def process_sunwarp(room_name, sunwarp_data):
|
||||
|
||||
|
||||
def process_progressive_door(room_name, progression_name, progression_doors):
|
||||
global PROGRESSIVE_ITEMS, PROGRESSIVE_DOORS_BY_ROOM
|
||||
|
||||
# Progressive items are configured as a list of doors.
|
||||
PROGRESSIVE_ITEMS.add(progression_name)
|
||||
|
||||
@@ -497,8 +484,6 @@ def process_progressive_door(room_name, progression_name, progression_doors):
|
||||
|
||||
|
||||
def process_progressive_panel(room_name, progression_name, progression_panel_doors):
|
||||
global PROGRESSIVE_ITEMS, PROGRESSIVE_PANELS_BY_ROOM
|
||||
|
||||
# Progressive items are configured as a list of panel doors.
|
||||
PROGRESSIVE_ITEMS.add(progression_name)
|
||||
|
||||
@@ -517,8 +502,6 @@ def process_progressive_panel(room_name, progression_name, progression_panel_doo
|
||||
|
||||
|
||||
def process_room(room_name, room_data):
|
||||
global ALL_ROOMS
|
||||
|
||||
room_obj = Room(room_name, [])
|
||||
|
||||
if "entrances" in room_data:
|
||||
|
||||
@@ -46,8 +46,16 @@ class MessengerWeb(WebWorld):
|
||||
"setup/en",
|
||||
["alwaysintreble"],
|
||||
)
|
||||
plando_en = Tutorial(
|
||||
"The Messenger Plando Guide",
|
||||
"A guide detailing The Messenger's various supported plando options.",
|
||||
"English",
|
||||
"plando_en.md",
|
||||
"plando/en",
|
||||
["alwaysintreble"],
|
||||
)
|
||||
|
||||
tutorials = [tut_en]
|
||||
tutorials = [tut_en, plando_en]
|
||||
|
||||
|
||||
class MessengerWorld(World):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# The Messenger
|
||||
|
||||
## Quick Links
|
||||
|
||||
- [Setup](/tutorial/The%20Messenger/setup/en)
|
||||
- [Options Page](/games/The%20Messenger/player-options)
|
||||
- [Courier Github](https://github.com/Brokemia/Courier)
|
||||
@@ -26,6 +27,7 @@ obtained. You'll be forced to do sections of the game in different ways with you
|
||||
## Where can I find items?
|
||||
|
||||
You can find items wherever items can be picked up in the original game. This includes:
|
||||
|
||||
* Shopkeeper dialog where the player originally gains movement items
|
||||
* Quest Item pickups
|
||||
* Music Box notes
|
||||
@@ -42,6 +44,7 @@ group of items. Hinting for a group will choose a random item from the group tha
|
||||
for it.
|
||||
|
||||
The groups you can use for The Messenger are:
|
||||
|
||||
* Notes - This covers the music notes
|
||||
* Keys - An alternative name for the music notes
|
||||
* Crest - The Sun and Moon Crests
|
||||
@@ -64,16 +67,29 @@ The groups you can use for The Messenger are:
|
||||
be entered in game.
|
||||
|
||||
## Known issues
|
||||
|
||||
* Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item
|
||||
* If you receive the Magic Firefly while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit
|
||||
to Searing Crags and re-enter to get it to play correctly.
|
||||
* Teleporting back to HQ, then returning to the same level you just left through a Portal can cause Ninja to run left
|
||||
and enter a different portal than the one entered by the player or lead to other incorrect inputs, causing a soft lock
|
||||
* Text entry menus don't accept controller input
|
||||
* In power seal hunt mode, the chest must be opened by entering the shop from a level. Teleporting to HQ and opening the
|
||||
chest will not work.
|
||||
|
||||
## What do I do if I have a problem?
|
||||
|
||||
If you believe something happened that isn't intended, please get the `log.txt` from the folder of your game
|
||||
installation and send a bug report either on GitHub or the [Archipelago Discord Server](http://archipelago.gg/discord)
|
||||
|
||||
## FAQ
|
||||
|
||||
* The tracker says I can get some checks in Howling Grotto, but I can't defeat the Emerald Golem. How do I get there?
|
||||
* Due to the way the vanilla game handles bosses and level transitions, if you die to him, the room will be unlocked,
|
||||
and you can leave.
|
||||
* I have the money wrench. Why won't the shopkeeper let me enter the sink?
|
||||
* The money wrench is both an item you must find or receive from another player and a location check, which you must
|
||||
purchase from the Artificer, as in vanilla.
|
||||
* How do I unfreeze Manfred? Where is the monk?
|
||||
* The monk will only appear near Manfred after you cleanse the Queen of Quills with the fairy (magic firefly).
|
||||
* I have all the power seals I need to win, but nothing is happening when I open the chest.
|
||||
* Due to how the level loading code works, I am currently unable to teleport you out of HQ at will; you must enter the
|
||||
shop from within a level.
|
||||
|
||||
101
worlds/messenger/docs/plando_en.md
Normal file
101
worlds/messenger/docs/plando_en.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# The Messenger Plando Guide
|
||||
|
||||
This guide details the usage of the game-specific plando options that The Messenger has. The Messenger also supports the
|
||||
generic item plando. For more information on what plando is and for information covering item plando, refer to the
|
||||
[generic Archipelago plando guide](/tutorial/Archipelago/plando/en). The Messenger also uses the generic connection
|
||||
plando system, but with specific behaviors that will be covered in this guide along with the other options.
|
||||
|
||||
## Shop Price Plando
|
||||
|
||||
This option allows you to specify prices for items in both shops. This also supports weighting, allowing you to choose
|
||||
from multiple different prices for any given item.
|
||||
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
The Messenger:
|
||||
shop_price_plan:
|
||||
Karuta Plates: 50
|
||||
Devil's Due: 1
|
||||
Barmath'azel Figurine:
|
||||
# left side is price, right side is weight
|
||||
500: 10
|
||||
700: 5
|
||||
1000: 20
|
||||
```
|
||||
|
||||
This block will make the item at the `Karuta Plates` node cost 50 shards, `Devil's Due` will cost 1 shard, and
|
||||
`Barmath'azel Figurine` will cost either 500, 700, or 1000, with 1000 being the most likely with a 20/35 chance.
|
||||
|
||||
## Portal Plando
|
||||
|
||||
This option allows you to specify certain outputs for the portals. This option will only be checked if portal shuffle
|
||||
and the `connections` plando host setting are enabled.
|
||||
|
||||
A portal connection is plandoed by specifying an `entrance` and an `exit`. This option also supports `percentage`, which
|
||||
is the percent chance that that connection occurs. The `entrance` is which portal is going to be entered, whereas the
|
||||
`exit` is where the portal will lead and can include a shop location, a checkpoint, or any portal. However, the
|
||||
portal exit must also be in the available pool for the selected portal shuffle option. For example, if portal shuffle is
|
||||
set to `shops`, then the valid exits will only be portals and shops; any exit that is a checkpoint will not be valid. If
|
||||
portal shuffle is set to `checkpoints`, you may not have multiple portals lead to the same area, e.g. `Seashell` and
|
||||
`Spike Wave` may not both be used since they are both in Quillshroom Marsh. If the option is set to `anywhere`, then all
|
||||
exits are valid.
|
||||
|
||||
All valid connections for portal shuffle can be found by scrolling through the [portals module](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/messenger/portals.py#L12).
|
||||
The entrance and exit should be written exactly as they appear within that file, except for when the **exit** point is a
|
||||
portal. In that case, it should have "Portal" included.
|
||||
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
The Messenger:
|
||||
portal_plando:
|
||||
- entrance: Riviere Turquoise
|
||||
exit: Wingsuit
|
||||
- entrance: Sunken Shrine
|
||||
exit: Sunny Day
|
||||
- entrance: Searing Crags
|
||||
exit: Glacial Peak Portal
|
||||
```
|
||||
|
||||
This block will make it so that the Riviere Turquoise Portal will exit to the Wingsuit Shop, the Sunken Shrine Portal
|
||||
will exit to the Sunny Day checkpoint, and the Searing Crags Portal will exit to the Glacial Peak Portal.
|
||||
|
||||
## Transition Plando
|
||||
|
||||
This option allows you to specify certain connections when using transition shuffle. This will only work if
|
||||
transition shuffle and the `connections` plando host setting are enabled.
|
||||
|
||||
Each transition connection is plandoed by specifying its attributes:
|
||||
|
||||
* `entrance` is where you will enter this transition from.
|
||||
* `exit` is where the transition will lead.
|
||||
* `percentage` is the chance this connection will happen at all.
|
||||
* `direction` is used to specify whether this connection will also go in reverse. This entry will be ignored if the
|
||||
transition shuffle is set to `coupled` or if the specified connection can only occur in one direction, such as exiting
|
||||
to Riviere Turquoise. The default direction is "both", which will make it so that returning through the exit
|
||||
transition will return you to where you entered it from. "entrance" and "exit" are treated the same, with them both
|
||||
making this transition only one-way.
|
||||
|
||||
Valid connections can be found in the [`RANDOMIZED_CONNECTIONS` dictionary](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/messenger/connections.py#L640).
|
||||
The keys (left) are entrances, and values (right) are exits. Whether you want the connection to go both ways or not,
|
||||
both sides must either be two-way or one-way; E.g. connecting Artificer (Corrupted Future Portal) to one of the
|
||||
Quillshroom Marsh entrances is not a valid pairing. A pairing can be determined to be two-way if both the entrance and
|
||||
exit of that pair are an exit and entrance of another pairing, respectively.
|
||||
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
The Messenger:
|
||||
plando_connections:
|
||||
- entrance: Searing Crags - Top
|
||||
exit: Dark Cave - Right
|
||||
- entrance: Glacial Peak - Left
|
||||
exit: Corrupted Future
|
||||
```
|
||||
|
||||
This block will create the following connections:
|
||||
1. Leaving Searing Crags towards Glacial Peak will take you to the beginning of Dark Cave, and leaving the Dark Cave
|
||||
door will return you to the top of Searing Crags.
|
||||
2. Taking Manfred to leave Glacial Peak, will take you to Corrupted Future. There is no reverse connection here so it
|
||||
will always be one-way.
|
||||
@@ -16,17 +16,8 @@ class MessengerAccessibility(ItemsAccessibility):
|
||||
|
||||
class PortalPlando(PlandoConnections):
|
||||
"""
|
||||
Plando connections to be used with portal shuffle. Direction is ignored.
|
||||
List of valid connections can be found here: https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/messenger/portals.py#L12.
|
||||
The entering Portal should *not* have "Portal" appended.
|
||||
For the exits, those in checkpoints and shops should just be the name of the spot, while portals should have " Portal" at the end.
|
||||
Example:
|
||||
- entrance: Riviere Turquoise
|
||||
exit: Wingsuit
|
||||
- entrance: Sunken Shrine
|
||||
exit: Sunny Day
|
||||
- entrance: Searing Crags
|
||||
exit: Glacial Peak Portal
|
||||
Plando connections to be used with portal shuffle.
|
||||
Documentation on using this can be found in The Messenger plando guide.
|
||||
"""
|
||||
display_name = "Portal Plando Connections"
|
||||
portals = [f"{portal} Portal" for portal in PORTALS]
|
||||
@@ -40,14 +31,7 @@ class PortalPlando(PlandoConnections):
|
||||
class TransitionPlando(PlandoConnections):
|
||||
"""
|
||||
Plando connections to be used with transition shuffle.
|
||||
List of valid connections can be found at https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/messenger/connections.py#L641.
|
||||
Dictionary keys (left) are entrances and values (right) are exits. If transition shuffle is on coupled all plando
|
||||
connections will be coupled. If on decoupled, "entrance" and "exit" will be treated the same, simply making the
|
||||
plando connection one-way from entrance to exit.
|
||||
Example:
|
||||
- entrance: Searing Crags - Top
|
||||
exit: Dark Cave - Right
|
||||
direction: both
|
||||
Documentation on using this can be found in The Messenger plando guide.
|
||||
"""
|
||||
display_name = "Transition Plando Connections"
|
||||
entrances = frozenset(RANDOMIZED_CONNECTIONS.keys())
|
||||
@@ -147,7 +131,9 @@ class MusicBox(DefaultOnToggle):
|
||||
|
||||
|
||||
class NotesNeeded(Range):
|
||||
"""How many notes are needed to access the Music Box."""
|
||||
"""
|
||||
How many notes need to be found in order to access the Music Box. 6 are always needed to enter, so this places the others in your start inventory.
|
||||
"""
|
||||
display_name = "Notes Needed"
|
||||
range_start = 1
|
||||
range_end = 6
|
||||
|
||||
@@ -148,12 +148,13 @@ def set_rules(world: "MLSSWorld", excluded):
|
||||
and StateLogic.canDash(state, world.player)
|
||||
and StateLogic.canCrash(state, world.player)
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleWendyLarryHallwayDigspot),
|
||||
lambda state: StateLogic.ultra(state, world.player)
|
||||
and StateLogic.fire(state, world.player)
|
||||
and StateLogic.canCrash(state, world.player)
|
||||
)
|
||||
if world.options.chuckle_beans != 0:
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleWendyLarryHallwayDigspot),
|
||||
lambda state: StateLogic.ultra(state, world.player)
|
||||
and StateLogic.fire(state, world.player)
|
||||
and StateLogic.canCrash(state, world.player)
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleBeforeFawfulFightBlock1),
|
||||
lambda state: StateLogic.canDig(state, world.player)
|
||||
|
||||
@@ -1580,16 +1580,22 @@ def create_regions(world):
|
||||
|
||||
world.random.shuffle(world.item_pool)
|
||||
if not world.options.key_items_only:
|
||||
if "Player's House 2F - Player's PC" in world.options.exclude_locations:
|
||||
acceptable_item = lambda item: item.excludable
|
||||
elif "Player's House 2F - Player's PC" in world.options.priority_locations:
|
||||
acceptable_item = lambda item: item.advancement
|
||||
else:
|
||||
acceptable_item = lambda item: True
|
||||
def acceptable_item(item):
|
||||
return ("Badge" not in item.name and "Trap" not in item.name and item.name != "Pokedex"
|
||||
and "Coins" not in item.name and "Progressive" not in item.name
|
||||
and ("Player's House 2F - Player's PC" not in world.options.exclude_locations or item.excludable)
|
||||
and ("Player's House 2F - Player's PC" in world.options.exclude_locations or
|
||||
"Player's House 2F - Player's PC" not in world.options.priority_locations or item.advancement))
|
||||
for i, item in enumerate(world.item_pool):
|
||||
if acceptable_item(item):
|
||||
if acceptable_item(item) and (item.name not in world.options.non_local_items.value):
|
||||
world.pc_item = world.item_pool.pop(i)
|
||||
break
|
||||
else:
|
||||
for i, item in enumerate(world.item_pool):
|
||||
if acceptable_item(item):
|
||||
world.pc_item = world.item_pool.pop(i)
|
||||
break
|
||||
|
||||
|
||||
advancement_items = [item.name for item in world.item_pool if item.advancement] \
|
||||
+ [item.name for item in world.multiworld.precollected_items[world.player] if
|
||||
|
||||
@@ -5,12 +5,11 @@ from NetUtils import JSONMessagePart
|
||||
from kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser
|
||||
from kivy.app import App
|
||||
from kivy.clock import Clock
|
||||
from kivymd.uix.tab import MDTabsItem, MDTabsItemText
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivymd.uix.tooltip import MDTooltip
|
||||
from kivy.uix.scrollview import ScrollView
|
||||
from kivy.properties import StringProperty
|
||||
|
||||
@@ -26,30 +25,22 @@ class HoverableButton(HoverBehavior, Button):
|
||||
pass
|
||||
|
||||
|
||||
class MissionButton(HoverableButton):
|
||||
class MissionButton(HoverableButton, MDTooltip):
|
||||
tooltip_text = StringProperty("Test")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HoverableButton, self).__init__(*args, **kwargs)
|
||||
self.layout = FloatLayout()
|
||||
self.popuplabel = ServerToolTip(text=self.text, markup=True)
|
||||
self.popuplabel.padding = [5, 2, 5, 2]
|
||||
self.layout.add_widget(self.popuplabel)
|
||||
super(HoverableButton, self).__init__(**kwargs)
|
||||
self._tooltip = ServerToolTip(text=self.text, markup=True)
|
||||
self._tooltip.padding = [5, 2, 5, 2]
|
||||
|
||||
def on_enter(self):
|
||||
self.popuplabel.text = self.tooltip_text
|
||||
self._tooltip.text = self.tooltip_text
|
||||
|
||||
if self.ctx.current_tooltip:
|
||||
App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
|
||||
|
||||
if self.tooltip_text == "":
|
||||
self.ctx.current_tooltip = None
|
||||
else:
|
||||
App.get_running_app().root.add_widget(self.layout)
|
||||
self.ctx.current_tooltip = self.layout
|
||||
if self.tooltip_text != "":
|
||||
self.display_tooltip()
|
||||
|
||||
def on_leave(self):
|
||||
self.ctx.ui.clear_tooltip()
|
||||
self.remove_tooltip()
|
||||
|
||||
@property
|
||||
def ctx(self) -> SC2Context:
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import BinaryIO, Optional
|
||||
import Utils
|
||||
from worlds.Files import APDeltaPatch
|
||||
|
||||
|
||||
USHASH = '6e9c94511d04fac6e0a1e582c170be3a'
|
||||
|
||||
|
||||
@@ -20,9 +19,9 @@ class SoEDeltaPatch(APDeltaPatch):
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: Optional[str] = None) -> str:
|
||||
options = Utils.get_options()
|
||||
if not file_name:
|
||||
file_name = options["soe_options"]["rom_file"]
|
||||
from . import SoEWorld
|
||||
file_name = SoEWorld.settings.rom_file
|
||||
if not file_name:
|
||||
raise ValueError("Missing soe_options -> rom_file from host.yaml")
|
||||
if not os.path.exists(file_name):
|
||||
|
||||
@@ -145,7 +145,7 @@ class StardewValleyWorld(World):
|
||||
|
||||
def create_items(self):
|
||||
self.precollect_starting_season()
|
||||
self.precollect_farm_type_items()
|
||||
self.precollect_building_items()
|
||||
items_to_exclude = [excluded_items
|
||||
for excluded_items in self.multiworld.precollected_items[self.player]
|
||||
if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK,
|
||||
@@ -200,9 +200,16 @@ class StardewValleyWorld(World):
|
||||
starting_season = self.create_item(self.random.choice(season_pool))
|
||||
self.multiworld.push_precollected(starting_season)
|
||||
|
||||
def precollect_farm_type_items(self):
|
||||
if self.options.farm_type == FarmType.option_meadowlands and self.options.building_progression & BuildingProgression.option_progressive:
|
||||
self.multiworld.push_precollected(self.create_item("Progressive Coop"))
|
||||
def precollect_building_items(self):
|
||||
building_progression = self.content.features.building_progression
|
||||
# Not adding items when building are vanilla because the buildings are already placed in the world.
|
||||
if not building_progression.is_progressive:
|
||||
return
|
||||
|
||||
for building in building_progression.starting_buildings:
|
||||
item, quantity = building_progression.to_progressive_item(building)
|
||||
for _ in range(quantity):
|
||||
self.multiworld.push_precollected(self.create_item(item))
|
||||
|
||||
def setup_logic_events(self):
|
||||
def register_event(name: str, region: str, rule: StardewRule):
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from . import content_packs
|
||||
from .feature import cropsanity, friendsanity, fishsanity, booksanity, skill_progression, tool_progression
|
||||
from .feature import cropsanity, friendsanity, fishsanity, booksanity, building_progression, skill_progression, tool_progression
|
||||
from .game_content import ContentPack, StardewContent, StardewFeatures
|
||||
from .unpacking import unpack_content
|
||||
from .. import options
|
||||
from ..strings.building_names import Building
|
||||
|
||||
|
||||
def create_content(player_options: options.StardewValleyOptions) -> StardewContent:
|
||||
@@ -20,7 +21,7 @@ def choose_content_packs(player_options: options.StardewValleyOptions):
|
||||
if player_options.special_order_locations & options.SpecialOrderLocations.value_qi:
|
||||
active_packs.append(content_packs.qi_board_content_pack)
|
||||
|
||||
for mod in player_options.mods.value:
|
||||
for mod in sorted(player_options.mods.value):
|
||||
active_packs.append(content_packs.by_mod[mod])
|
||||
|
||||
return active_packs
|
||||
@@ -29,6 +30,7 @@ def choose_content_packs(player_options: options.StardewValleyOptions):
|
||||
def choose_features(player_options: options.StardewValleyOptions) -> StardewFeatures:
|
||||
return StardewFeatures(
|
||||
choose_booksanity(player_options.booksanity),
|
||||
choose_building_progression(player_options.building_progression, player_options.farm_type),
|
||||
choose_cropsanity(player_options.cropsanity),
|
||||
choose_fishsanity(player_options.fishsanity),
|
||||
choose_friendsanity(player_options.friendsanity, player_options.friendsanity_heart_size),
|
||||
@@ -109,6 +111,32 @@ def choose_friendsanity(friendsanity_option: options.Friendsanity, heart_size: o
|
||||
raise ValueError(f"No friendsanity feature mapped to {str(friendsanity_option.value)}")
|
||||
|
||||
|
||||
def choose_building_progression(building_option: options.BuildingProgression,
|
||||
farm_type_option: options.FarmType) -> building_progression.BuildingProgressionFeature:
|
||||
starting_buildings = {Building.farm_house, Building.pet_bowl, Building.shipping_bin}
|
||||
|
||||
if farm_type_option == options.FarmType.option_meadowlands:
|
||||
starting_buildings.add(Building.coop)
|
||||
|
||||
if (building_option == options.BuildingProgression.option_vanilla
|
||||
or building_option == options.BuildingProgression.option_vanilla_cheap
|
||||
or building_option == options.BuildingProgression.option_vanilla_very_cheap):
|
||||
return building_progression.BuildingProgressionVanilla(
|
||||
starting_buildings=starting_buildings,
|
||||
)
|
||||
|
||||
starting_buildings.remove(Building.shipping_bin)
|
||||
|
||||
if (building_option == options.BuildingProgression.option_progressive
|
||||
or building_option == options.BuildingProgression.option_progressive_cheap
|
||||
or building_option == options.BuildingProgression.option_progressive_very_cheap):
|
||||
return building_progression.BuildingProgressionProgressive(
|
||||
starting_buildings=starting_buildings,
|
||||
)
|
||||
|
||||
raise ValueError(f"No building progression feature mapped to {str(building_option.value)}")
|
||||
|
||||
|
||||
skill_progression_by_option = {
|
||||
options.SkillProgression.option_vanilla: skill_progression.SkillProgressionVanilla(),
|
||||
options.SkillProgression.option_progressive: skill_progression.SkillProgressionProgressive(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from . import booksanity
|
||||
from . import building_progression
|
||||
from . import cropsanity
|
||||
from . import fishsanity
|
||||
from . import friendsanity
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
from abc import ABC
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar, Set, Tuple
|
||||
|
||||
from ...strings.building_names import Building
|
||||
|
||||
progressive_house = "Progressive House"
|
||||
|
||||
# This assumes that the farm house is always available, which might not be true forever...
|
||||
progressive_house_by_upgrade_name = {
|
||||
Building.farm_house: 0,
|
||||
Building.kitchen: 1,
|
||||
Building.kids_room: 2,
|
||||
Building.cellar: 3
|
||||
}
|
||||
|
||||
|
||||
def to_progressive_item(building: str) -> Tuple[str, int]:
|
||||
"""Return the name of the progressive item and its quantity required to unlock the building.
|
||||
"""
|
||||
if building in [Building.coop, Building.barn, Building.shed]:
|
||||
return f"Progressive {building}", 1
|
||||
elif building.startswith("Big"):
|
||||
return f"Progressive {building[building.index(' ') + 1:]}", 2
|
||||
elif building.startswith("Deluxe"):
|
||||
return f"Progressive {building[building.index(' ') + 1:]}", 3
|
||||
elif building in progressive_house_by_upgrade_name:
|
||||
return progressive_house, progressive_house_by_upgrade_name[building]
|
||||
|
||||
return building, 1
|
||||
|
||||
|
||||
def to_location_name(building: str) -> str:
|
||||
return f"{building} Blueprint"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BuildingProgressionFeature(ABC):
|
||||
is_progressive: ClassVar[bool]
|
||||
starting_buildings: Set[str]
|
||||
|
||||
to_progressive_item = staticmethod(to_progressive_item)
|
||||
progressive_house = progressive_house
|
||||
|
||||
to_location_name = staticmethod(to_location_name)
|
||||
|
||||
|
||||
class BuildingProgressionVanilla(BuildingProgressionFeature):
|
||||
is_progressive = False
|
||||
|
||||
|
||||
class BuildingProgressionProgressive(BuildingProgressionFeature):
|
||||
is_progressive = True
|
||||
@@ -3,9 +3,10 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Iterable, Set, Any, Mapping, Type, Tuple, Union
|
||||
|
||||
from .feature import booksanity, cropsanity, fishsanity, friendsanity, skill_progression, tool_progression
|
||||
from .feature import booksanity, cropsanity, fishsanity, friendsanity, skill_progression, building_progression, tool_progression
|
||||
from ..data.building import Building
|
||||
from ..data.fish_data import FishItem
|
||||
from ..data.game_item import GameItem, ItemSource, ItemTag
|
||||
from ..data.game_item import GameItem, Source, ItemTag
|
||||
from ..data.skill import Skill
|
||||
from ..data.villagers_data import Villager
|
||||
|
||||
@@ -20,16 +21,17 @@ class StardewContent:
|
||||
game_items: Dict[str, GameItem] = field(default_factory=dict)
|
||||
fishes: Dict[str, FishItem] = field(default_factory=dict)
|
||||
villagers: Dict[str, Villager] = field(default_factory=dict)
|
||||
farm_buildings: Dict[str, Building] = field(default_factory=dict)
|
||||
skills: Dict[str, Skill] = field(default_factory=dict)
|
||||
quests: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def find_sources_of_type(self, types: Union[Type[ItemSource], Tuple[Type[ItemSource]]]) -> Iterable[ItemSource]:
|
||||
def find_sources_of_type(self, types: Union[Type[Source], Tuple[Type[Source]]]) -> Iterable[Source]:
|
||||
for item in self.game_items.values():
|
||||
for source in item.sources:
|
||||
if isinstance(source, types):
|
||||
yield source
|
||||
|
||||
def source_item(self, item_name: str, *sources: ItemSource):
|
||||
def source_item(self, item_name: str, *sources: Source):
|
||||
item = self.game_items.setdefault(item_name, GameItem(item_name))
|
||||
item.add_sources(sources)
|
||||
|
||||
@@ -50,6 +52,7 @@ class StardewContent:
|
||||
@dataclass(frozen=True)
|
||||
class StardewFeatures:
|
||||
booksanity: booksanity.BooksanityFeature
|
||||
building_progression: building_progression.BuildingProgressionFeature
|
||||
cropsanity: cropsanity.CropsanityFeature
|
||||
fishsanity: fishsanity.FishsanityFeature
|
||||
friendsanity: friendsanity.FriendsanityFeature
|
||||
@@ -70,13 +73,13 @@ class ContentPack:
|
||||
# def item_hook
|
||||
# ...
|
||||
|
||||
harvest_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict)
|
||||
harvest_sources: Mapping[str, Iterable[Source]] = field(default_factory=dict)
|
||||
"""Harvest sources contains both crops and forageables, but also fruits from trees, the cave farm and stuff harvested from tapping like maple syrup."""
|
||||
|
||||
def harvest_source_hook(self, content: StardewContent):
|
||||
...
|
||||
|
||||
shop_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict)
|
||||
shop_sources: Mapping[str, Iterable[Source]] = field(default_factory=dict)
|
||||
|
||||
def shop_source_hook(self, content: StardewContent):
|
||||
...
|
||||
@@ -86,12 +89,12 @@ class ContentPack:
|
||||
def fish_hook(self, content: StardewContent):
|
||||
...
|
||||
|
||||
crafting_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict)
|
||||
crafting_sources: Mapping[str, Iterable[Source]] = field(default_factory=dict)
|
||||
|
||||
def crafting_hook(self, content: StardewContent):
|
||||
...
|
||||
|
||||
artisan_good_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict)
|
||||
artisan_good_sources: Mapping[str, Iterable[Source]] = field(default_factory=dict)
|
||||
|
||||
def artisan_good_hook(self, content: StardewContent):
|
||||
...
|
||||
@@ -101,6 +104,11 @@ class ContentPack:
|
||||
def villager_hook(self, content: StardewContent):
|
||||
...
|
||||
|
||||
farm_buildings: Iterable[Building] = ()
|
||||
|
||||
def farm_building_hook(self, content: StardewContent):
|
||||
...
|
||||
|
||||
skills: Iterable[Skill] = ()
|
||||
|
||||
def skill_hook(self, content: StardewContent):
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
from ..game_content import ContentPack
|
||||
from ..mod_registry import register_mod_content_pack
|
||||
from ...data.building import Building
|
||||
from ...data.shop import ShopSource
|
||||
from ...mods.mod_data import ModNames
|
||||
from ...strings.artisan_good_names import ArtisanGood
|
||||
from ...strings.building_names import ModBuilding
|
||||
from ...strings.metal_names import MetalBar
|
||||
from ...strings.region_names import Region
|
||||
|
||||
register_mod_content_pack(ContentPack(
|
||||
ModNames.tractor,
|
||||
farm_buildings=(
|
||||
Building(
|
||||
ModBuilding.tractor_garage,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=150_000,
|
||||
items_price=((20, MetalBar.iron), (5, MetalBar.iridium), (1, ArtisanGood.battery_pack)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Iterable, Mapping, Callable
|
||||
|
||||
from .game_content import StardewContent, ContentPack, StardewFeatures
|
||||
from .vanilla.base import base_game as base_game_content_pack
|
||||
from ..data.game_item import GameItem, ItemSource
|
||||
from ..data.game_item import GameItem, Source
|
||||
|
||||
|
||||
def unpack_content(features: StardewFeatures, packs: Iterable[ContentPack]) -> StardewContent:
|
||||
@@ -61,6 +61,10 @@ def register_pack(content: StardewContent, pack: ContentPack):
|
||||
content.villagers[villager.name] = villager
|
||||
pack.villager_hook(content)
|
||||
|
||||
for building in pack.farm_buildings:
|
||||
content.farm_buildings[building.name] = building
|
||||
pack.farm_building_hook(content)
|
||||
|
||||
for skill in pack.skills:
|
||||
content.skills[skill.name] = skill
|
||||
pack.skill_hook(content)
|
||||
@@ -73,7 +77,7 @@ def register_pack(content: StardewContent, pack: ContentPack):
|
||||
|
||||
|
||||
def register_sources_and_call_hook(content: StardewContent,
|
||||
sources_by_item_name: Mapping[str, Iterable[ItemSource]],
|
||||
sources_by_item_name: Mapping[str, Iterable[Source]],
|
||||
hook: Callable[[StardewContent], None]):
|
||||
for item_name, sources in sources_by_item_name.items():
|
||||
item = content.game_items.setdefault(item_name, GameItem(item_name))
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from ..game_content import ContentPack
|
||||
from ...data import villagers_data, fish_data
|
||||
from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource, CompoundSource
|
||||
from ...data.building import Building
|
||||
from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource
|
||||
from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource
|
||||
from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement
|
||||
from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
|
||||
from ...strings.artisan_good_names import ArtisanGood
|
||||
from ...strings.book_names import Book
|
||||
from ...strings.building_names import Building as BuildingNames
|
||||
from ...strings.crop_names import Fruit
|
||||
from ...strings.fish_names import WaterItem
|
||||
from ...strings.food_names import Beverage, Meal
|
||||
@@ -12,6 +15,7 @@ from ...strings.forageable_names import Forageable, Mushroom
|
||||
from ...strings.fruit_tree_names import Sapling
|
||||
from ...strings.generic_names import Generic
|
||||
from ...strings.material_names import Material
|
||||
from ...strings.metal_names import MetalBar
|
||||
from ...strings.region_names import Region, LogicRegion
|
||||
from ...strings.season_names import Season
|
||||
from ...strings.seed_names import Seed, TreeSeed
|
||||
@@ -229,10 +233,10 @@ pelican_town = ContentPack(
|
||||
ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
|
||||
Book.mapping_cave_systems: (
|
||||
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
|
||||
CompoundSource(sources=(
|
||||
GenericSource(regions=(Region.adventurer_guild_bedroom,)),
|
||||
ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),
|
||||
))),
|
||||
GenericSource(regions=(Region.adventurer_guild_bedroom,)),
|
||||
# Disabling the shop source for better game design.
|
||||
# ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),
|
||||
),
|
||||
Book.monster_compendium: (
|
||||
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
|
||||
CustomRuleSource(create_rule=lambda logic: logic.monster.can_kill_many(Generic.any)),
|
||||
@@ -385,5 +389,204 @@ pelican_town = ContentPack(
|
||||
villagers_data.vincent,
|
||||
villagers_data.willy,
|
||||
villagers_data.wizard,
|
||||
),
|
||||
farm_buildings=(
|
||||
Building(
|
||||
BuildingNames.barn,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=6000,
|
||||
items_price=((350, Material.wood), (150, Material.stone))
|
||||
),
|
||||
),
|
||||
),
|
||||
Building(
|
||||
BuildingNames.big_barn,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=12_000,
|
||||
items_price=((450, Material.wood), (200, Material.stone))
|
||||
),
|
||||
),
|
||||
upgrade_from=BuildingNames.barn,
|
||||
),
|
||||
Building(
|
||||
BuildingNames.deluxe_barn,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=25_000,
|
||||
items_price=((550, Material.wood), (300, Material.stone))
|
||||
),
|
||||
),
|
||||
upgrade_from=BuildingNames.big_barn,
|
||||
),
|
||||
Building(
|
||||
BuildingNames.coop,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=4000,
|
||||
items_price=((300, Material.wood), (100, Material.stone))
|
||||
),
|
||||
),
|
||||
),
|
||||
Building(
|
||||
BuildingNames.big_coop,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=10_000,
|
||||
items_price=((400, Material.wood), (150, Material.stone))
|
||||
),
|
||||
),
|
||||
upgrade_from=BuildingNames.coop,
|
||||
),
|
||||
Building(
|
||||
BuildingNames.deluxe_coop,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=20_000,
|
||||
items_price=((500, Material.wood), (200, Material.stone))
|
||||
),
|
||||
),
|
||||
upgrade_from=BuildingNames.big_coop,
|
||||
),
|
||||
Building(
|
||||
BuildingNames.fish_pond,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=5000,
|
||||
items_price=((200, Material.stone), (5, WaterItem.seaweed), (5, WaterItem.green_algae))
|
||||
),
|
||||
),
|
||||
),
|
||||
Building(
|
||||
BuildingNames.mill,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=2500,
|
||||
items_price=((50, Material.stone), (150, Material.wood), (4, ArtisanGood.cloth))
|
||||
),
|
||||
),
|
||||
),
|
||||
Building(
|
||||
BuildingNames.shed,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=15_000,
|
||||
items_price=((300, Material.wood),)
|
||||
),
|
||||
),
|
||||
),
|
||||
Building(
|
||||
BuildingNames.big_shed,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=20_000,
|
||||
items_price=((550, Material.wood), (300, Material.stone))
|
||||
),
|
||||
),
|
||||
upgrade_from=BuildingNames.shed,
|
||||
),
|
||||
Building(
|
||||
BuildingNames.silo,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=100,
|
||||
items_price=((100, Material.stone), (10, Material.clay), (5, MetalBar.copper))
|
||||
),
|
||||
),
|
||||
),
|
||||
Building(
|
||||
BuildingNames.slime_hutch,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=10_000,
|
||||
items_price=((500, Material.stone), (10, MetalBar.quartz), (1, MetalBar.iridium))
|
||||
),
|
||||
),
|
||||
),
|
||||
Building(
|
||||
BuildingNames.stable,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=10_000,
|
||||
items_price=((100, Material.hardwood), (5, MetalBar.iron))
|
||||
),
|
||||
),
|
||||
),
|
||||
Building(
|
||||
BuildingNames.well,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=1000,
|
||||
items_price=((75, Material.stone),)
|
||||
),
|
||||
),
|
||||
),
|
||||
Building(
|
||||
BuildingNames.shipping_bin,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=250,
|
||||
items_price=((150, Material.wood),)
|
||||
),
|
||||
),
|
||||
),
|
||||
Building(
|
||||
BuildingNames.pet_bowl,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=5000,
|
||||
items_price=((25, Material.hardwood),)
|
||||
),
|
||||
),
|
||||
),
|
||||
Building(
|
||||
BuildingNames.kitchen,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=10_000,
|
||||
items_price=((450, Material.wood),)
|
||||
),
|
||||
),
|
||||
upgrade_from=BuildingNames.farm_house,
|
||||
),
|
||||
Building(
|
||||
BuildingNames.kids_room,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=65_000,
|
||||
items_price=((100, Material.hardwood),)
|
||||
),
|
||||
),
|
||||
upgrade_from=BuildingNames.kitchen,
|
||||
),
|
||||
Building(
|
||||
BuildingNames.cellar,
|
||||
sources=(
|
||||
ShopSource(
|
||||
shop_region=Region.carpenter,
|
||||
money_price=100_000,
|
||||
),
|
||||
),
|
||||
upgrade_from=BuildingNames.kids_room,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .game_item import ItemSource
|
||||
from .game_item import Source
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MachineSource(ItemSource):
|
||||
class MachineSource(Source):
|
||||
item: str # this should be optional (worm bin)
|
||||
machine: str
|
||||
# seasons
|
||||
|
||||
16
worlds/stardew_valley/data/building.py
Normal file
16
worlds/stardew_valley/data/building.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from dataclasses import dataclass, field
|
||||
from functools import cached_property
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from .game_item import Source
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Building:
|
||||
name: str
|
||||
sources: Tuple[Source, ...] = field(kw_only=True)
|
||||
upgrade_from: Optional[str] = field(default=None, kw_only=True)
|
||||
|
||||
@cached_property
|
||||
def is_upgrade(self) -> bool:
|
||||
return self.upgrade_from is not None
|
||||
@@ -27,7 +27,7 @@ class ItemTag(enum.Enum):
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ItemSource(ABC):
|
||||
class Source(ABC):
|
||||
add_tags: ClassVar[Tuple[ItemTag]] = ()
|
||||
|
||||
other_requirements: Tuple[Requirement, ...] = field(kw_only=True, default_factory=tuple)
|
||||
@@ -38,23 +38,18 @@ class ItemSource(ABC):
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GenericSource(ItemSource):
|
||||
class GenericSource(Source):
|
||||
regions: Tuple[str, ...] = ()
|
||||
"""No region means it's available everywhere."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CustomRuleSource(ItemSource):
|
||||
class CustomRuleSource(Source):
|
||||
"""Hopefully once everything is migrated to sources, we won't need these custom logic anymore."""
|
||||
create_rule: Callable[[Any], StardewRule]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class CompoundSource(ItemSource):
|
||||
sources: Tuple[ItemSource, ...] = ()
|
||||
|
||||
|
||||
class Tag(ItemSource):
|
||||
class Tag(Source):
|
||||
"""Not a real source, just a way to add tags to an item. Will be removed from the item sources during unpacking."""
|
||||
tag: Tuple[ItemTag, ...]
|
||||
|
||||
@@ -69,10 +64,10 @@ class Tag(ItemSource):
|
||||
@dataclass(frozen=True)
|
||||
class GameItem:
|
||||
name: str
|
||||
sources: List[ItemSource] = field(default_factory=list)
|
||||
sources: List[Source] = field(default_factory=list)
|
||||
tags: Set[ItemTag] = field(default_factory=set)
|
||||
|
||||
def add_sources(self, sources: Iterable[ItemSource]):
|
||||
def add_sources(self, sources: Iterable[Source]):
|
||||
self.sources.extend(source for source in sources if type(source) is not Tag)
|
||||
for source in sources:
|
||||
self.add_tags(source.add_tags)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple, Sequence, Mapping
|
||||
|
||||
from .game_item import ItemSource, ItemTag
|
||||
from .game_item import Source, ItemTag
|
||||
from ..strings.season_names import Season
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ForagingSource(ItemSource):
|
||||
class ForagingSource(Source):
|
||||
regions: Tuple[str, ...]
|
||||
seasons: Tuple[str, ...] = Season.all
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SeasonalForagingSource(ItemSource):
|
||||
class SeasonalForagingSource(Source):
|
||||
season: str
|
||||
days: Sequence[int]
|
||||
regions: Tuple[str, ...]
|
||||
@@ -22,17 +22,17 @@ class SeasonalForagingSource(ItemSource):
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FruitBatsSource(ItemSource):
|
||||
class FruitBatsSource(Source):
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MushroomCaveSource(ItemSource):
|
||||
class MushroomCaveSource(Source):
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HarvestFruitTreeSource(ItemSource):
|
||||
class HarvestFruitTreeSource(Source):
|
||||
add_tags = (ItemTag.CROPSANITY,)
|
||||
|
||||
sapling: str
|
||||
@@ -46,7 +46,7 @@ class HarvestFruitTreeSource(ItemSource):
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HarvestCropSource(ItemSource):
|
||||
class HarvestCropSource(Source):
|
||||
add_tags = (ItemTag.CROPSANITY,)
|
||||
|
||||
seed: str
|
||||
@@ -61,5 +61,5 @@ class HarvestCropSource(ItemSource):
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ArtifactSpotSource(ItemSource):
|
||||
class ArtifactSpotSource(Source):
|
||||
amount: int
|
||||
|
||||
@@ -509,6 +509,7 @@ id,name,classification,groups,mod_name
|
||||
561,Fishing Bar Size Bonus,filler,PLAYER_BUFF,
|
||||
562,Quality Bonus,filler,PLAYER_BUFF,
|
||||
563,Glow Bonus,filler,PLAYER_BUFF,
|
||||
564,Pet Bowl,progression,BUILDING,
|
||||
4001,Burnt Trap,trap,TRAP,
|
||||
4002,Darkness Trap,trap,TRAP,
|
||||
4003,Frozen Trap,trap,TRAP,
|
||||
|
||||
|
@@ -21,6 +21,11 @@ class SkillRequirement(Requirement):
|
||||
level: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RegionRequirement(Requirement):
|
||||
region: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SeasonRequirement(Requirement):
|
||||
season: str
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple, Optional
|
||||
|
||||
from .game_item import ItemSource
|
||||
from .game_item import Source
|
||||
from ..strings.season_names import Season
|
||||
|
||||
ItemPrice = Tuple[int, str]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ShopSource(ItemSource):
|
||||
class ShopSource(Source):
|
||||
shop_region: str
|
||||
money_price: Optional[int] = None
|
||||
items_price: Optional[Tuple[ItemPrice, ...]] = None
|
||||
@@ -20,20 +20,20 @@ class ShopSource(ItemSource):
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MysteryBoxSource(ItemSource):
|
||||
class MysteryBoxSource(Source):
|
||||
amount: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ArtifactTroveSource(ItemSource):
|
||||
class ArtifactTroveSource(Source):
|
||||
amount: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class PrizeMachineSource(ItemSource):
|
||||
class PrizeMachineSource(Source):
|
||||
amount: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FishingTreasureChestSource(ItemSource):
|
||||
class FishingTreasureChestSource(Source):
|
||||
amount: int
|
||||
|
||||
@@ -23,9 +23,9 @@ def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions,
|
||||
|
||||
add_seasonal_candidates(early_candidates, options)
|
||||
|
||||
if options.building_progression & stardew_options.BuildingProgression.option_progressive:
|
||||
if content.features.building_progression.is_progressive:
|
||||
early_forced.append(Building.shipping_bin)
|
||||
if options.farm_type != stardew_options.FarmType.option_meadowlands:
|
||||
if Building.coop not in content.features.building_progression.starting_buildings:
|
||||
early_candidates.append("Progressive Coop")
|
||||
early_candidates.append("Progressive Barn")
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from .data.game_item import ItemTag
|
||||
from .logic.logic_event import all_events
|
||||
from .mods.mod_data import ModNames
|
||||
from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \
|
||||
BuildingProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \
|
||||
ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \
|
||||
Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs
|
||||
from .strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName
|
||||
from .strings.ap_names.ap_weapon_names import APWeapon
|
||||
@@ -225,7 +225,7 @@ def create_unique_items(item_factory: StardewItemFactory, options: StardewValley
|
||||
create_tools(item_factory, content, items)
|
||||
create_skills(item_factory, content, items)
|
||||
create_wizard_buildings(item_factory, options, items)
|
||||
create_carpenter_buildings(item_factory, options, items)
|
||||
create_carpenter_buildings(item_factory, content, items)
|
||||
items.append(item_factory("Railroad Boulder Removed"))
|
||||
items.append(item_factory(CommunityUpgrade.fruit_bats))
|
||||
items.append(item_factory(CommunityUpgrade.mushroom_boxes))
|
||||
@@ -353,30 +353,14 @@ def create_wizard_buildings(item_factory: StardewItemFactory, options: StardewVa
|
||||
items.append(item_factory("Woods Obelisk"))
|
||||
|
||||
|
||||
def create_carpenter_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
|
||||
building_option = options.building_progression
|
||||
if not building_option & BuildingProgression.option_progressive:
|
||||
def create_carpenter_buildings(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]):
|
||||
building_progression = content.features.building_progression
|
||||
if not building_progression.is_progressive:
|
||||
return
|
||||
items.append(item_factory("Progressive Coop"))
|
||||
items.append(item_factory("Progressive Coop"))
|
||||
items.append(item_factory("Progressive Coop"))
|
||||
items.append(item_factory("Progressive Barn"))
|
||||
items.append(item_factory("Progressive Barn"))
|
||||
items.append(item_factory("Progressive Barn"))
|
||||
items.append(item_factory("Well"))
|
||||
items.append(item_factory("Silo"))
|
||||
items.append(item_factory("Mill"))
|
||||
items.append(item_factory("Progressive Shed"))
|
||||
items.append(item_factory("Progressive Shed", ItemClassification.useful))
|
||||
items.append(item_factory("Fish Pond"))
|
||||
items.append(item_factory("Stable"))
|
||||
items.append(item_factory("Slime Hutch"))
|
||||
items.append(item_factory("Shipping Bin"))
|
||||
items.append(item_factory("Progressive House"))
|
||||
items.append(item_factory("Progressive House"))
|
||||
items.append(item_factory("Progressive House"))
|
||||
if ModNames.tractor in options.mods:
|
||||
items.append(item_factory("Tractor Garage"))
|
||||
|
||||
for building in content.farm_buildings.values():
|
||||
item_name, _ = building_progression.to_progressive_item(building.name)
|
||||
items.append(item_factory(item_name))
|
||||
|
||||
|
||||
def create_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
|
||||
|
||||
@@ -11,7 +11,7 @@ from .data.game_item import ItemTag
|
||||
from .data.museum_data import all_museum_items
|
||||
from .mods.mod_data import ModNames
|
||||
from .options import ExcludeGingerIsland, ArcadeMachineLocations, SpecialOrderLocations, Museumsanity, \
|
||||
FestivalLocations, BuildingProgression, ElevatorProgression, BackpackProgression, FarmType
|
||||
FestivalLocations, ElevatorProgression, BackpackProgression, FarmType
|
||||
from .options import StardewValleyOptions, Craftsanity, Chefsanity, Cooksanity, Shipsanity, Monstersanity
|
||||
from .strings.goal_names import Goal
|
||||
from .strings.quest_names import ModQuest, Quest
|
||||
@@ -261,6 +261,19 @@ def extend_baby_locations(randomized_locations: List[LocationData]):
|
||||
randomized_locations.extend(baby_locations)
|
||||
|
||||
|
||||
def extend_building_locations(randomized_locations: List[LocationData], content: StardewContent):
|
||||
building_progression = content.features.building_progression
|
||||
if not building_progression.is_progressive:
|
||||
return
|
||||
|
||||
for building in content.farm_buildings.values():
|
||||
if building.name in building_progression.starting_buildings:
|
||||
continue
|
||||
|
||||
location_name = building_progression.to_location_name(building.name)
|
||||
randomized_locations.append(location_table[location_name])
|
||||
|
||||
|
||||
def extend_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random):
|
||||
if options.festival_locations == FestivalLocations.option_disabled:
|
||||
return
|
||||
@@ -485,10 +498,7 @@ def create_locations(location_collector: StardewLocationCollector,
|
||||
if skill_progression.is_mastery_randomized(skill):
|
||||
randomized_locations.append(location_table[skill.mastery_name])
|
||||
|
||||
if options.building_progression & BuildingProgression.option_progressive:
|
||||
for location in locations_by_tag[LocationTags.BUILDING_BLUEPRINT]:
|
||||
if location.mod_name is None or location.mod_name in options.mods:
|
||||
randomized_locations.append(location_table[location.name])
|
||||
extend_building_locations(randomized_locations, content)
|
||||
|
||||
if options.arcade_machine_locations != ArcadeMachineLocations.option_disabled:
|
||||
randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE_VICTORY])
|
||||
|
||||
@@ -20,7 +20,6 @@ class LogicRegistry:
|
||||
self.museum_rules: Dict[str, StardewRule] = {}
|
||||
self.festival_rules: Dict[str, StardewRule] = {}
|
||||
self.quest_rules: Dict[str, StardewRule] = {}
|
||||
self.building_rules: Dict[str, StardewRule] = {}
|
||||
self.special_order_rules: Dict[str, StardewRule] = {}
|
||||
|
||||
self.sve_location_rules: Dict[str, StardewRule] = {}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import typing
|
||||
from functools import cached_property
|
||||
from typing import Dict, Union
|
||||
from typing import Union
|
||||
|
||||
from Utils import cache_self1
|
||||
from .base_logic import BaseLogic, BaseLogicMixin
|
||||
from .has_logic import HasLogicMixin
|
||||
from .money_logic import MoneyLogicMixin
|
||||
from .received_logic import ReceivedLogicMixin
|
||||
from .region_logic import RegionLogicMixin
|
||||
from ..options import BuildingProgression
|
||||
from ..stardew_rule import StardewRule, True_, False_, Has
|
||||
from ..strings.artisan_good_names import ArtisanGood
|
||||
from ..stardew_rule import StardewRule, true_
|
||||
from ..strings.building_names import Building
|
||||
from ..strings.fish_names import WaterItem
|
||||
from ..strings.material_names import Material
|
||||
from ..strings.metal_names import MetalBar
|
||||
from ..strings.region_names import Region
|
||||
|
||||
has_group = "building"
|
||||
if typing.TYPE_CHECKING:
|
||||
from .source_logic import SourceLogicMixin
|
||||
else:
|
||||
SourceLogicMixin = object
|
||||
|
||||
AUTO_BUILDING_BUILDINGS = {Building.shipping_bin, Building.pet_bowl, Building.farm_house}
|
||||
|
||||
|
||||
class BuildingLogicMixin(BaseLogicMixin):
|
||||
@@ -25,78 +25,38 @@ class BuildingLogicMixin(BaseLogicMixin):
|
||||
self.building = BuildingLogic(*args, **kwargs)
|
||||
|
||||
|
||||
class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, MoneyLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin]]):
|
||||
def initialize_rules(self):
|
||||
self.registry.building_rules.update({
|
||||
# @formatter:off
|
||||
Building.barn: self.logic.money.can_spend(6000) & self.logic.has_all(Material.wood, Material.stone),
|
||||
Building.big_barn: self.logic.money.can_spend(12000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.barn),
|
||||
Building.deluxe_barn: self.logic.money.can_spend(25000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.big_barn),
|
||||
Building.coop: self.logic.money.can_spend(4000) & self.logic.has_all(Material.wood, Material.stone),
|
||||
Building.big_coop: self.logic.money.can_spend(10000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.coop),
|
||||
Building.deluxe_coop: self.logic.money.can_spend(20000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.big_coop),
|
||||
Building.fish_pond: self.logic.money.can_spend(5000) & self.logic.has_all(Material.stone, WaterItem.seaweed, WaterItem.green_algae),
|
||||
Building.mill: self.logic.money.can_spend(2500) & self.logic.has_all(Material.stone, Material.wood, ArtisanGood.cloth),
|
||||
Building.shed: self.logic.money.can_spend(15000) & self.logic.has(Material.wood),
|
||||
Building.big_shed: self.logic.money.can_spend(20000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.shed),
|
||||
Building.silo: self.logic.money.can_spend(100) & self.logic.has_all(Material.stone, Material.clay, MetalBar.copper),
|
||||
Building.slime_hutch: self.logic.money.can_spend(10000) & self.logic.has_all(Material.stone, MetalBar.quartz, MetalBar.iridium),
|
||||
Building.stable: self.logic.money.can_spend(10000) & self.logic.has_all(Material.hardwood, MetalBar.iron),
|
||||
Building.well: self.logic.money.can_spend(1000) & self.logic.has(Material.stone),
|
||||
Building.shipping_bin: self.logic.money.can_spend(250) & self.logic.has(Material.wood),
|
||||
Building.kitchen: self.logic.money.can_spend(10000) & self.logic.has(Material.wood) & self.logic.building.has_house(0),
|
||||
Building.kids_room: self.logic.money.can_spend(65000) & self.logic.has(Material.hardwood) & self.logic.building.has_house(1),
|
||||
Building.cellar: self.logic.money.can_spend(100000) & self.logic.building.has_house(2),
|
||||
# @formatter:on
|
||||
})
|
||||
|
||||
def update_rules(self, new_rules: Dict[str, StardewRule]):
|
||||
self.registry.building_rules.update(new_rules)
|
||||
class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SourceLogicMixin]]):
|
||||
|
||||
@cache_self1
|
||||
def has_building(self, building: str) -> StardewRule:
|
||||
# Shipping bin is special. The mod auto-builds it when received, no need to go to Robin.
|
||||
if building is Building.shipping_bin:
|
||||
if not self.options.building_progression & BuildingProgression.option_progressive:
|
||||
return True_()
|
||||
return self.logic.received(building)
|
||||
def can_build(self, building_name: str) -> StardewRule:
|
||||
building = self.content.farm_buildings.get(building_name)
|
||||
assert building is not None, f"Building {building_name} not found."
|
||||
|
||||
source_rule = self.logic.source.has_access_to_any(building.sources)
|
||||
if not building.is_upgrade:
|
||||
return source_rule
|
||||
|
||||
upgrade_rule = self.logic.building.has_building(building.upgrade_from)
|
||||
return self.logic.and_(upgrade_rule, source_rule)
|
||||
|
||||
@cache_self1
|
||||
def has_building(self, building_name: str) -> StardewRule:
|
||||
building_progression = self.content.features.building_progression
|
||||
|
||||
if building_name in building_progression.starting_buildings:
|
||||
return true_
|
||||
|
||||
if not building_progression.is_progressive:
|
||||
return self.logic.building.can_build(building_name)
|
||||
|
||||
# Those buildings are special. The mod auto-builds them when received, no need to go to Robin.
|
||||
if building_name in AUTO_BUILDING_BUILDINGS:
|
||||
return self.logic.received(Building.shipping_bin)
|
||||
|
||||
carpenter_rule = self.logic.building.can_construct_buildings
|
||||
if not self.options.building_progression & BuildingProgression.option_progressive:
|
||||
return Has(building, self.registry.building_rules, has_group) & carpenter_rule
|
||||
|
||||
count = 1
|
||||
if building in [Building.coop, Building.barn, Building.shed]:
|
||||
building = f"Progressive {building}"
|
||||
elif building.startswith("Big"):
|
||||
count = 2
|
||||
building = " ".join(["Progressive", *building.split(" ")[1:]])
|
||||
elif building.startswith("Deluxe"):
|
||||
count = 3
|
||||
building = " ".join(["Progressive", *building.split(" ")[1:]])
|
||||
return self.logic.received(building, count) & carpenter_rule
|
||||
item, count = building_progression.to_progressive_item(building_name)
|
||||
return self.logic.received(item, count) & carpenter_rule
|
||||
|
||||
@cached_property
|
||||
def can_construct_buildings(self) -> StardewRule:
|
||||
return self.logic.region.can_reach(Region.carpenter)
|
||||
|
||||
@cache_self1
|
||||
def has_house(self, upgrade_level: int) -> StardewRule:
|
||||
if upgrade_level < 1:
|
||||
return True_()
|
||||
|
||||
if upgrade_level > 3:
|
||||
return False_()
|
||||
|
||||
carpenter_rule = self.logic.building.can_construct_buildings
|
||||
if self.options.building_progression & BuildingProgression.option_progressive:
|
||||
return carpenter_rule & self.logic.received(f"Progressive House", upgrade_level)
|
||||
|
||||
if upgrade_level == 1:
|
||||
return carpenter_rule & Has(Building.kitchen, self.registry.building_rules, has_group)
|
||||
|
||||
if upgrade_level == 2:
|
||||
return carpenter_rule & Has(Building.kids_room, self.registry.building_rules, has_group)
|
||||
|
||||
# if upgrade_level == 3:
|
||||
return carpenter_rule & Has(Building.cellar, self.registry.building_rules, has_group)
|
||||
|
||||
@@ -17,6 +17,7 @@ from ..data.recipe_data import RecipeSource, StarterSource, ShopSource, SkillSou
|
||||
from ..data.recipe_source import CutsceneSource, ShopTradeSource
|
||||
from ..options import Chefsanity
|
||||
from ..stardew_rule import StardewRule, True_, False_
|
||||
from ..strings.building_names import Building
|
||||
from ..strings.region_names import LogicRegion
|
||||
from ..strings.skill_names import Skill
|
||||
from ..strings.tv_channel_names import Channel
|
||||
@@ -32,7 +33,7 @@ class CookingLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogi
|
||||
BuildingLogicMixin, RelationshipLogicMixin, SkillLogicMixin, CookingLogicMixin]]):
|
||||
@cached_property
|
||||
def can_cook_in_kitchen(self) -> StardewRule:
|
||||
return self.logic.building.has_house(1) | self.logic.skill.has_level(Skill.foraging, 9)
|
||||
return self.logic.building.has_building(Building.kitchen) | self.logic.skill.has_level(Skill.foraging, 9)
|
||||
|
||||
# Should be cached
|
||||
def can_cook(self, recipe: CookingRecipe = None) -> StardewRule:
|
||||
|
||||
@@ -44,7 +44,7 @@ class GoalLogic(BaseLogic[StardewLogic]):
|
||||
self.logic.museum.can_complete_museum(),
|
||||
# Catching every fish not expected
|
||||
# Shipping every item not expected
|
||||
self.logic.relationship.can_get_married() & self.logic.building.has_house(2),
|
||||
self.logic.relationship.can_get_married() & self.logic.building.has_building(Building.kids_room),
|
||||
self.logic.relationship.has_hearts_with_n(5, 8), # 5 Friends
|
||||
self.logic.relationship.has_hearts_with_n(10, 8), # 10 friends
|
||||
self.logic.pet.has_pet_hearts(5), # Max Pet
|
||||
|
||||
@@ -13,6 +13,7 @@ from ..strings.craftable_names import Consumable
|
||||
from ..strings.currency_names import Currency
|
||||
from ..strings.fish_names import WaterChest
|
||||
from ..strings.geode_names import Geode
|
||||
from ..strings.material_names import Material
|
||||
from ..strings.region_names import Region
|
||||
from ..strings.tool_names import Tool
|
||||
|
||||
@@ -21,9 +22,14 @@ if TYPE_CHECKING:
|
||||
else:
|
||||
ToolLogicMixin = object
|
||||
|
||||
MIN_ITEMS = 10
|
||||
MAX_ITEMS = 999
|
||||
PERCENT_REQUIRED_FOR_MAX_ITEM = 24
|
||||
MIN_MEDIUM_ITEMS = 10
|
||||
MAX_MEDIUM_ITEMS = 999
|
||||
PERCENT_REQUIRED_FOR_MAX_MEDIUM_ITEM = 24
|
||||
|
||||
EASY_ITEMS = {Material.wood, Material.stone, Material.fiber, Material.sap}
|
||||
MIN_EASY_ITEMS = 300
|
||||
MAX_EASY_ITEMS = 2997
|
||||
PERCENT_REQUIRED_FOR_MAX_EASY_ITEM = 6
|
||||
|
||||
|
||||
class GrindLogicMixin(BaseLogicMixin):
|
||||
@@ -43,7 +49,7 @@ class GrindLogic(BaseLogic[Union[GrindLogicMixin, HasLogicMixin, ReceivedLogicMi
|
||||
# Assuming one box per day, but halved because we don't know how many months have passed before Mr. Qi's Plane Ride.
|
||||
time_rule = self.logic.time.has_lived_months(quantity // 14)
|
||||
return self.logic.and_(opening_rule, mystery_box_rule,
|
||||
book_of_mysteries_rule, time_rule,)
|
||||
book_of_mysteries_rule, time_rule, )
|
||||
|
||||
def can_grind_artifact_troves(self, quantity: int) -> StardewRule:
|
||||
opening_rule = self.logic.region.can_reach(Region.blacksmith)
|
||||
@@ -67,11 +73,26 @@ class GrindLogic(BaseLogic[Union[GrindLogicMixin, HasLogicMixin, ReceivedLogicMi
|
||||
# Assuming twelve per month if the player does not grind it.
|
||||
self.logic.time.has_lived_months(quantity // 12))
|
||||
|
||||
def can_grind_item(self, quantity: int, item: str | None = None) -> StardewRule:
|
||||
if item in EASY_ITEMS:
|
||||
return self.logic.grind.can_grind_easy_item(quantity)
|
||||
else:
|
||||
return self.logic.grind.can_grind_medium_item(quantity)
|
||||
|
||||
@cache_self1
|
||||
def can_grind_item(self, quantity: int) -> StardewRule:
|
||||
if quantity <= MIN_ITEMS:
|
||||
def can_grind_medium_item(self, quantity: int) -> StardewRule:
|
||||
if quantity <= MIN_MEDIUM_ITEMS:
|
||||
return self.logic.true_
|
||||
|
||||
quantity = min(quantity, MAX_ITEMS)
|
||||
price = max(1, quantity * PERCENT_REQUIRED_FOR_MAX_ITEM // MAX_ITEMS)
|
||||
quantity = min(quantity, MAX_MEDIUM_ITEMS)
|
||||
price = max(1, quantity * PERCENT_REQUIRED_FOR_MAX_MEDIUM_ITEM // MAX_MEDIUM_ITEMS)
|
||||
return HasProgressionPercent(self.player, price)
|
||||
|
||||
@cache_self1
|
||||
def can_grind_easy_item(self, quantity: int) -> StardewRule:
|
||||
if quantity <= MIN_EASY_ITEMS:
|
||||
return self.logic.true_
|
||||
|
||||
quantity = min(quantity, MAX_EASY_ITEMS)
|
||||
price = max(1, quantity * PERCENT_REQUIRED_FOR_MAX_EASY_ITEM // MAX_EASY_ITEMS)
|
||||
return HasProgressionPercent(self.player, price)
|
||||
|
||||
@@ -254,7 +254,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
Geode.omni: self.mine.can_mine_in_the_mines_floor_41_80() | self.region.can_reach(Region.desert) | self.tool.has_tool(Tool.pan, ToolMaterial.iron) | self.received(Wallet.rusty_key) | (self.has(Fish.octopus) & self.building.has_building(Building.fish_pond)) | self.region.can_reach(Region.volcano_floor_10),
|
||||
Gift.bouquet: self.relationship.has_hearts_with_any_bachelor(8) & self.money.can_spend_at(Region.pierre_store, 100),
|
||||
Gift.golden_pumpkin: self.season.has(Season.fall) | self.action.can_open_geode(Geode.artifact_trove),
|
||||
Gift.mermaid_pendant: self.region.can_reach(Region.tide_pools) & self.relationship.has_hearts_with_any_bachelor(10) & self.building.has_house(1) & self.has(Consumable.rain_totem),
|
||||
Gift.mermaid_pendant: self.region.can_reach(Region.tide_pools) & self.relationship.has_hearts_with_any_bachelor(10) & self.building.has_building(Building.kitchen) & self.has(Consumable.rain_totem),
|
||||
Gift.movie_ticket: self.money.can_spend_at(Region.movie_ticket_stand, 1000),
|
||||
Gift.pearl: (self.has(Fish.blobfish) & self.building.has_building(Building.fish_pond)) | self.action.can_open_geode(Geode.artifact_trove),
|
||||
Gift.tea_set: self.season.has(Season.winter) & self.time.has_lived_max_months,
|
||||
@@ -355,9 +355,6 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
obtention_rule = self.registry.item_rules[recipe] if recipe in self.registry.item_rules else False_()
|
||||
self.registry.item_rules[recipe] = obtention_rule | crafting_rule
|
||||
|
||||
self.building.initialize_rules()
|
||||
self.building.update_rules(self.mod.building.get_modded_building_rules())
|
||||
|
||||
self.quest.initialize_rules()
|
||||
self.quest.update_rules(self.mod.quest.get_modded_quest_rules())
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ from ..strings.region_names import Region, LogicRegion
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .shipping_logic import ShippingLogicMixin
|
||||
|
||||
assert ShippingLogicMixin
|
||||
else:
|
||||
ShippingLogicMixin = object
|
||||
|
||||
qi_gem_rewards = ("100 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems", "25 Qi Gems",
|
||||
"20 Qi Gems", "15 Qi Gems", "10 Qi Gems")
|
||||
@@ -31,7 +31,7 @@ class MoneyLogicMixin(BaseLogicMixin):
|
||||
|
||||
|
||||
class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SeasonLogicMixin,
|
||||
GrindLogicMixin, 'ShippingLogicMixin']]):
|
||||
GrindLogicMixin, ShippingLogicMixin]]):
|
||||
|
||||
@cache_self1
|
||||
def can_have_earned_total(self, amount: int) -> StardewRule:
|
||||
@@ -80,7 +80,7 @@ GrindLogicMixin, 'ShippingLogicMixin']]):
|
||||
item_rules = []
|
||||
if source.items_price is not None:
|
||||
for price, item in source.items_price:
|
||||
item_rules.append(self.logic.has(item) & self.logic.grind.can_grind_item(price))
|
||||
item_rules.append(self.logic.has(item) & self.logic.grind.can_grind_item(price, item))
|
||||
|
||||
region_rule = self.logic.region.can_reach(source.shop_region)
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ from ..content.feature import friendsanity
|
||||
from ..data.villagers_data import Villager
|
||||
from ..stardew_rule import StardewRule, True_, false_, true_
|
||||
from ..strings.ap_names.mods.mod_items import SVEQuestItem
|
||||
from ..strings.building_names import Building
|
||||
from ..strings.generic_names import Generic
|
||||
from ..strings.gift_names import Gift
|
||||
from ..strings.quest_names import ModQuest
|
||||
from ..strings.region_names import Region
|
||||
from ..strings.season_names import Season
|
||||
from ..strings.villager_names import NPC, ModNPC
|
||||
@@ -63,7 +63,7 @@ ReceivedLogicMixin, HasLogicMixin, ModLogicMixin]]):
|
||||
if not self.content.features.friendsanity.is_enabled:
|
||||
return self.logic.relationship.can_reproduce(number_children)
|
||||
|
||||
return self.logic.received_n(*possible_kids, count=number_children) & self.logic.building.has_house(2)
|
||||
return self.logic.received_n(*possible_kids, count=number_children) & self.logic.building.has_building(Building.kids_room)
|
||||
|
||||
def can_reproduce(self, number_children: int = 1) -> StardewRule:
|
||||
assert number_children >= 0, "Can't have a negative amount of children."
|
||||
@@ -71,7 +71,7 @@ ReceivedLogicMixin, HasLogicMixin, ModLogicMixin]]):
|
||||
return True_()
|
||||
|
||||
baby_rules = [self.logic.relationship.can_get_married(),
|
||||
self.logic.building.has_house(2),
|
||||
self.logic.building.has_building(Building.kids_room),
|
||||
self.logic.relationship.has_hearts_with_any_bachelor(12),
|
||||
self.logic.relationship.has_children(number_children - 1)]
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from .fishing_logic import FishingLogicMixin
|
||||
from .has_logic import HasLogicMixin
|
||||
from .quest_logic import QuestLogicMixin
|
||||
from .received_logic import ReceivedLogicMixin
|
||||
from .region_logic import RegionLogicMixin
|
||||
from .relationship_logic import RelationshipLogicMixin
|
||||
from .season_logic import SeasonLogicMixin
|
||||
from .skill_logic import SkillLogicMixin
|
||||
@@ -16,7 +17,7 @@ from .tool_logic import ToolLogicMixin
|
||||
from .walnut_logic import WalnutLogicMixin
|
||||
from ..data.game_item import Requirement
|
||||
from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, CombatRequirement, QuestRequirement, \
|
||||
RelationshipRequirement, FishingRequirement, WalnutRequirement
|
||||
RelationshipRequirement, FishingRequirement, WalnutRequirement, RegionRequirement
|
||||
|
||||
|
||||
class RequirementLogicMixin(BaseLogicMixin):
|
||||
@@ -26,7 +27,7 @@ class RequirementLogicMixin(BaseLogicMixin):
|
||||
|
||||
|
||||
class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin,
|
||||
SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, RelationshipLogicMixin, FishingLogicMixin, WalnutLogicMixin]]):
|
||||
SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, RelationshipLogicMixin, FishingLogicMixin, WalnutLogicMixin, RegionLogicMixin]]):
|
||||
|
||||
def meet_all_requirements(self, requirements: Iterable[Requirement]):
|
||||
if not requirements:
|
||||
@@ -45,6 +46,10 @@ SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, Relationshi
|
||||
def _(self, requirement: SkillRequirement):
|
||||
return self.logic.skill.has_level(requirement.skill, requirement.level)
|
||||
|
||||
@meet_requirement.register
|
||||
def _(self, requirement: RegionRequirement):
|
||||
return self.logic.region.can_reach(requirement.region)
|
||||
|
||||
@meet_requirement.register
|
||||
def _(self, requirement: BookRequirement):
|
||||
return self.logic.book.has_book_power(requirement.book)
|
||||
@@ -76,5 +81,3 @@ SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, Relationshi
|
||||
@meet_requirement.register
|
||||
def _(self, requirement: FishingRequirement):
|
||||
return self.logic.fishing.can_fish_at(requirement.region)
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from .region_logic import RegionLogicMixin
|
||||
from .requirement_logic import RequirementLogicMixin
|
||||
from .tool_logic import ToolLogicMixin
|
||||
from ..data.artisan import MachineSource
|
||||
from ..data.game_item import GenericSource, ItemSource, GameItem, CustomRuleSource, CompoundSource
|
||||
from ..data.game_item import GenericSource, Source, GameItem, CustomRuleSource
|
||||
from ..data.harvest import ForagingSource, FruitBatsSource, MushroomCaveSource, SeasonalForagingSource, \
|
||||
HarvestCropSource, HarvestFruitTreeSource, ArtifactSpotSource
|
||||
from ..data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
|
||||
@@ -25,7 +25,7 @@ class SourceLogicMixin(BaseLogicMixin):
|
||||
|
||||
|
||||
class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogicMixin, HarvestingLogicMixin, MoneyLogicMixin, RegionLogicMixin,
|
||||
ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]):
|
||||
ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]):
|
||||
|
||||
def has_access_to_item(self, item: GameItem):
|
||||
rules = []
|
||||
@@ -36,14 +36,10 @@ class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogic
|
||||
rules.append(self.logic.source.has_access_to_any(item.sources))
|
||||
return self.logic.and_(*rules)
|
||||
|
||||
def has_access_to_any(self, sources: Iterable[ItemSource]):
|
||||
def has_access_to_any(self, sources: Iterable[Source]):
|
||||
return self.logic.or_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements)
|
||||
for source in sources))
|
||||
|
||||
def has_access_to_all(self, sources: Iterable[ItemSource]):
|
||||
return self.logic.and_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements)
|
||||
for source in sources))
|
||||
|
||||
@functools.singledispatchmethod
|
||||
def has_access_to(self, source: Any):
|
||||
raise ValueError(f"Sources of type{type(source)} have no rule registered.")
|
||||
@@ -56,10 +52,6 @@ class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogic
|
||||
def _(self, source: CustomRuleSource):
|
||||
return source.create_rule(self.logic)
|
||||
|
||||
@has_access_to.register
|
||||
def _(self, source: CompoundSource):
|
||||
return self.logic.source.has_access_to_all(source.sources)
|
||||
|
||||
@has_access_to.register
|
||||
def _(self, source: ForagingSource):
|
||||
return self.logic.harvesting.can_forage_from(source)
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
from typing import Dict, Union
|
||||
|
||||
from ..mod_data import ModNames
|
||||
from ...logic.base_logic import BaseLogicMixin, BaseLogic
|
||||
from ...logic.has_logic import HasLogicMixin
|
||||
from ...logic.money_logic import MoneyLogicMixin
|
||||
from ...stardew_rule import StardewRule
|
||||
from ...strings.artisan_good_names import ArtisanGood
|
||||
from ...strings.building_names import ModBuilding
|
||||
from ...strings.metal_names import MetalBar
|
||||
from ...strings.region_names import Region
|
||||
|
||||
|
||||
class ModBuildingLogicMixin(BaseLogicMixin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.building = ModBuildingLogic(*args, **kwargs)
|
||||
|
||||
|
||||
class ModBuildingLogic(BaseLogic[Union[MoneyLogicMixin, HasLogicMixin]]):
|
||||
|
||||
def get_modded_building_rules(self) -> Dict[str, StardewRule]:
|
||||
buildings = dict()
|
||||
if ModNames.tractor in self.options.mods:
|
||||
tractor_rule = (self.logic.money.can_spend_at(Region.carpenter, 150000) &
|
||||
self.logic.has_all(MetalBar.iron, MetalBar.iridium, ArtisanGood.battery_pack))
|
||||
buildings.update({ModBuilding.tractor_garage: tractor_rule})
|
||||
return buildings
|
||||
@@ -1,4 +1,3 @@
|
||||
from .buildings_logic import ModBuildingLogicMixin
|
||||
from .deepwoods_logic import DeepWoodsLogicMixin
|
||||
from .elevator_logic import ModElevatorLogicMixin
|
||||
from .item_logic import ModItemLogicMixin
|
||||
@@ -16,6 +15,6 @@ class ModLogicMixin(BaseLogicMixin):
|
||||
self.mod = ModLogic(*args, **kwargs)
|
||||
|
||||
|
||||
class ModLogic(ModElevatorLogicMixin, MagicLogicMixin, ModSkillLogicMixin, ModItemLogicMixin, ModQuestLogicMixin, ModBuildingLogicMixin,
|
||||
class ModLogic(ModElevatorLogicMixin, MagicLogicMixin, ModSkillLogicMixin, ModItemLogicMixin, ModQuestLogicMixin,
|
||||
ModSpecialOrderLogicMixin, DeepWoodsLogicMixin, SVELogicMixin):
|
||||
pass
|
||||
|
||||
@@ -34,7 +34,7 @@ class RegionData:
|
||||
merged_exits.extend(self.exits)
|
||||
if exits is not None:
|
||||
merged_exits.extend(exits)
|
||||
merged_exits = list(set(merged_exits))
|
||||
merged_exits = sorted(set(merged_exits))
|
||||
return RegionData(self.name, merged_exits, is_ginger_island=self.is_ginger_island)
|
||||
|
||||
def get_without_exits(self, exits_to_remove: Set[str]):
|
||||
|
||||
@@ -521,7 +521,7 @@ def create_final_regions(world_options) -> List[RegionData]:
|
||||
final_regions.extend(vanilla_regions)
|
||||
if world_options.mods is None:
|
||||
return final_regions
|
||||
for mod in world_options.mods.value:
|
||||
for mod in sorted(world_options.mods.value):
|
||||
if mod not in ModDataList:
|
||||
continue
|
||||
for mod_region in ModDataList[mod].regions:
|
||||
@@ -747,8 +747,7 @@ def swap_one_random_connection(regions_by_name, connections_by_name, randomized_
|
||||
randomized_connections_already_shuffled = {connection: randomized_connections[connection]
|
||||
for connection in randomized_connections
|
||||
if connection != randomized_connections[connection]}
|
||||
unreachable_regions_names_leading_somewhere = tuple([region for region in unreachable_regions
|
||||
if len(regions_by_name[region].exits) > 0])
|
||||
unreachable_regions_names_leading_somewhere = [region for region in sorted(unreachable_regions) if len(regions_by_name[region].exits) > 0]
|
||||
unreachable_regions_leading_somewhere = [regions_by_name[region_name] for region_name in unreachable_regions_names_leading_somewhere]
|
||||
unreachable_regions_exits_names = [exit_name for region in unreachable_regions_leading_somewhere for exit_name in region.exits]
|
||||
unreachable_connections = [connections_by_name[exit_name] for exit_name in unreachable_regions_exits_names]
|
||||
|
||||
@@ -19,7 +19,7 @@ from .logic.logic import StardewLogic
|
||||
from .logic.time_logic import MAX_MONTHS
|
||||
from .logic.tool_logic import tool_upgrade_prices
|
||||
from .mods.mod_data import ModNames
|
||||
from .options import BuildingProgression, ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \
|
||||
from .options import ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \
|
||||
Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity, StardewValleyOptions, Walnutsanity
|
||||
from .stardew_rule import And, StardewRule, true_
|
||||
from .stardew_rule.indirect_connection import look_for_indirect_connection
|
||||
@@ -71,7 +71,7 @@ def set_rules(world):
|
||||
set_tool_rules(logic, multiworld, player, world_content)
|
||||
set_skills_rules(logic, multiworld, player, world_content)
|
||||
set_bundle_rules(bundle_rooms, logic, multiworld, player, world_options)
|
||||
set_building_rules(logic, multiworld, player, world_options)
|
||||
set_building_rules(logic, multiworld, player, world_content)
|
||||
set_cropsanity_rules(logic, multiworld, player, world_content)
|
||||
set_story_quests_rules(all_location_names, logic, multiworld, player, world_options)
|
||||
set_special_order_rules(all_location_names, logic, multiworld, player, world_options)
|
||||
@@ -130,15 +130,19 @@ def set_tool_rules(logic: StardewLogic, multiworld, player, content: StardewCont
|
||||
MultiWorldRules.set_rule(tool_upgrade_location, logic.tool.has_tool(tool, previous))
|
||||
|
||||
|
||||
def set_building_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
|
||||
if not world_options.building_progression & BuildingProgression.option_progressive:
|
||||
def set_building_rules(logic: StardewLogic, multiworld, player, content: StardewContent):
|
||||
building_progression = content.features.building_progression
|
||||
if not building_progression.is_progressive:
|
||||
return
|
||||
|
||||
for building in locations.locations_by_tag[LocationTags.BUILDING_BLUEPRINT]:
|
||||
if building.mod_name is not None and building.mod_name not in world_options.mods:
|
||||
for building in content.farm_buildings.values():
|
||||
if building.name in building_progression.starting_buildings:
|
||||
continue
|
||||
MultiWorldRules.set_rule(multiworld.get_location(building.name, player),
|
||||
logic.registry.building_rules[building.name.replace(" Blueprint", "")])
|
||||
|
||||
location_name = building_progression.to_location_name(building.name)
|
||||
|
||||
MultiWorldRules.set_rule(multiworld.get_location(location_name, player),
|
||||
logic.building.can_build(building.name))
|
||||
|
||||
|
||||
def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
|
||||
@@ -241,7 +245,7 @@ def set_dangerous_mine_rules(logic, multiworld, player, world_options: StardewVa
|
||||
|
||||
|
||||
def set_farm_buildings_entrance_rules(logic, multiworld, player):
|
||||
set_entrance_rule(multiworld, player, Entrance.downstairs_to_cellar, logic.building.has_house(3))
|
||||
set_entrance_rule(multiworld, player, Entrance.downstairs_to_cellar, logic.building.has_building(Building.cellar))
|
||||
set_entrance_rule(multiworld, player, Entrance.use_desert_obelisk, logic.can_use_obelisk(Transportation.desert_obelisk))
|
||||
set_entrance_rule(multiworld, player, Entrance.enter_greenhouse, logic.received("Greenhouse"))
|
||||
set_entrance_rule(multiworld, player, Entrance.enter_coop, logic.building.has_building(Building.coop))
|
||||
|
||||
@@ -14,9 +14,11 @@ class Building:
|
||||
stable = "Stable"
|
||||
well = "Well"
|
||||
shipping_bin = "Shipping Bin"
|
||||
farm_house = "Farm House"
|
||||
kitchen = "Kitchen"
|
||||
kids_room = "Kids Room"
|
||||
cellar = "Cellar"
|
||||
pet_bowl = "Pet Bowl"
|
||||
|
||||
|
||||
class ModBuilding:
|
||||
|
||||
@@ -61,11 +61,13 @@ class TestBooksanityNone(SVTestBase):
|
||||
for location in self.multiworld.get_locations():
|
||||
if not location.name.startswith(shipsanity_prefix):
|
||||
continue
|
||||
|
||||
item_to_ship = location.name[len(shipsanity_prefix):]
|
||||
if item_to_ship not in power_books and item_to_ship not in skill_books:
|
||||
continue
|
||||
|
||||
with self.subTest(location.name):
|
||||
self.assert_can_reach_location(location, self.multiworld.state)
|
||||
self.assert_can_reach_location(location)
|
||||
|
||||
|
||||
class TestBooksanityPowers(SVTestBase):
|
||||
@@ -107,11 +109,13 @@ class TestBooksanityPowers(SVTestBase):
|
||||
for location in self.multiworld.get_locations():
|
||||
if not location.name.startswith(shipsanity_prefix):
|
||||
continue
|
||||
|
||||
item_to_ship = location.name[len(shipsanity_prefix):]
|
||||
if item_to_ship not in power_books and item_to_ship not in skill_books:
|
||||
continue
|
||||
|
||||
with self.subTest(location.name):
|
||||
self.assert_can_reach_location(location, self.multiworld.state)
|
||||
self.assert_can_reach_location(location)
|
||||
|
||||
|
||||
class TestBooksanityPowersAndSkills(SVTestBase):
|
||||
@@ -153,11 +157,13 @@ class TestBooksanityPowersAndSkills(SVTestBase):
|
||||
for location in self.multiworld.get_locations():
|
||||
if not location.name.startswith(shipsanity_prefix):
|
||||
continue
|
||||
|
||||
item_to_ship = location.name[len(shipsanity_prefix):]
|
||||
if item_to_ship not in power_books and item_to_ship not in skill_books:
|
||||
continue
|
||||
|
||||
with self.subTest(location.name):
|
||||
self.assert_can_reach_location(location, self.multiworld.state)
|
||||
self.assert_can_reach_location(location)
|
||||
|
||||
|
||||
class TestBooksanityAll(SVTestBase):
|
||||
@@ -199,8 +205,10 @@ class TestBooksanityAll(SVTestBase):
|
||||
for location in self.multiworld.get_locations():
|
||||
if not location.name.startswith(shipsanity_prefix):
|
||||
continue
|
||||
|
||||
item_to_ship = location.name[len(shipsanity_prefix):]
|
||||
if item_to_ship not in power_books and item_to_ship not in skill_books:
|
||||
continue
|
||||
|
||||
with self.subTest(location.name):
|
||||
self.assert_can_reach_location(location, self.multiworld.state)
|
||||
self.assert_can_reach_location(location)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
from . import SVTestBase
|
||||
from .. import options
|
||||
from ..strings.ap_names.transport_names import Transportation
|
||||
from ..strings.building_names import Building
|
||||
from ..strings.region_names import Region
|
||||
from ..strings.seed_names import Seed
|
||||
|
||||
|
||||
class TestCropsanityRules(SVTestBase):
|
||||
@@ -8,13 +12,13 @@ class TestCropsanityRules(SVTestBase):
|
||||
}
|
||||
|
||||
def test_need_greenhouse_for_cactus(self):
|
||||
harvest_cactus = self.world.logic.region.can_reach_location("Harvest Cactus Fruit")
|
||||
self.assert_rule_false(harvest_cactus, self.multiworld.state)
|
||||
harvest_cactus_fruit = "Harvest Cactus Fruit"
|
||||
self.assert_cannot_reach_location(harvest_cactus_fruit)
|
||||
|
||||
self.multiworld.state.collect(self.create_item("Cactus Seeds"))
|
||||
self.multiworld.state.collect(self.create_item("Shipping Bin"))
|
||||
self.multiworld.state.collect(self.create_item("Desert Obelisk"))
|
||||
self.assert_rule_false(harvest_cactus, self.multiworld.state)
|
||||
self.multiworld.state.collect(self.create_item(Seed.cactus))
|
||||
self.multiworld.state.collect(self.create_item(Building.shipping_bin))
|
||||
self.multiworld.state.collect(self.create_item(Transportation.desert_obelisk))
|
||||
self.assert_cannot_reach_location(harvest_cactus_fruit)
|
||||
|
||||
self.multiworld.state.collect(self.create_item("Greenhouse"))
|
||||
self.assert_rule_true(harvest_cactus, self.multiworld.state)
|
||||
self.multiworld.state.collect(self.create_item(Region.greenhouse))
|
||||
self.assert_can_reach_location(harvest_cactus_fruit)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from collections import Counter
|
||||
|
||||
from . import SVTestBase
|
||||
from .assertion import WorldAssertMixin
|
||||
from .. import options
|
||||
@@ -5,27 +7,49 @@ from .. import options
|
||||
|
||||
class TestStartInventoryStandardFarm(WorldAssertMixin, SVTestBase):
|
||||
options = {
|
||||
options.FarmType.internal_name: options.FarmType.option_standard,
|
||||
options.FarmType: options.FarmType.option_standard,
|
||||
}
|
||||
|
||||
def test_start_inventory_progressive_coops(self):
|
||||
start_items = dict(map(lambda x: (x.name, self.multiworld.precollected_items[self.player].count(x)), self.multiworld.precollected_items[self.player]))
|
||||
items = dict(map(lambda x: (x.name, self.multiworld.itempool.count(x)), self.multiworld.itempool))
|
||||
start_items = Counter((i.name for i in self.multiworld.precollected_items[self.player]))
|
||||
items = Counter((i.name for i in self.multiworld.itempool))
|
||||
|
||||
self.assertIn("Progressive Coop", items)
|
||||
self.assertEqual(items["Progressive Coop"], 3)
|
||||
self.assertNotIn("Progressive Coop", start_items)
|
||||
|
||||
def test_coop_is_not_logically_available(self):
|
||||
self.assert_rule_false(self.world.logic.building.has_building("Coop"))
|
||||
|
||||
class TestStartInventoryMeadowLands(WorldAssertMixin, SVTestBase):
|
||||
|
||||
class TestStartInventoryMeadowLandsProgressiveBuilding(WorldAssertMixin, SVTestBase):
|
||||
options = {
|
||||
options.FarmType.internal_name: options.FarmType.option_meadowlands,
|
||||
options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive,
|
||||
options.FarmType: options.FarmType.option_meadowlands,
|
||||
options.BuildingProgression: options.BuildingProgression.option_progressive,
|
||||
}
|
||||
|
||||
def test_start_inventory_progressive_coops(self):
|
||||
start_items = dict(map(lambda x: (x.name, self.multiworld.precollected_items[self.player].count(x)), self.multiworld.precollected_items[self.player]))
|
||||
items = dict(map(lambda x: (x.name, self.multiworld.itempool.count(x)), self.multiworld.itempool))
|
||||
start_items = Counter((i.name for i in self.multiworld.precollected_items[self.player]))
|
||||
items = Counter((i.name for i in self.multiworld.itempool))
|
||||
|
||||
self.assertIn("Progressive Coop", items)
|
||||
self.assertEqual(items["Progressive Coop"], 2)
|
||||
self.assertIn("Progressive Coop", start_items)
|
||||
self.assertEqual(start_items["Progressive Coop"], 1)
|
||||
|
||||
def test_coop_is_logically_available(self):
|
||||
self.assert_rule_true(self.world.logic.building.has_building("Coop"))
|
||||
|
||||
|
||||
class TestStartInventoryMeadowLandsVanillaBuildings(WorldAssertMixin, SVTestBase):
|
||||
options = {
|
||||
options.FarmType: options.FarmType.option_meadowlands,
|
||||
options.BuildingProgression: options.BuildingProgression.option_vanilla,
|
||||
}
|
||||
|
||||
def test_start_inventory_has_no_coop(self):
|
||||
start_items = Counter((i.name for i in self.multiworld.precollected_items[self.player]))
|
||||
self.assertNotIn("Progressive Coop", start_items)
|
||||
|
||||
def test_coop_is_logically_available(self):
|
||||
self.assert_rule_true(self.world.logic.building.has_building("Coop"))
|
||||
|
||||
@@ -3,13 +3,30 @@ from typing import List
|
||||
from BaseClasses import ItemClassification, Item
|
||||
from . import SVTestBase
|
||||
from .. import items, location_table, options
|
||||
from ..items import Group
|
||||
from ..items import Group, ItemData
|
||||
from ..locations import LocationTags
|
||||
from ..options import Friendsanity, SpecialOrderLocations, Shipsanity, Chefsanity, SeasonRandomization, Craftsanity, ExcludeGingerIsland, SkillProgression, \
|
||||
Booksanity, Walnutsanity
|
||||
from ..strings.region_names import Region
|
||||
|
||||
|
||||
def get_all_permanent_progression_items() -> List[ItemData]:
|
||||
"""Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression.
|
||||
"""
|
||||
return [
|
||||
item
|
||||
for item in items.all_items
|
||||
if ItemClassification.progression in item.classification
|
||||
if item.mod_name is None
|
||||
if item.name not in {event.name for event in items.events}
|
||||
if item.name not in {deprecated.name for deprecated in items.items_by_group[Group.DEPRECATED]}
|
||||
if item.name not in {season.name for season in items.items_by_group[Group.SEASON]}
|
||||
if item.name not in {weapon.name for weapon in items.items_by_group[Group.WEAPON]}
|
||||
if item.name not in {baby.name for baby in items.items_by_group[Group.BABY]}
|
||||
if item.name != "The Gateway Gazette"
|
||||
]
|
||||
|
||||
|
||||
class TestBaseItemGeneration(SVTestBase):
|
||||
options = {
|
||||
SeasonRandomization.internal_name: SeasonRandomization.option_progressive,
|
||||
@@ -25,17 +42,8 @@ class TestBaseItemGeneration(SVTestBase):
|
||||
}
|
||||
|
||||
def test_all_progression_items_are_added_to_the_pool(self):
|
||||
all_created_items = [item.name for item in self.multiworld.itempool]
|
||||
# Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression
|
||||
items_to_ignore = [event.name for event in items.events]
|
||||
items_to_ignore.extend(item.name for item in items.all_items if item.mod_name is not None)
|
||||
items_to_ignore.extend(deprecated.name for deprecated in items.items_by_group[Group.DEPRECATED])
|
||||
items_to_ignore.extend(season.name for season in items.items_by_group[Group.SEASON])
|
||||
items_to_ignore.extend(weapon.name for weapon in items.items_by_group[Group.WEAPON])
|
||||
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
|
||||
items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK])
|
||||
items_to_ignore.append("The Gateway Gazette")
|
||||
progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore]
|
||||
all_created_items = set(self.get_all_created_items())
|
||||
progression_items = get_all_permanent_progression_items()
|
||||
for progression_item in progression_items:
|
||||
with self.subTest(f"{progression_item.name}"):
|
||||
self.assertIn(progression_item.name, all_created_items)
|
||||
@@ -45,19 +53,19 @@ class TestBaseItemGeneration(SVTestBase):
|
||||
self.assertEqual(len(non_event_locations), len(self.multiworld.itempool))
|
||||
|
||||
def test_does_not_create_deprecated_items(self):
|
||||
all_created_items = [item.name for item in self.multiworld.itempool]
|
||||
all_created_items = set(self.get_all_created_items())
|
||||
for deprecated_item in items.items_by_group[items.Group.DEPRECATED]:
|
||||
with self.subTest(f"{deprecated_item.name}"):
|
||||
self.assertNotIn(deprecated_item.name, all_created_items)
|
||||
|
||||
def test_does_not_create_more_than_one_maximum_one_items(self):
|
||||
all_created_items = [item.name for item in self.multiworld.itempool]
|
||||
all_created_items = self.get_all_created_items()
|
||||
for maximum_one_item in items.items_by_group[items.Group.MAXIMUM_ONE]:
|
||||
with self.subTest(f"{maximum_one_item.name}"):
|
||||
self.assertLessEqual(all_created_items.count(maximum_one_item.name), 1)
|
||||
|
||||
def test_does_not_create_exactly_two_items(self):
|
||||
all_created_items = [item.name for item in self.multiworld.itempool]
|
||||
def test_does_not_create_or_create_two_of_exactly_two_items(self):
|
||||
all_created_items = self.get_all_created_items()
|
||||
for exactly_two_item in items.items_by_group[items.Group.EXACTLY_TWO]:
|
||||
with self.subTest(f"{exactly_two_item.name}"):
|
||||
count = all_created_items.count(exactly_two_item.name)
|
||||
@@ -77,17 +85,10 @@ class TestNoGingerIslandItemGeneration(SVTestBase):
|
||||
}
|
||||
|
||||
def test_all_progression_items_except_island_are_added_to_the_pool(self):
|
||||
all_created_items = [item.name for item in self.multiworld.itempool]
|
||||
# Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression
|
||||
items_to_ignore = [event.name for event in items.events]
|
||||
items_to_ignore.extend(item.name for item in items.all_items if item.mod_name is not None)
|
||||
items_to_ignore.extend(deprecated.name for deprecated in items.items_by_group[Group.DEPRECATED])
|
||||
items_to_ignore.extend(season.name for season in items.items_by_group[Group.SEASON])
|
||||
items_to_ignore.extend(season.name for season in items.items_by_group[Group.WEAPON])
|
||||
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
|
||||
items_to_ignore.append("The Gateway Gazette")
|
||||
progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore]
|
||||
all_created_items = set(self.get_all_created_items())
|
||||
progression_items = get_all_permanent_progression_items()
|
||||
for progression_item in progression_items:
|
||||
|
||||
with self.subTest(f"{progression_item.name}"):
|
||||
if Group.GINGER_ISLAND in progression_item.groups:
|
||||
self.assertNotIn(progression_item.name, all_created_items)
|
||||
@@ -100,19 +101,19 @@ class TestNoGingerIslandItemGeneration(SVTestBase):
|
||||
self.assertEqual(len(non_event_locations), len(self.multiworld.itempool))
|
||||
|
||||
def test_does_not_create_deprecated_items(self):
|
||||
all_created_items = [item.name for item in self.multiworld.itempool]
|
||||
all_created_items = self.get_all_created_items()
|
||||
for deprecated_item in items.items_by_group[items.Group.DEPRECATED]:
|
||||
with self.subTest(f"Deprecated item: {deprecated_item.name}"):
|
||||
self.assertNotIn(deprecated_item.name, all_created_items)
|
||||
|
||||
def test_does_not_create_more_than_one_maximum_one_items(self):
|
||||
all_created_items = [item.name for item in self.multiworld.itempool]
|
||||
all_created_items = self.get_all_created_items()
|
||||
for maximum_one_item in items.items_by_group[items.Group.MAXIMUM_ONE]:
|
||||
with self.subTest(f"{maximum_one_item.name}"):
|
||||
self.assertLessEqual(all_created_items.count(maximum_one_item.name), 1)
|
||||
|
||||
def test_does_not_create_exactly_two_items(self):
|
||||
all_created_items = [item.name for item in self.multiworld.itempool]
|
||||
all_created_items = self.get_all_created_items()
|
||||
for exactly_two_item in items.items_by_group[items.Group.EXACTLY_TWO]:
|
||||
with self.subTest(f"{exactly_two_item.name}"):
|
||||
count = all_created_items.count(exactly_two_item.name)
|
||||
|
||||
@@ -49,9 +49,9 @@ class LogicTestBase(RuleAssertMixin, TestCase):
|
||||
self.assert_rule_can_be_resolved(rule, self.multiworld.state)
|
||||
|
||||
def test_given_building_rule_then_can_be_resolved(self):
|
||||
for building in self.logic.registry.building_rules.keys():
|
||||
for building in self.world.content.farm_buildings:
|
||||
with self.subTest(msg=building):
|
||||
rule = self.logic.registry.building_rules[building]
|
||||
rule = self.logic.building.can_build(building)
|
||||
self.assert_rule_can_be_resolved(rule, self.multiworld.state)
|
||||
|
||||
def test_given_quest_rule_then_can_be_resolved(self):
|
||||
|
||||
@@ -8,9 +8,8 @@ class TestBitFlagsVanilla(SVTestBase):
|
||||
BuildingProgression.internal_name: BuildingProgression.option_vanilla}
|
||||
|
||||
def test_options_are_not_detected_as_progressive(self):
|
||||
world_options = self.world.options
|
||||
tool_progressive = self.world.content.features.tool_progression.is_progressive
|
||||
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
|
||||
building_progressive = self.world.content.features.building_progression.is_progressive
|
||||
self.assertFalse(tool_progressive)
|
||||
self.assertFalse(building_progressive)
|
||||
|
||||
@@ -25,9 +24,8 @@ class TestBitFlagsVanillaCheap(SVTestBase):
|
||||
BuildingProgression.internal_name: BuildingProgression.option_vanilla_cheap}
|
||||
|
||||
def test_options_are_not_detected_as_progressive(self):
|
||||
world_options = self.world.options
|
||||
tool_progressive = self.world.content.features.tool_progression.is_progressive
|
||||
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
|
||||
building_progressive = self.world.content.features.building_progression.is_progressive
|
||||
self.assertFalse(tool_progressive)
|
||||
self.assertFalse(building_progressive)
|
||||
|
||||
@@ -42,9 +40,8 @@ class TestBitFlagsVanillaVeryCheap(SVTestBase):
|
||||
BuildingProgression.internal_name: BuildingProgression.option_vanilla_very_cheap}
|
||||
|
||||
def test_options_are_not_detected_as_progressive(self):
|
||||
world_options = self.world.options
|
||||
tool_progressive = self.world.content.features.tool_progression.is_progressive
|
||||
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
|
||||
building_progressive = self.world.content.features.building_progression.is_progressive
|
||||
self.assertFalse(tool_progressive)
|
||||
self.assertFalse(building_progressive)
|
||||
|
||||
@@ -59,9 +56,8 @@ class TestBitFlagsProgressive(SVTestBase):
|
||||
BuildingProgression.internal_name: BuildingProgression.option_progressive}
|
||||
|
||||
def test_options_are_detected_as_progressive(self):
|
||||
world_options = self.world.options
|
||||
tool_progressive = self.world.content.features.tool_progression.is_progressive
|
||||
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
|
||||
building_progressive = self.world.content.features.building_progression.is_progressive
|
||||
self.assertTrue(tool_progressive)
|
||||
self.assertTrue(building_progressive)
|
||||
|
||||
@@ -76,9 +72,8 @@ class TestBitFlagsProgressiveCheap(SVTestBase):
|
||||
BuildingProgression.internal_name: BuildingProgression.option_progressive_cheap}
|
||||
|
||||
def test_options_are_detected_as_progressive(self):
|
||||
world_options = self.world.options
|
||||
tool_progressive = self.world.content.features.tool_progression.is_progressive
|
||||
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
|
||||
building_progressive = self.world.content.features.building_progression.is_progressive
|
||||
self.assertTrue(tool_progressive)
|
||||
self.assertTrue(building_progressive)
|
||||
|
||||
@@ -93,9 +88,8 @@ class TestBitFlagsProgressiveVeryCheap(SVTestBase):
|
||||
BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap}
|
||||
|
||||
def test_options_are_detected_as_progressive(self):
|
||||
world_options = self.world.options
|
||||
tool_progressive = self.world.content.features.tool_progression.is_progressive
|
||||
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
|
||||
building_progressive = self.world.content.features.building_progression.is_progressive
|
||||
self.assertTrue(tool_progressive)
|
||||
self.assertTrue(building_progressive)
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import itertools
|
||||
from typing import ClassVar
|
||||
|
||||
from BaseClasses import ItemClassification
|
||||
from Options import NamedRange
|
||||
from test.param import classvar_matrix
|
||||
from . import SVTestCase, solo_multiworld, SVTestBase
|
||||
from .assertion import WorldAssertMixin
|
||||
from .long.option_names import all_option_choices
|
||||
from .options.option_names import all_option_choices
|
||||
from .options.presets import allsanity_no_mods_6_x_x, allsanity_mods_6_x_x
|
||||
from .. import items_by_group, Group, StardewValleyWorld
|
||||
from .. import items_by_group, Group
|
||||
from ..locations import locations_by_tag, LocationTags, location_table
|
||||
from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations
|
||||
from ..strings.goal_names import Goal as GoalName
|
||||
@@ -18,42 +19,36 @@ SEASONS = {Season.spring, Season.summer, Season.fall, Season.winter}
|
||||
TOOLS = {"Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can", "Fishing Rod"}
|
||||
|
||||
|
||||
@classvar_matrix(option_and_choice=all_option_choices)
|
||||
class TestGenerateDynamicOptions(WorldAssertMixin, SVTestCase):
|
||||
def test_given_special_range_when_generate_then_basic_checks(self):
|
||||
options = StardewValleyWorld.options_dataclass.type_hints
|
||||
for option_name, option in options.items():
|
||||
if not issubclass(option, NamedRange):
|
||||
continue
|
||||
for value in option.special_range_names:
|
||||
world_options = {option_name: option.special_range_names[value]}
|
||||
with self.solo_world_sub_test(f"{option_name}: {value}", world_options) as (multiworld, _):
|
||||
self.assert_basic_checks(multiworld)
|
||||
option_and_choice: ClassVar[tuple[str, str]]
|
||||
|
||||
def test_given_choice_when_generate_then_basic_checks(self):
|
||||
options = StardewValleyWorld.options_dataclass.type_hints
|
||||
for option_name, option in options.items():
|
||||
if not option.options:
|
||||
continue
|
||||
for value in option.options:
|
||||
world_options = {option_name: option.options[value]}
|
||||
with self.solo_world_sub_test(f"{option_name}: {value}", world_options) as (multiworld, _):
|
||||
self.assert_basic_checks(multiworld)
|
||||
def test_given_option_and_choice_when_generate_then_basic_checks(self):
|
||||
option, choice = self.option_and_choice
|
||||
world_options = {option: choice}
|
||||
with solo_multiworld(world_options) as (multiworld, stardew_world):
|
||||
self.assert_basic_checks(multiworld)
|
||||
|
||||
|
||||
@classvar_matrix(goal_and_location=[
|
||||
("community_center", GoalName.community_center),
|
||||
("grandpa_evaluation", GoalName.grandpa_evaluation),
|
||||
("bottom_of_the_mines", GoalName.bottom_of_the_mines),
|
||||
("cryptic_note", GoalName.cryptic_note),
|
||||
("master_angler", GoalName.master_angler),
|
||||
("complete_collection", GoalName.complete_museum),
|
||||
("full_house", GoalName.full_house),
|
||||
("perfection", GoalName.perfection),
|
||||
])
|
||||
class TestGoal(SVTestCase):
|
||||
goal_and_location: ClassVar[tuple[str, str]]
|
||||
|
||||
def test_given_goal_when_generate_then_victory_is_in_correct_location(self):
|
||||
for goal, location in [("community_center", GoalName.community_center),
|
||||
("grandpa_evaluation", GoalName.grandpa_evaluation),
|
||||
("bottom_of_the_mines", GoalName.bottom_of_the_mines),
|
||||
("cryptic_note", GoalName.cryptic_note),
|
||||
("master_angler", GoalName.master_angler),
|
||||
("complete_collection", GoalName.complete_museum),
|
||||
("full_house", GoalName.full_house),
|
||||
("perfection", GoalName.perfection)]:
|
||||
world_options = {Goal.internal_name: Goal.options[goal]}
|
||||
with self.solo_world_sub_test(f"Goal: {goal}, Location: {location}", world_options) as (multi_world, _):
|
||||
victory = multi_world.find_item("Victory", 1)
|
||||
self.assertEqual(victory.name, location)
|
||||
goal, location = self.goal_and_location
|
||||
world_options = {Goal.internal_name: goal}
|
||||
with solo_multiworld(world_options) as (multi_world, _):
|
||||
victory = multi_world.find_item("Victory", 1)
|
||||
self.assertEqual(victory.name, location)
|
||||
|
||||
|
||||
class TestSeasonRandomization(SVTestCase):
|
||||
@@ -104,26 +99,28 @@ class TestToolProgression(SVTestBase):
|
||||
self.assertEqual(useful_count, 1)
|
||||
|
||||
|
||||
@classvar_matrix(option_and_choice=all_option_choices)
|
||||
class TestGenerateAllOptionsWithExcludeGingerIsland(WorldAssertMixin, SVTestCase):
|
||||
option_and_choice: ClassVar[tuple[str, str]]
|
||||
|
||||
def test_given_choice_when_generate_exclude_ginger_island_then_ginger_island_is_properly_excluded(self):
|
||||
for option, option_choice in all_option_choices:
|
||||
if option is ExcludeGingerIsland:
|
||||
continue
|
||||
option, option_choice = self.option_and_choice
|
||||
|
||||
world_options = {
|
||||
ExcludeGingerIsland: ExcludeGingerIsland.option_true,
|
||||
option: option_choice
|
||||
}
|
||||
if option == ExcludeGingerIsland.internal_name:
|
||||
self.skipTest("ExcludeGingerIsland is forced to true")
|
||||
|
||||
with self.solo_world_sub_test(f"{option.internal_name}: {option_choice}", world_options) as (multiworld, stardew_world):
|
||||
world_options = {
|
||||
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
|
||||
option: option_choice
|
||||
}
|
||||
|
||||
# Some options, like goals, will force Ginger island back in the game. We want to skip testing those.
|
||||
if stardew_world.options.exclude_ginger_island != ExcludeGingerIsland.option_true:
|
||||
continue
|
||||
with solo_multiworld(world_options) as (multiworld, stardew_world):
|
||||
|
||||
self.assert_basic_checks(multiworld)
|
||||
self.assert_no_ginger_island_content(multiworld)
|
||||
if stardew_world.options.exclude_ginger_island != ExcludeGingerIsland.option_true:
|
||||
self.skipTest("Some options, like goals, will force Ginger island back in the game. We want to skip testing those.")
|
||||
|
||||
self.assert_basic_checks(multiworld)
|
||||
self.assert_no_ginger_island_content(multiworld)
|
||||
|
||||
|
||||
class TestTraps(SVTestCase):
|
||||
|
||||
29
worlds/stardew_valley/test/TestRandomWorlds.py
Normal file
29
worlds/stardew_valley/test/TestRandomWorlds.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from BaseClasses import MultiWorld, get_seed
|
||||
from test.param import classvar_matrix
|
||||
from . import SVTestCase, skip_long_tests, solo_multiworld
|
||||
from .assertion import GoalAssertMixin, OptionAssertMixin, WorldAssertMixin
|
||||
from .options.option_names import generate_random_world_options
|
||||
|
||||
|
||||
@classvar_matrix(n=range(10 if skip_long_tests() else 1000))
|
||||
class TestGenerateManyWorlds(GoalAssertMixin, OptionAssertMixin, WorldAssertMixin, SVTestCase):
|
||||
n: ClassVar[int]
|
||||
|
||||
def test_generate_many_worlds_then_check_results(self):
|
||||
seed = get_seed()
|
||||
world_options = generate_random_world_options(seed + self.n)
|
||||
|
||||
print(f"Generating solo multiworld with seed {seed} for Stardew Valley...")
|
||||
with solo_multiworld(world_options, seed=seed, world_caching=False) as (multiworld, _):
|
||||
self.assert_multiworld_is_valid(multiworld)
|
||||
|
||||
def assert_multiworld_is_valid(self, multiworld: MultiWorld):
|
||||
self.assert_victory_exists(multiworld)
|
||||
self.assert_same_number_items_locations(multiworld)
|
||||
self.assert_goal_world_is_valid(multiworld)
|
||||
self.assert_can_reach_island_if_should(multiworld)
|
||||
self.assert_cropsanity_same_number_items_and_locations(multiworld)
|
||||
self.assert_festivals_give_access_to_deluxe_scarecrow(multiworld)
|
||||
self.assert_has_festival_recipes(multiworld)
|
||||
@@ -70,7 +70,6 @@ class TestWalnutsanityPuzzles(SVTestBase):
|
||||
def test_field_office_locations_require_professor_snail(self):
|
||||
location_names = ["Complete Large Animal Collection", "Complete Snake Collection", "Complete Mummified Frog Collection",
|
||||
"Complete Mummified Bat Collection", "Purple Flowers Island Survey", "Purple Starfish Island Survey", ]
|
||||
locations = [location for location in self.multiworld.get_locations() if location.name in location_names]
|
||||
self.collect("Island Obelisk")
|
||||
self.collect("Island North Turtle")
|
||||
self.collect("Island West Turtle")
|
||||
@@ -84,11 +83,11 @@ class TestWalnutsanityPuzzles(SVTestBase):
|
||||
self.collect("Progressive Sword", 5)
|
||||
self.collect("Combat Level", 10)
|
||||
self.collect("Mining Level", 10)
|
||||
for location in locations:
|
||||
self.assert_cannot_reach_location(location, self.multiworld.state)
|
||||
for location in location_names:
|
||||
self.assert_cannot_reach_location(location)
|
||||
self.collect("Open Professor Snail Cave")
|
||||
for location in locations:
|
||||
self.assert_can_reach_location(location, self.multiworld.state)
|
||||
for location in location_names:
|
||||
self.assert_can_reach_location(location)
|
||||
|
||||
|
||||
class TestWalnutsanityBushes(SVTestBase):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
@@ -11,7 +12,8 @@ from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_mul
|
||||
from worlds.AutoWorld import call_all
|
||||
from .assertion import RuleAssertMixin
|
||||
from .options.utils import fill_namespace_with_default, parse_class_option_keys, fill_dataclass_with_default
|
||||
from .. import StardewValleyWorld, StardewItem
|
||||
from .. import StardewValleyWorld, StardewItem, StardewRule
|
||||
from ..logic.time_logic import MONTH_COEFFICIENT
|
||||
from ..options import StardewValleyOption
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -20,21 +22,19 @@ DEFAULT_TEST_SEED = get_seed()
|
||||
logger.info(f"Default Test Seed: {DEFAULT_TEST_SEED}")
|
||||
|
||||
|
||||
class SVTestCase(unittest.TestCase):
|
||||
# Set False to not skip some 'extra' tests
|
||||
skip_base_tests: bool = True
|
||||
# Set False to run tests that take long
|
||||
skip_long_tests: bool = True
|
||||
def skip_default_tests() -> bool:
|
||||
return not bool(os.environ.get("base", False))
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
base_tests_key = "base"
|
||||
if base_tests_key in os.environ:
|
||||
cls.skip_base_tests = not bool(os.environ[base_tests_key])
|
||||
long_tests_key = "long"
|
||||
if long_tests_key in os.environ:
|
||||
cls.skip_long_tests = not bool(os.environ[long_tests_key])
|
||||
|
||||
def skip_long_tests() -> bool:
|
||||
return not bool(os.environ.get("long", False))
|
||||
|
||||
|
||||
class SVTestCase(unittest.TestCase):
|
||||
skip_default_tests: bool = skip_default_tests()
|
||||
"""Set False to not skip the base fill tests"""
|
||||
skip_long_tests: bool = skip_long_tests()
|
||||
"""Set False to run tests that take long"""
|
||||
|
||||
@contextmanager
|
||||
def solo_world_sub_test(self, msg: Optional[str] = None,
|
||||
@@ -92,10 +92,16 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
|
||||
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
if self.skip_base_tests:
|
||||
if self.skip_default_tests:
|
||||
return False
|
||||
return super().run_default_tests
|
||||
|
||||
def collect_months(self, months: int) -> None:
|
||||
real_total_prog_items = self.world.total_progression_items
|
||||
percent = months * MONTH_COEFFICIENT
|
||||
self.collect("Stardrop", real_total_prog_items * 100 // percent)
|
||||
self.world.total_progression_items = real_total_prog_items
|
||||
|
||||
def collect_lots_of_money(self, percent: float = 0.25):
|
||||
self.collect("Shipping Bin")
|
||||
real_total_prog_items = self.world.total_progression_items
|
||||
@@ -145,12 +151,35 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
|
||||
def create_item(self, item: str) -> StardewItem:
|
||||
return self.world.create_item(item)
|
||||
|
||||
def get_all_created_items(self) -> list[str]:
|
||||
return [item.name for item in itertools.chain(self.multiworld.get_items(), self.multiworld.precollected_items[self.player])]
|
||||
|
||||
def remove_one_by_name(self, item: str) -> None:
|
||||
self.remove(self.create_item(item))
|
||||
|
||||
def reset_collection_state(self):
|
||||
def reset_collection_state(self) -> None:
|
||||
self.multiworld.state = self.original_state.copy()
|
||||
|
||||
def assert_rule_true(self, rule: StardewRule, state: CollectionState | None = None) -> None:
|
||||
if state is None:
|
||||
state = self.multiworld.state
|
||||
super().assert_rule_true(rule, state)
|
||||
|
||||
def assert_rule_false(self, rule: StardewRule, state: CollectionState | None = None) -> None:
|
||||
if state is None:
|
||||
state = self.multiworld.state
|
||||
super().assert_rule_false(rule, state)
|
||||
|
||||
def assert_can_reach_location(self, location: Location | str, state: CollectionState | None = None) -> None:
|
||||
if state is None:
|
||||
state = self.multiworld.state
|
||||
super().assert_can_reach_location(location, state)
|
||||
|
||||
def assert_cannot_reach_location(self, location: Location | str, state: CollectionState | None = None) -> None:
|
||||
if state is None:
|
||||
state = self.multiworld.state
|
||||
super().assert_cannot_reach_location(location, state)
|
||||
|
||||
|
||||
pre_generated_worlds = {}
|
||||
|
||||
@@ -165,21 +194,22 @@ def solo_multiworld(world_options: Optional[Dict[Union[str, StardewValleyOption]
|
||||
yield multiworld, multiworld.worlds[1]
|
||||
else:
|
||||
multiworld = setup_solo_multiworld(world_options, seed)
|
||||
multiworld.lock.acquire()
|
||||
world = multiworld.worlds[1]
|
||||
try:
|
||||
multiworld.lock.acquire()
|
||||
world = multiworld.worlds[1]
|
||||
|
||||
original_state = multiworld.state.copy()
|
||||
original_itempool = multiworld.itempool.copy()
|
||||
unfilled_locations = multiworld.get_unfilled_locations(1)
|
||||
original_state = multiworld.state.copy()
|
||||
original_itempool = multiworld.itempool.copy()
|
||||
unfilled_locations = multiworld.get_unfilled_locations(1)
|
||||
|
||||
yield multiworld, world
|
||||
yield multiworld, world
|
||||
|
||||
multiworld.state = original_state
|
||||
multiworld.itempool = original_itempool
|
||||
for location in unfilled_locations:
|
||||
location.item = None
|
||||
|
||||
multiworld.lock.release()
|
||||
multiworld.state = original_state
|
||||
multiworld.itempool = original_itempool
|
||||
for location in unfilled_locations:
|
||||
location.item = None
|
||||
finally:
|
||||
multiworld.lock.release()
|
||||
|
||||
|
||||
# Mostly a copy of test.general.setup_solo_multiworld, I just don't want to change the core.
|
||||
|
||||
@@ -2,9 +2,11 @@ import unittest
|
||||
from typing import ClassVar, Tuple
|
||||
|
||||
from ...content import content_packs, ContentPack, StardewContent, unpack_content, StardewFeatures, feature
|
||||
from ...strings.building_names import Building
|
||||
|
||||
default_features = StardewFeatures(
|
||||
feature.booksanity.BooksanityDisabled(),
|
||||
feature.building_progression.BuildingProgressionVanilla(starting_buildings={Building.farm_house}),
|
||||
feature.cropsanity.CropsanityDisabled(),
|
||||
feature.fishsanity.FishsanityNone(),
|
||||
feature.friendsanity.FriendsanityNone(),
|
||||
|
||||
@@ -1,61 +1,19 @@
|
||||
import unittest
|
||||
from itertools import combinations, product
|
||||
from itertools import combinations
|
||||
from typing import ClassVar
|
||||
|
||||
from BaseClasses import get_seed
|
||||
from .option_names import all_option_choices, get_option_choices
|
||||
from .. import SVTestCase
|
||||
from test.param import classvar_matrix
|
||||
from .. import SVTestCase, solo_multiworld, skip_long_tests
|
||||
from ..assertion import WorldAssertMixin, ModAssertMixin
|
||||
from ..options.option_names import all_option_choices
|
||||
from ... import options
|
||||
from ...mods.mod_data import ModNames
|
||||
|
||||
assert unittest
|
||||
from ...options.options import all_mods
|
||||
|
||||
|
||||
class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
if cls.skip_long_tests:
|
||||
raise unittest.SkipTest("Long tests disabled")
|
||||
|
||||
def test_given_mod_pairs_when_generate_then_basic_checks(self):
|
||||
for mod_pair in combinations(options.Mods.valid_keys, 2):
|
||||
world_options = {
|
||||
options.Mods: frozenset(mod_pair)
|
||||
}
|
||||
|
||||
with self.solo_world_sub_test(f"Mods: {mod_pair}", world_options, world_caching=False) as (multiworld, _):
|
||||
self.assert_basic_checks(multiworld)
|
||||
self.assert_stray_mod_items(list(mod_pair), multiworld)
|
||||
|
||||
def test_given_mod_names_when_generate_paired_with_other_options_then_basic_checks(self):
|
||||
for mod, (option, value) in product(options.Mods.valid_keys, all_option_choices):
|
||||
world_options = {
|
||||
option: value,
|
||||
options.Mods: mod
|
||||
}
|
||||
|
||||
with self.solo_world_sub_test(f"{option.internal_name}: {value}, Mod: {mod}", world_options, world_caching=False) as (multiworld, _):
|
||||
self.assert_basic_checks(multiworld)
|
||||
self.assert_stray_mod_items(mod, multiworld)
|
||||
|
||||
def test_given_no_quest_all_mods_when_generate_with_all_goals_then_basic_checks(self):
|
||||
for goal, (option, value) in product(get_option_choices(options.Goal), all_option_choices):
|
||||
if option is options.QuestLocations:
|
||||
continue
|
||||
|
||||
world_options = {
|
||||
options.Goal: goal,
|
||||
option: value,
|
||||
options.QuestLocations: -1,
|
||||
options.Mods: frozenset(options.Mods.valid_keys),
|
||||
}
|
||||
|
||||
with self.solo_world_sub_test(f"Goal: {goal}, {option.internal_name}: {value}", world_options, world_caching=False) as (multiworld, _):
|
||||
self.assert_basic_checks(multiworld)
|
||||
|
||||
@unittest.skip
|
||||
@unittest.skip
|
||||
class TestTroubleshootMods(WorldAssertMixin, ModAssertMixin, SVTestCase):
|
||||
def test_troubleshoot_option(self):
|
||||
seed = get_seed(78709133382876990000)
|
||||
|
||||
@@ -67,3 +25,60 @@ class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase):
|
||||
with self.solo_world_sub_test(world_options=world_options, seed=seed, world_caching=False) as (multiworld, _):
|
||||
self.assert_basic_checks(multiworld)
|
||||
self.assert_stray_mod_items(world_options[options.Mods], multiworld)
|
||||
|
||||
|
||||
if skip_long_tests():
|
||||
raise unittest.SkipTest("Long tests disabled")
|
||||
|
||||
|
||||
@classvar_matrix(mod_pair=combinations(sorted(all_mods), 2))
|
||||
class TestGenerateModsPairs(WorldAssertMixin, ModAssertMixin, SVTestCase):
|
||||
mod_pair: ClassVar[tuple[str, str]]
|
||||
|
||||
def test_given_mod_pairs_when_generate_then_basic_checks(self):
|
||||
world_options = {
|
||||
options.Mods.internal_name: frozenset(self.mod_pair)
|
||||
}
|
||||
|
||||
with solo_multiworld(world_options, world_caching=False) as (multiworld, _):
|
||||
self.assert_basic_checks(multiworld)
|
||||
self.assert_stray_mod_items(list(self.mod_pair), multiworld)
|
||||
|
||||
|
||||
@classvar_matrix(mod=all_mods, option_and_choice=all_option_choices)
|
||||
class TestGenerateModAndOptionChoice(WorldAssertMixin, ModAssertMixin, SVTestCase):
|
||||
mod: ClassVar[str]
|
||||
option_and_choice: ClassVar[tuple[str, str]]
|
||||
|
||||
def test_given_mod_names_when_generate_paired_with_other_options_then_basic_checks(self):
|
||||
option, choice = self.option_and_choice
|
||||
|
||||
world_options = {
|
||||
option: choice,
|
||||
options.Mods.internal_name: self.mod
|
||||
}
|
||||
|
||||
with solo_multiworld(world_options, world_caching=False) as (multiworld, _):
|
||||
self.assert_basic_checks(multiworld)
|
||||
self.assert_stray_mod_items(self.mod, multiworld)
|
||||
|
||||
|
||||
@classvar_matrix(goal=options.Goal.options.keys(), option_and_choice=all_option_choices)
|
||||
class TestGenerateAllGoalAndAllOptionWithAllModsWithoutQuest(WorldAssertMixin, ModAssertMixin, SVTestCase):
|
||||
goal = ClassVar[str]
|
||||
option_and_choice = ClassVar[tuple[str, str]]
|
||||
|
||||
def test_given_no_quest_all_mods_when_generate_with_all_goals_then_basic_checks(self):
|
||||
option, choice = self.option_and_choice
|
||||
if option == options.QuestLocations.internal_name:
|
||||
self.skipTest("QuestLocations are disabled")
|
||||
|
||||
world_options = {
|
||||
options.Goal.internal_name: self.goal,
|
||||
option: choice,
|
||||
options.QuestLocations.internal_name: -1,
|
||||
options.Mods.internal_name: frozenset(options.Mods.valid_keys),
|
||||
}
|
||||
|
||||
with solo_multiworld(world_options, world_caching=False) as (multiworld, _):
|
||||
self.assert_basic_checks(multiworld)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user