mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-25 06:03:20 -07:00
Compare commits
53 Commits
adventure-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
117295fd29 | ||
|
|
ec1e113b4c | ||
|
|
347efac0cd | ||
|
|
b7b5bf58aa | ||
|
|
a324c97815 | ||
|
|
f263a0bc91 | ||
|
|
6a9299018c | ||
|
|
ee471a48bd | ||
|
|
879d7c23b7 | ||
|
|
934b09238e | ||
|
|
1fd8e4435e | ||
|
|
50fd42d0c2 | ||
|
|
399958c881 | ||
|
|
78c93d7e39 | ||
|
|
e3b8a60584 | ||
|
|
b7263edfd0 | ||
|
|
1ee749b352 | ||
|
|
f93734f9e3 | ||
|
|
e211dfa1c2 | ||
|
|
0f7deb1d2a | ||
|
|
f2cb16a5be | ||
|
|
98477e27aa | ||
|
|
4149db1a01 | ||
|
|
9ac921380f | ||
|
|
286e24629f | ||
|
|
ab2efc0c5c | ||
|
|
60d6078e1f | ||
|
|
f94492b2d3 | ||
|
|
f03bb61747 | ||
|
|
dc4e8bae98 | ||
|
|
ac26f8be8b | ||
|
|
8c79499573 | ||
|
|
63fbcc5fc8 | ||
|
|
cad217af19 | ||
|
|
a6ad4a8293 | ||
|
|
503999cb32 | ||
|
|
c2d8f2443e | ||
|
|
4571ed7e2f | ||
|
|
ef5cbd3ba3 | ||
|
|
5c162bd7ce | ||
|
|
7bdaaa25c1 | ||
|
|
9a5a02b654 | ||
|
|
4fea6b6e9b | ||
|
|
bd8b8822ac | ||
|
|
0a44c3ec49 | ||
|
|
3262984386 | ||
|
|
180265c8f4 | ||
|
|
a9b4d33cd2 | ||
|
|
5dfb9b28f7 | ||
|
|
ec75793ac3 | ||
|
|
cd4da36863 | ||
|
|
1749e22569 | ||
|
|
0cce88cfbc |
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
|
continue-on-error: false
|
||||||
if: env.diff != '' && matrix.task == 'flake8'
|
if: env.diff != '' && matrix.task == 'flake8'
|
||||||
run: |
|
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"
|
- name: "flake8: Lint modified files"
|
||||||
continue-on-error: true
|
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
|
if-no-files-found: error
|
||||||
retention-days: 7 # keep for 7 days, should be enough
|
retention-days: 7 # keep for 7 days, should be enough
|
||||||
|
|
||||||
build-ubuntu2004:
|
build-ubuntu2204:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
# - copy code below to release.yml -
|
# - copy code below to release.yml -
|
||||||
- uses: actions/checkout@v4
|
- 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-windows: # this is done by hand because of signing
|
||||||
# build-release-macos: # LF volunteer
|
# build-release-macos: # LF volunteer
|
||||||
|
|
||||||
build-release-ubuntu2004:
|
build-release-ubuntu2204:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Set env
|
- name: Set env
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
|||||||
@@ -616,7 +616,7 @@ class MultiWorld():
|
|||||||
locations: Set[Location] = set()
|
locations: Set[Location] = set()
|
||||||
events: Set[Location] = set()
|
events: Set[Location] = set()
|
||||||
for location in self.get_filled_locations():
|
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)
|
locations.add(location)
|
||||||
else:
|
else:
|
||||||
events.add(location)
|
events.add(location)
|
||||||
@@ -1106,6 +1106,9 @@ class Region:
|
|||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return self._list.__len__()
|
return self._list.__len__()
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._list)
|
||||||
|
|
||||||
# This seems to not be needed, but that's a bit suspicious.
|
# This seems to not be needed, but that's a bit suspicious.
|
||||||
# def __del__(self):
|
# def __del__(self):
|
||||||
# self.clear()
|
# self.clear()
|
||||||
@@ -1310,9 +1313,6 @@ class Location:
|
|||||||
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
|
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
|
||||||
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash((self.name, self.player))
|
|
||||||
|
|
||||||
def __lt__(self, other: Location):
|
def __lt__(self, other: Location):
|
||||||
return (self.player, self.name) < (other.player, other.name)
|
return (self.player, self.name) < (other.player, other.name)
|
||||||
|
|
||||||
@@ -1416,6 +1416,10 @@ class Item:
|
|||||||
def flags(self) -> int:
|
def flags(self) -> int:
|
||||||
return self.classification.as_flag()
|
return self.classification.as_flag()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_event(self) -> bool:
|
||||||
|
return self.code is None
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
def __eq__(self, other: object) -> bool:
|
||||||
if not isinstance(other, Item):
|
if not isinstance(other, Item):
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|||||||
19
Fill.py
19
Fill.py
@@ -75,9 +75,11 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
|||||||
items_to_place.append(reachable_items[next_player].pop())
|
items_to_place.append(reachable_items[next_player].pop())
|
||||||
|
|
||||||
for item in items_to_place:
|
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:
|
if pool_item is item:
|
||||||
item_pool.pop(p)
|
del item_pool[-p]
|
||||||
break
|
break
|
||||||
|
|
||||||
maximum_exploration_state = sweep_from_pool(
|
maximum_exploration_state = sweep_from_pool(
|
||||||
@@ -500,13 +502,15 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
|||||||
|
|
||||||
if prioritylocations:
|
if prioritylocations:
|
||||||
# "priority fill"
|
# "priority fill"
|
||||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
|
maximum_exploration_state = sweep_from_pool(multiworld.state)
|
||||||
|
fill_restrictive(multiworld, maximum_exploration_state, prioritylocations, progitempool,
|
||||||
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
|
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
|
||||||
name="Priority", one_item_per_player=True, allow_partial=True)
|
name="Priority", one_item_per_player=True, allow_partial=True)
|
||||||
|
|
||||||
if prioritylocations:
|
if prioritylocations:
|
||||||
# retry with one_item_per_player off because some priority fills can fail to fill with that optimization
|
# retry with one_item_per_player off because some priority fills can fail to fill with that optimization
|
||||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
|
maximum_exploration_state = sweep_from_pool(multiworld.state)
|
||||||
|
fill_restrictive(multiworld, maximum_exploration_state, prioritylocations, progitempool,
|
||||||
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
|
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
|
||||||
name="Priority Retry", one_item_per_player=False)
|
name="Priority Retry", one_item_per_player=False)
|
||||||
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
||||||
@@ -514,14 +518,15 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
|||||||
|
|
||||||
if progitempool:
|
if progitempool:
|
||||||
# "advancement/progression fill"
|
# "advancement/progression fill"
|
||||||
|
maximum_exploration_state = sweep_from_pool(multiworld.state)
|
||||||
if panic_method == "swap":
|
if panic_method == "swap":
|
||||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=True,
|
fill_restrictive(multiworld, maximum_exploration_state, defaultlocations, progitempool, swap=True,
|
||||||
name="Progression", single_player_placement=single_player)
|
name="Progression", single_player_placement=single_player)
|
||||||
elif panic_method == "raise":
|
elif panic_method == "raise":
|
||||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
|
fill_restrictive(multiworld, maximum_exploration_state, defaultlocations, progitempool, swap=False,
|
||||||
name="Progression", single_player_placement=single_player)
|
name="Progression", single_player_placement=single_player)
|
||||||
elif panic_method == "start_inventory":
|
elif panic_method == "start_inventory":
|
||||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
|
fill_restrictive(multiworld, maximum_exploration_state, defaultlocations, progitempool, swap=False,
|
||||||
allow_partial=True, name="Progression", single_player_placement=single_player)
|
allow_partial=True, name="Progression", single_player_placement=single_player)
|
||||||
if progitempool:
|
if progitempool:
|
||||||
for item in progitempool:
|
for item in progitempool:
|
||||||
|
|||||||
13
Generate.py
13
Generate.py
@@ -54,12 +54,22 @@ def mystery_argparse():
|
|||||||
parser.add_argument("--skip_output", action="store_true",
|
parser.add_argument("--skip_output", action="store_true",
|
||||||
help="Skips generation assertion and output stages and skips multidata and spoiler output. "
|
help="Skips generation assertion and output stages and skips multidata and spoiler output. "
|
||||||
"Intended for debugging and testing purposes.")
|
"Intended for debugging and testing purposes.")
|
||||||
|
parser.add_argument("--spoiler_only", action="store_true",
|
||||||
|
help="Skips generation assertion and multidata, outputting only a spoiler log. "
|
||||||
|
"Intended for debugging and testing purposes.")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.skip_output and args.spoiler_only:
|
||||||
|
parser.error("Cannot mix --skip_output and --spoiler_only")
|
||||||
|
elif args.spoiler == 0 and args.spoiler_only:
|
||||||
|
parser.error("Cannot use --spoiler_only when --spoiler=0. Use --skip_output or set --spoiler to a different value")
|
||||||
|
|
||||||
if not os.path.isabs(args.weights_file_path):
|
if not os.path.isabs(args.weights_file_path):
|
||||||
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
|
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
|
||||||
if not os.path.isabs(args.meta_file_path):
|
if not os.path.isabs(args.meta_file_path):
|
||||||
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
|
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
|
||||||
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
|
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
@@ -108,6 +118,8 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
|||||||
raise Exception("Cannot mix --sameoptions with --meta")
|
raise Exception("Cannot mix --sameoptions with --meta")
|
||||||
else:
|
else:
|
||||||
meta_weights = None
|
meta_weights = None
|
||||||
|
|
||||||
|
|
||||||
player_id = 1
|
player_id = 1
|
||||||
player_files = {}
|
player_files = {}
|
||||||
for file in os.scandir(args.player_files_path):
|
for file in os.scandir(args.player_files_path):
|
||||||
@@ -164,6 +176,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
|||||||
erargs.outputpath = args.outputpath
|
erargs.outputpath = args.outputpath
|
||||||
erargs.skip_prog_balancing = args.skip_prog_balancing
|
erargs.skip_prog_balancing = args.skip_prog_balancing
|
||||||
erargs.skip_output = args.skip_output
|
erargs.skip_output = args.skip_output
|
||||||
|
erargs.spoiler_only = args.spoiler_only
|
||||||
erargs.name = {}
|
erargs.name = {}
|
||||||
erargs.csv_output = args.csv_output
|
erargs.csv_output = args.csv_output
|
||||||
|
|
||||||
|
|||||||
292
Launcher.py
292
Launcher.py
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Archipelago launcher for bundled app.
|
Archipelago Launcher
|
||||||
|
|
||||||
* if run with APBP as argument, launch corresponding client.
|
* if run with APBP as argument, launch corresponding client.
|
||||||
* if run with executable as argument, run it passing argv[2:] as arguments
|
* if run with executable as argument, run it passing argv[2:] as arguments
|
||||||
@@ -8,9 +8,7 @@ Archipelago launcher for bundled app.
|
|||||||
Scroll down to components= to add components to the launcher as well as setup.py
|
Scroll down to components= to add components to the launcher as well as setup.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import itertools
|
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import shlex
|
import shlex
|
||||||
@@ -20,10 +18,11 @@ import urllib.parse
|
|||||||
import webbrowser
|
import webbrowser
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from typing import Callable, Optional, Sequence, Tuple, Union
|
from typing import Callable, Optional, Sequence, Tuple, Union, Any
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
|
|
||||||
ModuleUpdate.update()
|
ModuleUpdate.update()
|
||||||
|
|
||||||
import settings
|
import settings
|
||||||
@@ -105,7 +104,8 @@ components.extend([
|
|||||||
Component("Generate Template Options", func=generate_yamls),
|
Component("Generate Template Options", func=generate_yamls),
|
||||||
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")),
|
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")),
|
||||||
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
||||||
Component("Unrated/18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
Component("Unrated/18+ Discord Server", icon="discord",
|
||||||
|
func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
||||||
Component("Browse Files", func=browse_files),
|
Component("Browse Files", func=browse_files),
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
|||||||
url = urllib.parse.urlparse(path)
|
url = urllib.parse.urlparse(path)
|
||||||
queries = urllib.parse.parse_qs(url.query)
|
queries = urllib.parse.parse_qs(url.query)
|
||||||
launch_args = (path, *launch_args)
|
launch_args = (path, *launch_args)
|
||||||
client_component = None
|
client_component = []
|
||||||
text_client_component = None
|
text_client_component = None
|
||||||
if "game" in queries:
|
if "game" in queries:
|
||||||
game = queries["game"][0]
|
game = queries["game"][0]
|
||||||
@@ -122,49 +122,40 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
|||||||
game = "Archipelago"
|
game = "Archipelago"
|
||||||
for component in components:
|
for component in components:
|
||||||
if component.supports_uri and component.game_name == game:
|
if component.supports_uri and component.game_name == game:
|
||||||
client_component = component
|
client_component.append(component)
|
||||||
elif component.display_name == "Text Client":
|
elif component.display_name == "Text Client":
|
||||||
text_client_component = component
|
text_client_component = component
|
||||||
|
|
||||||
if client_component is None:
|
from kvui import MDButton, MDButtonText
|
||||||
|
from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogContentContainer, MDDialogSupportingText
|
||||||
|
from kivymd.uix.divider import MDDivider
|
||||||
|
|
||||||
|
if not client_component:
|
||||||
run_component(text_client_component, *launch_args)
|
run_component(text_client_component, *launch_args)
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
popup_text = MDDialogSupportingText(text="Select client to open and connect with.")
|
||||||
|
component_buttons = [MDDivider()]
|
||||||
|
for component in [text_client_component, *client_component]:
|
||||||
|
component_buttons.append(MDButton(
|
||||||
|
MDButtonText(text=component.display_name),
|
||||||
|
on_release=lambda *args, comp=component: run_component(comp, *launch_args),
|
||||||
|
style="text"
|
||||||
|
))
|
||||||
|
component_buttons.append(MDDivider())
|
||||||
|
|
||||||
from kvui import App, Button, BoxLayout, Label, Window
|
MDDialog(
|
||||||
|
# Headline
|
||||||
|
MDDialogHeadlineText(text="Connect to Multiworld"),
|
||||||
|
# Text
|
||||||
|
popup_text,
|
||||||
|
# Content
|
||||||
|
MDDialogContentContainer(
|
||||||
|
*component_buttons,
|
||||||
|
orientation="vertical"
|
||||||
|
),
|
||||||
|
|
||||||
class Popup(App):
|
).open()
|
||||||
def __init__(self):
|
|
||||||
self.title = "Connect to Multiworld"
|
|
||||||
self.icon = r"data/icon.png"
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def build(self):
|
|
||||||
layout = BoxLayout(orientation="vertical")
|
|
||||||
layout.add_widget(Label(text="Select client to open and connect with."))
|
|
||||||
button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4))
|
|
||||||
|
|
||||||
text_client_button = Button(
|
|
||||||
text=text_client_component.display_name,
|
|
||||||
on_release=lambda *args: run_component(text_client_component, *launch_args)
|
|
||||||
)
|
|
||||||
button_row.add_widget(text_client_button)
|
|
||||||
|
|
||||||
game_client_button = Button(
|
|
||||||
text=client_component.display_name,
|
|
||||||
on_release=lambda *args: run_component(client_component, *launch_args)
|
|
||||||
)
|
|
||||||
button_row.add_widget(game_client_button)
|
|
||||||
|
|
||||||
layout.add_widget(button_row)
|
|
||||||
|
|
||||||
return layout
|
|
||||||
|
|
||||||
def _stop(self, *largs):
|
|
||||||
# see run_gui Launcher _stop comment for details
|
|
||||||
self.root_window.close()
|
|
||||||
super()._stop(*largs)
|
|
||||||
|
|
||||||
Popup().run()
|
|
||||||
|
|
||||||
|
|
||||||
def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]:
|
def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]:
|
||||||
@@ -220,100 +211,166 @@ def launch(exe, in_terminal=False):
|
|||||||
subprocess.Popen(exe)
|
subprocess.Popen(exe)
|
||||||
|
|
||||||
|
|
||||||
|
def create_shortcut(button: Any, component: Component) -> None:
|
||||||
|
from pyshortcuts import make_shortcut
|
||||||
|
script = sys.argv[0]
|
||||||
|
wkdir = Utils.local_path()
|
||||||
|
|
||||||
|
script = f"{script} \"{component.display_name}\""
|
||||||
|
make_shortcut(script, name=f"Archipelago {component.display_name}", icon=local_path("data", "icon.ico"),
|
||||||
|
startmenu=False, terminal=False, working_dir=wkdir)
|
||||||
|
button.menu.dismiss()
|
||||||
|
|
||||||
|
|
||||||
refresh_components: Optional[Callable[[], None]] = None
|
refresh_components: Optional[Callable[[], None]] = None
|
||||||
|
|
||||||
|
|
||||||
def run_gui():
|
def run_gui(path: str, args: Any) -> None:
|
||||||
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage
|
from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, ScrollBox)
|
||||||
|
from kivy.properties import ObjectProperty
|
||||||
from kivy.core.window import Window
|
from kivy.core.window import Window
|
||||||
from kivy.uix.relativelayout import RelativeLayout
|
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.snackbar import MDSnackbar, MDSnackbarText
|
||||||
|
|
||||||
class Launcher(App):
|
from kivy.lang.builder import Builder
|
||||||
|
|
||||||
|
class LauncherCard(MDCard):
|
||||||
|
component: Component | None
|
||||||
|
image: str
|
||||||
|
context_button: MDIconButton = ObjectProperty(None)
|
||||||
|
|
||||||
|
def __init__(self, *args, component: Component | None = None, image_path: str = "", **kwargs):
|
||||||
|
self.component = component
|
||||||
|
self.image = image_path
|
||||||
|
super().__init__(args, kwargs)
|
||||||
|
|
||||||
|
class Launcher(ThemedApp):
|
||||||
base_title: str = "Archipelago Launcher"
|
base_title: str = "Archipelago Launcher"
|
||||||
container: ContainerLayout
|
top_screen: MDFloatLayout = ObjectProperty(None)
|
||||||
grid: GridLayout
|
navigation: MDGridLayout = ObjectProperty(None)
|
||||||
_tool_layout: Optional[ScrollBox] = None
|
grid: MDGridLayout = ObjectProperty(None)
|
||||||
_client_layout: Optional[ScrollBox] = None
|
button_layout: ScrollBox = ObjectProperty(None)
|
||||||
|
cards: list[LauncherCard]
|
||||||
|
current_filter: Sequence[str | Type] | None
|
||||||
|
|
||||||
def __init__(self, ctx=None):
|
def __init__(self, ctx=None, path=None, args=None):
|
||||||
self.title = self.base_title + " " + Utils.__version__
|
self.title = self.base_title + " " + Utils.__version__
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
self.icon = r"data/icon.png"
|
self.icon = r"data/icon.png"
|
||||||
|
self.favorites = []
|
||||||
|
self.launch_uri = path
|
||||||
|
self.launch_args = args
|
||||||
|
self.cards = []
|
||||||
|
self.current_filter = (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
|
||||||
|
persistent = Utils.persistent_load()
|
||||||
|
if "launcher" in persistent:
|
||||||
|
if "favorites" in persistent["launcher"]:
|
||||||
|
self.favorites.extend(persistent["launcher"]["favorites"])
|
||||||
|
if "filter" in persistent["launcher"]:
|
||||||
|
if persistent["launcher"]["filter"]:
|
||||||
|
filters = []
|
||||||
|
for filter in persistent["launcher"]["filter"].split(", "):
|
||||||
|
if filter == "favorites":
|
||||||
|
filters.append(filter)
|
||||||
|
else:
|
||||||
|
filters.append(Type[filter])
|
||||||
|
self.current_filter = filters
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def _refresh_components(self) -> None:
|
def set_favorite(self, caller):
|
||||||
|
if caller.component.display_name in self.favorites:
|
||||||
|
self.favorites.remove(caller.component.display_name)
|
||||||
|
caller.icon = "star-outline"
|
||||||
|
else:
|
||||||
|
self.favorites.append(caller.component.display_name)
|
||||||
|
caller.icon = "star"
|
||||||
|
|
||||||
def build_button(component: Component) -> Widget:
|
def build_card(self, component: Component) -> LauncherCard:
|
||||||
|
"""
|
||||||
|
Builds a card widget for a given component.
|
||||||
|
|
||||||
|
:param component: The component associated with the button.
|
||||||
|
|
||||||
|
:return: The created Card Widget.
|
||||||
"""
|
"""
|
||||||
Builds a button widget for a given component.
|
button_card = LauncherCard(component=component,
|
||||||
|
image_path=icon_paths[component.icon])
|
||||||
|
|
||||||
Args:
|
def open_menu(caller):
|
||||||
component (Component): The component associated with the button.
|
caller.menu.open()
|
||||||
|
|
||||||
Returns:
|
menu_items = [
|
||||||
None. The button is added to the parent grid layout.
|
{
|
||||||
|
"text": "Add shortcut on desktop",
|
||||||
|
"leading_icon": "laptop",
|
||||||
|
"on_release": lambda: create_shortcut(button_card.context_button, component)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
button_card.context_button.menu = MDDropdownMenu(caller=button_card.context_button, items=menu_items)
|
||||||
|
button_card.context_button.bind(on_release=open_menu)
|
||||||
|
|
||||||
"""
|
return button_card
|
||||||
button = Button(text=component.display_name, size_hint_y=None, height=40)
|
|
||||||
button.component = component
|
def _refresh_components(self, type_filter: Sequence[str | Type] | None = None) -> None:
|
||||||
button.bind(on_release=self.component_action)
|
if not type_filter:
|
||||||
if component.icon != "icon":
|
type_filter = [Type.CLIENT, Type.ADJUSTER, Type.TOOL, Type.MISC]
|
||||||
image = ApAsyncImage(source=icon_paths[component.icon],
|
favorites = "favorites" in type_filter
|
||||||
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
|
|
||||||
box_layout = RelativeLayout(size_hint_y=None, height=40)
|
|
||||||
box_layout.add_widget(button)
|
|
||||||
box_layout.add_widget(image)
|
|
||||||
return box_layout
|
|
||||||
return button
|
|
||||||
|
|
||||||
# clear before repopulating
|
# clear before repopulating
|
||||||
assert self._tool_layout and self._client_layout, "must call `build` first"
|
assert self.button_layout, "must call `build` first"
|
||||||
tool_children = reversed(self._tool_layout.layout.children)
|
tool_children = reversed(self.button_layout.layout.children)
|
||||||
for child in tool_children:
|
for child in tool_children:
|
||||||
self._tool_layout.layout.remove_widget(child)
|
self.button_layout.layout.remove_widget(child)
|
||||||
client_children = reversed(self._client_layout.layout.children)
|
|
||||||
for child in client_children:
|
|
||||||
self._client_layout.layout.remove_widget(child)
|
|
||||||
|
|
||||||
_tools = {c.display_name: c for c in components if c.type == Type.TOOL}
|
cards = [card for card in self.cards if card.component.type in type_filter
|
||||||
_clients = {c.display_name: c for c in components if c.type == Type.CLIENT}
|
or favorites and card.component.display_name in self.favorites]
|
||||||
_adjusters = {c.display_name: c for c in components if c.type == Type.ADJUSTER}
|
|
||||||
_miscs = {c.display_name: c for c in components if c.type == Type.MISC}
|
|
||||||
|
|
||||||
for (tool, client) in itertools.zip_longest(itertools.chain(
|
self.current_filter = type_filter
|
||||||
_tools.items(), _miscs.items(), _adjusters.items()
|
|
||||||
), _clients.items()):
|
for card in cards:
|
||||||
# column 1
|
self.button_layout.layout.add_widget(card)
|
||||||
if tool:
|
|
||||||
self._tool_layout.layout.add_widget(build_button(tool[1]))
|
top = self.button_layout.children[0].y + self.button_layout.children[0].height \
|
||||||
# column 2
|
- self.button_layout.height
|
||||||
if client:
|
scroll_percent = self.button_layout.convert_distance_to_scroll(0, top)
|
||||||
self._client_layout.layout.add_widget(build_button(client[1]))
|
self.button_layout.scroll_y = max(0, min(1, scroll_percent[1]))
|
||||||
|
|
||||||
|
def filter_clients(self, caller):
|
||||||
|
self._refresh_components(caller.type)
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
self.container = ContainerLayout()
|
self.top_screen = Builder.load_file(Utils.local_path("data/launcher.kv"))
|
||||||
self.grid = GridLayout(cols=2)
|
self.grid = self.top_screen.ids.grid
|
||||||
self.container.add_widget(self.grid)
|
self.navigation = self.top_screen.ids.navigation
|
||||||
self.grid.add_widget(Label(text="General", size_hint_y=None, height=40))
|
self.button_layout = self.top_screen.ids.button_layout
|
||||||
self.grid.add_widget(Label(text="Clients", size_hint_y=None, height=40))
|
self.set_colors()
|
||||||
self._tool_layout = ScrollBox()
|
self.top_screen.md_bg_color = self.theme_cls.backgroundColor
|
||||||
self._tool_layout.layout.orientation = "vertical"
|
|
||||||
self.grid.add_widget(self._tool_layout)
|
|
||||||
self._client_layout = ScrollBox()
|
|
||||||
self._client_layout.layout.orientation = "vertical"
|
|
||||||
self.grid.add_widget(self._client_layout)
|
|
||||||
|
|
||||||
self._refresh_components()
|
|
||||||
|
|
||||||
global refresh_components
|
global refresh_components
|
||||||
refresh_components = self._refresh_components
|
refresh_components = self._refresh_components
|
||||||
|
|
||||||
Window.bind(on_drop_file=self._on_drop_file)
|
Window.bind(on_drop_file=self._on_drop_file)
|
||||||
|
|
||||||
return self.container
|
for component in components:
|
||||||
|
self.cards.append(self.build_card(component))
|
||||||
|
|
||||||
|
self._refresh_components(self.current_filter)
|
||||||
|
|
||||||
|
return self.top_screen
|
||||||
|
|
||||||
|
def on_start(self):
|
||||||
|
if self.launch_uri:
|
||||||
|
handle_uri(self.launch_uri, self.launch_args)
|
||||||
|
self.launch_uri = None
|
||||||
|
self.launch_args = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def component_action(button):
|
def component_action(button):
|
||||||
|
MDSnackbar(MDSnackbarText(text="Opening in a new window..."), y=dp(24), pos_hint={"center_x": 0.5},
|
||||||
|
size_hint_x=0.5).open()
|
||||||
if button.component.func:
|
if button.component.func:
|
||||||
button.component.func()
|
button.component.func()
|
||||||
else:
|
else:
|
||||||
@@ -333,7 +390,13 @@ def run_gui():
|
|||||||
self.root_window.close()
|
self.root_window.close()
|
||||||
super()._stop(*largs)
|
super()._stop(*largs)
|
||||||
|
|
||||||
Launcher().run()
|
def on_stop(self):
|
||||||
|
Utils.persistent_store("launcher", "favorites", self.favorites)
|
||||||
|
Utils.persistent_store("launcher", "filter", ", ".join(filter.name if isinstance(filter, Type) else filter
|
||||||
|
for filter in self.current_filter))
|
||||||
|
super().on_stop()
|
||||||
|
|
||||||
|
Launcher(path=path, args=args).run()
|
||||||
|
|
||||||
# avoiding Launcher reference leak
|
# avoiding Launcher reference leak
|
||||||
# and don't try to do something with widgets after window closed
|
# and don't try to do something with widgets after window closed
|
||||||
@@ -360,16 +423,14 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
|||||||
|
|
||||||
path = args.get("Patch|Game|Component|url", None)
|
path = args.get("Patch|Game|Component|url", None)
|
||||||
if path is not None:
|
if path is not None:
|
||||||
if path.startswith("archipelago://"):
|
if not path.startswith("archipelago://"):
|
||||||
handle_uri(path, args.get("args", ()))
|
file, component = identify(path)
|
||||||
return
|
if file:
|
||||||
file, component = identify(path)
|
args['file'] = file
|
||||||
if file:
|
if component:
|
||||||
args['file'] = file
|
args['component'] = component
|
||||||
if component:
|
if not component:
|
||||||
args['component'] = component
|
logging.warning(f"Could not identify Component responsible for {path}")
|
||||||
if not component:
|
|
||||||
logging.warning(f"Could not identify Component responsible for {path}")
|
|
||||||
|
|
||||||
if args["update_settings"]:
|
if args["update_settings"]:
|
||||||
update_settings()
|
update_settings()
|
||||||
@@ -378,7 +439,7 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
|||||||
elif "component" in args:
|
elif "component" in args:
|
||||||
run_component(args["component"], *args["args"])
|
run_component(args["component"], *args["args"])
|
||||||
elif not args["update_settings"]:
|
elif not args["update_settings"]:
|
||||||
run_gui()
|
run_gui(path, args.get("args", ()))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
@@ -400,6 +461,7 @@ if __name__ == '__main__':
|
|||||||
main(parser.parse_args())
|
main(parser.parse_args())
|
||||||
|
|
||||||
from worlds.LauncherComponents import processes
|
from worlds.LauncherComponents import processes
|
||||||
|
|
||||||
for process in processes:
|
for process in processes:
|
||||||
# we await all child processes to close before we tear down the process host
|
# we await all child processes to close before we tear down the process host
|
||||||
# this makes it feel like each one is its own program, as the Launcher is closed now
|
# this makes it feel like each one is its own program, as the Launcher is closed now
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import typing
|
|||||||
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
||||||
server_loop)
|
server_loop)
|
||||||
from NetUtils import ClientStatus
|
from NetUtils import ClientStatus
|
||||||
|
from worlds.ladx import LinksAwakeningWorld
|
||||||
from worlds.ladx.Common import BASE_ID as LABaseID
|
from worlds.ladx.Common import BASE_ID as LABaseID
|
||||||
from worlds.ladx.GpsTracker import GpsTracker
|
from worlds.ladx.GpsTracker import GpsTracker
|
||||||
from worlds.ladx.TrackerConsts import storage_key
|
from worlds.ladx.TrackerConsts import storage_key
|
||||||
@@ -139,7 +140,7 @@ class RAGameboy():
|
|||||||
def set_checks_range(self, checks_start, checks_size):
|
def set_checks_range(self, checks_start, checks_size):
|
||||||
self.checks_start = checks_start
|
self.checks_start = checks_start
|
||||||
self.checks_size = checks_size
|
self.checks_size = checks_size
|
||||||
|
|
||||||
def set_location_range(self, location_start, location_size, critical_addresses):
|
def set_location_range(self, location_start, location_size, critical_addresses):
|
||||||
self.location_start = location_start
|
self.location_start = location_start
|
||||||
self.location_size = location_size
|
self.location_size = location_size
|
||||||
@@ -237,7 +238,7 @@ class RAGameboy():
|
|||||||
self.cache[start:start + len(hram_block)] = hram_block
|
self.cache[start:start + len(hram_block)] = hram_block
|
||||||
|
|
||||||
self.last_cache_read = time.time()
|
self.last_cache_read = time.time()
|
||||||
|
|
||||||
async def read_memory_block(self, address: int, size: int):
|
async def read_memory_block(self, address: int, size: int):
|
||||||
block = bytearray()
|
block = bytearray()
|
||||||
remaining_size = size
|
remaining_size = size
|
||||||
@@ -245,7 +246,7 @@ class RAGameboy():
|
|||||||
chunk = await self.async_read_memory(address + len(block), remaining_size)
|
chunk = await self.async_read_memory(address + len(block), remaining_size)
|
||||||
remaining_size -= len(chunk)
|
remaining_size -= len(chunk)
|
||||||
block += chunk
|
block += chunk
|
||||||
|
|
||||||
return block
|
return block
|
||||||
|
|
||||||
async def read_memory_cache(self, addresses):
|
async def read_memory_cache(self, addresses):
|
||||||
@@ -514,8 +515,8 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
magpie_task = None
|
magpie_task = None
|
||||||
won = False
|
won = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def slot_storage_key(self):
|
def slot_storage_key(self):
|
||||||
return f"{self.slot_info[self.slot].name}_{storage_key}"
|
return f"{self.slot_info[self.slot].name}_{storage_key}"
|
||||||
|
|
||||||
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
|
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
|
||||||
@@ -529,9 +530,7 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
|
|
||||||
def run_gui(self) -> None:
|
def run_gui(self) -> None:
|
||||||
import webbrowser
|
import webbrowser
|
||||||
import kvui
|
from kvui import GameManager, ImageButton
|
||||||
from kvui import Button, GameManager
|
|
||||||
from kivy.uix.image import Image
|
|
||||||
|
|
||||||
class LADXManager(GameManager):
|
class LADXManager(GameManager):
|
||||||
logging_pairs = [
|
logging_pairs = [
|
||||||
@@ -544,21 +543,15 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
b = super().build()
|
b = super().build()
|
||||||
|
|
||||||
if self.ctx.magpie_enabled:
|
if self.ctx.magpie_enabled:
|
||||||
button = Button(text="", size=(30, 30), size_hint_x=None,
|
button = ImageButton(texture=magpie_logo(), fit_mode="cover", image_size=(32, 32), size_hint_x=None,
|
||||||
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
||||||
image = Image(size=(16, 16), texture=magpie_logo())
|
|
||||||
button.add_widget(image)
|
|
||||||
|
|
||||||
def set_center(_, center):
|
|
||||||
image.center = center
|
|
||||||
button.bind(center=set_center)
|
|
||||||
|
|
||||||
self.connect_layout.add_widget(button)
|
self.connect_layout.add_widget(button)
|
||||||
|
|
||||||
return b
|
return b
|
||||||
|
|
||||||
self.ui = LADXManager(self)
|
self.ui = LADXManager(self)
|
||||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||||
|
|
||||||
async def send_new_entrances(self, entrances: typing.Dict[str, str]):
|
async def send_new_entrances(self, entrances: typing.Dict[str, str]):
|
||||||
# Store the entrances we find on the server for future sessions
|
# Store the entrances we find on the server for future sessions
|
||||||
message = [{
|
message = [{
|
||||||
@@ -597,12 +590,12 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
logger.info("victory!")
|
logger.info("victory!")
|
||||||
await self.send_msgs(message)
|
await self.send_msgs(message)
|
||||||
self.won = True
|
self.won = True
|
||||||
|
|
||||||
async def request_found_entrances(self):
|
async def request_found_entrances(self):
|
||||||
await self.send_msgs([{"cmd": "Get", "keys": [self.slot_storage_key]}])
|
await self.send_msgs([{"cmd": "Get", "keys": [self.slot_storage_key]}])
|
||||||
|
|
||||||
# Ask for updates so that players can co-op entrances in a seed
|
# Ask for updates so that players can co-op entrances in a seed
|
||||||
await self.send_msgs([{"cmd": "SetNotify", "keys": [self.slot_storage_key]}])
|
await self.send_msgs([{"cmd": "SetNotify", "keys": [self.slot_storage_key]}])
|
||||||
|
|
||||||
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
|
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
|
||||||
if self.ENABLE_DEATHLINK:
|
if self.ENABLE_DEATHLINK:
|
||||||
@@ -638,12 +631,18 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
if cmd == "Connected":
|
if cmd == "Connected":
|
||||||
self.game = self.slot_info[self.slot].game
|
self.game = self.slot_info[self.slot].game
|
||||||
self.slot_data = args.get("slot_data", {})
|
self.slot_data = args.get("slot_data", {})
|
||||||
|
# This is sent to magpie over local websocket to make its own connection
|
||||||
|
self.slot_data.update({
|
||||||
|
"server_address": self.server_address,
|
||||||
|
"slot_name": self.player_names[self.slot],
|
||||||
|
"password": self.password,
|
||||||
|
})
|
||||||
|
|
||||||
# TODO - use watcher_event
|
# TODO - use watcher_event
|
||||||
if cmd == "ReceivedItems":
|
if cmd == "ReceivedItems":
|
||||||
for index, item in enumerate(args["items"], start=args["index"]):
|
for index, item in enumerate(args["items"], start=args["index"]):
|
||||||
self.client.recvd_checks[index] = item
|
self.client.recvd_checks[index] = item
|
||||||
|
|
||||||
if cmd == "Retrieved" and self.magpie_enabled and self.slot_storage_key in args["keys"]:
|
if cmd == "Retrieved" and self.magpie_enabled and self.slot_storage_key in args["keys"]:
|
||||||
self.client.gps_tracker.receive_found_entrances(args["keys"][self.slot_storage_key])
|
self.client.gps_tracker.receive_found_entrances(args["keys"][self.slot_storage_key])
|
||||||
|
|
||||||
@@ -722,8 +721,10 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
try:
|
try:
|
||||||
self.magpie.set_checks(self.client.tracker.all_checks)
|
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||||
await self.magpie.set_item_tracker(self.client.item_tracker)
|
await self.magpie.set_item_tracker(self.client.item_tracker)
|
||||||
self.magpie.slot_data = self.slot_data
|
if self.slot_data and "slot_data" in self.magpie.features and not self.magpie.has_sent_slot_data:
|
||||||
|
self.magpie.slot_data = self.slot_data
|
||||||
|
await self.magpie.send_slot_data()
|
||||||
|
|
||||||
if self.client.gps_tracker.needs_found_entrances:
|
if self.client.gps_tracker.needs_found_entrances:
|
||||||
await self.request_found_entrances()
|
await self.request_found_entrances()
|
||||||
self.client.gps_tracker.needs_found_entrances = False
|
self.client.gps_tracker.needs_found_entrances = False
|
||||||
@@ -741,8 +742,8 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
await asyncio.sleep(1.0)
|
await asyncio.sleep(1.0)
|
||||||
|
|
||||||
def run_game(romfile: str) -> None:
|
def run_game(romfile: str) -> None:
|
||||||
auto_start = typing.cast(typing.Union[bool, str],
|
auto_start = LinksAwakeningWorld.settings.rom_start
|
||||||
Utils.get_options()["ladx_options"].get("rom_start", True))
|
|
||||||
if auto_start is True:
|
if auto_start is True:
|
||||||
import webbrowser
|
import webbrowser
|
||||||
webbrowser.open(romfile)
|
webbrowser.open(romfile)
|
||||||
|
|||||||
11
Main.py
11
Main.py
@@ -81,7 +81,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
del item_digits, location_digits, item_count, location_count
|
del item_digits, location_digits, item_count, location_count
|
||||||
|
|
||||||
# This assertion method should not be necessary to run if we are not outputting any multidata.
|
# This assertion method should not be necessary to run if we are not outputting any multidata.
|
||||||
if not args.skip_output:
|
if not args.skip_output and not args.spoiler_only:
|
||||||
AutoWorld.call_stage(multiworld, "assert_generate")
|
AutoWorld.call_stage(multiworld, "assert_generate")
|
||||||
|
|
||||||
AutoWorld.call_all(multiworld, "generate_early")
|
AutoWorld.call_all(multiworld, "generate_early")
|
||||||
@@ -224,6 +224,15 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
logger.info(f'Beginning output...')
|
logger.info(f'Beginning output...')
|
||||||
outfilebase = 'AP_' + multiworld.seed_name
|
outfilebase = 'AP_' + multiworld.seed_name
|
||||||
|
|
||||||
|
if args.spoiler_only:
|
||||||
|
if args.spoiler > 1:
|
||||||
|
logger.info('Calculating playthrough.')
|
||||||
|
multiworld.spoiler.create_playthrough(create_paths=args.spoiler > 2)
|
||||||
|
|
||||||
|
multiworld.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
|
||||||
|
logger.info('Done. Skipped multidata modification. Total time: %s', time.perf_counter() - start)
|
||||||
|
return multiworld
|
||||||
|
|
||||||
output = tempfile.TemporaryDirectory()
|
output = tempfile.TemporaryDirectory()
|
||||||
with output as temp_dir:
|
with output as temp_dir:
|
||||||
output_players = [player for player in multiworld.player_ids if AutoWorld.World.generate_output.__code__
|
output_players = [player for player in multiworld.player_ids if AutoWorld.World.generate_output.__code__
|
||||||
|
|||||||
@@ -66,9 +66,13 @@ def pop_from_container(container, value):
|
|||||||
return container
|
return container
|
||||||
|
|
||||||
|
|
||||||
def update_dict(dictionary, entries):
|
def update_container_unique(container, entries):
|
||||||
dictionary.update(entries)
|
if isinstance(container, list):
|
||||||
return dictionary
|
existing_container_as_set = set(container)
|
||||||
|
container.extend([entry for entry in entries if entry not in existing_container_as_set])
|
||||||
|
else:
|
||||||
|
container.update(entries)
|
||||||
|
return container
|
||||||
|
|
||||||
|
|
||||||
def queue_gc():
|
def queue_gc():
|
||||||
@@ -109,7 +113,7 @@ modify_functions = {
|
|||||||
# lists/dicts:
|
# lists/dicts:
|
||||||
"remove": remove_from_list,
|
"remove": remove_from_list,
|
||||||
"pop": pop_from_container,
|
"pop": pop_from_container,
|
||||||
"update": update_dict,
|
"update": update_container_unique,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1978,11 +1982,13 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
new_hint = new_hint.re_prioritize(ctx, status)
|
new_hint = new_hint.re_prioritize(ctx, status)
|
||||||
if hint == new_hint:
|
if hint == new_hint:
|
||||||
return
|
return
|
||||||
ctx.replace_hint(client.team, hint.finding_player, hint, new_hint)
|
|
||||||
ctx.replace_hint(client.team, hint.receiving_player, hint, new_hint)
|
concerning_slots = ctx.slot_set(hint.receiving_player) | {hint.finding_player}
|
||||||
|
for slot in concerning_slots:
|
||||||
|
ctx.replace_hint(client.team, slot, hint, new_hint)
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.on_changed_hints(client.team, hint.finding_player)
|
for slot in concerning_slots:
|
||||||
ctx.on_changed_hints(client.team, hint.receiving_player)
|
ctx.on_changed_hints(client.team, slot)
|
||||||
|
|
||||||
elif cmd == 'StatusUpdate':
|
elif cmd == 'StatusUpdate':
|
||||||
update_client_status(ctx, client, args["status"])
|
update_client_status(ctx, client, args["status"])
|
||||||
@@ -2037,7 +2043,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
value = func(value, operation["value"])
|
value = func(value, operation["value"])
|
||||||
ctx.stored_data[args["key"]] = args["value"] = value
|
ctx.stored_data[args["key"]] = args["value"] = value
|
||||||
targets = set(ctx.stored_data_notification_clients[args["key"]])
|
targets = set(ctx.stored_data_notification_clients[args["key"]])
|
||||||
if args.get("want_reply", True):
|
if args.get("want_reply", False):
|
||||||
targets.add(client)
|
targets.add(client)
|
||||||
if targets:
|
if targets:
|
||||||
ctx.broadcast(targets, [args])
|
ctx.broadcast(targets, [args])
|
||||||
|
|||||||
@@ -214,17 +214,11 @@ class WargrooveContext(CommonContext):
|
|||||||
def run_gui(self):
|
def run_gui(self):
|
||||||
"""Import kivy UI system and start running it as self.ui_task."""
|
"""Import kivy UI system and start running it as self.ui_task."""
|
||||||
from kvui import GameManager, HoverBehavior, ServerToolTip
|
from kvui import GameManager, HoverBehavior, ServerToolTip
|
||||||
from kivy.uix.tabbedpanel import TabbedPanelItem
|
from kivymd.uix.tab import MDTabsItem, MDTabsItemText
|
||||||
from kivy.lang import Builder
|
from kivy.lang import Builder
|
||||||
from kivy.uix.button import Button
|
|
||||||
from kivy.uix.togglebutton import ToggleButton
|
from kivy.uix.togglebutton import ToggleButton
|
||||||
from kivy.uix.boxlayout import BoxLayout
|
from kivy.uix.boxlayout import BoxLayout
|
||||||
from kivy.uix.gridlayout import GridLayout
|
|
||||||
from kivy.uix.image import AsyncImage, Image
|
|
||||||
from kivy.uix.stacklayout import StackLayout
|
|
||||||
from kivy.uix.label import Label
|
from kivy.uix.label import Label
|
||||||
from kivy.properties import ColorProperty
|
|
||||||
from kivy.uix.image import Image
|
|
||||||
import pkgutil
|
import pkgutil
|
||||||
|
|
||||||
class TrackerLayout(BoxLayout):
|
class TrackerLayout(BoxLayout):
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from threading import Event, Thread
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID
|
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 Utils import restricted_loads
|
||||||
from .locker import Locker, AlreadyRunningException
|
from .locker import Locker, AlreadyRunningException
|
||||||
@@ -36,12 +36,21 @@ def handle_generation_failure(result: BaseException):
|
|||||||
logging.exception(e)
|
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):
|
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||||
try:
|
try:
|
||||||
meta = json.loads(generation.meta)
|
meta = json.loads(generation.meta)
|
||||||
options = restricted_loads(generation.options)
|
options = restricted_loads(generation.options)
|
||||||
logging.info(f"Generating {generation.id} for {len(options)} players")
|
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,
|
{"meta": meta,
|
||||||
"sid": generation.id,
|
"sid": generation.id,
|
||||||
"owner": generation.owner},
|
"owner": generation.owner},
|
||||||
@@ -55,6 +64,10 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
|||||||
|
|
||||||
|
|
||||||
def init_generator(config: dict[str, Any]) -> None:
|
def init_generator(config: dict[str, Any]) -> None:
|
||||||
|
from setproctitle import setproctitle
|
||||||
|
|
||||||
|
setproctitle("Generator (idle)")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import resource
|
import resource
|
||||||
except ModuleNotFoundError:
|
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,
|
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||||
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
||||||
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
|
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
|
||||||
|
from setproctitle import setproctitle
|
||||||
|
|
||||||
|
setproctitle(name)
|
||||||
Utils.init_logging(name)
|
Utils.init_logging(name)
|
||||||
try:
|
try:
|
||||||
import resource
|
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.")
|
raise Exception("Worlds system should not be loaded in the custom server.")
|
||||||
|
|
||||||
import gc
|
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
|
gc.collect() # free intermediate objects used during setup
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
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
|
assert ctx.server is None
|
||||||
try:
|
try:
|
||||||
ctx.server = websockets.serve(
|
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
|
await ctx.server
|
||||||
except OSError: # likely port in use
|
except OSError: # likely port in use
|
||||||
ctx.server = websockets.serve(
|
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
|
await ctx.server
|
||||||
port = 0
|
port = 0
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
|
|||||||
{"bosses", "items", "connections", "texts"}))
|
{"bosses", "items", "connections", "texts"}))
|
||||||
erargs.skip_prog_balancing = False
|
erargs.skip_prog_balancing = False
|
||||||
erargs.skip_output = False
|
erargs.skip_output = False
|
||||||
|
erargs.spoiler_only = False
|
||||||
erargs.csv_output = False
|
erargs.csv_output = False
|
||||||
|
|
||||||
name_counter = Counter()
|
name_counter = Counter()
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ bokeh>=3.6.3
|
|||||||
markupsafe>=3.0.2
|
markupsafe>=3.0.2
|
||||||
Markdown>=3.7
|
Markdown>=3.7
|
||||||
mdx-breakless-lists>=1.0.1
|
mdx-breakless-lists>=1.0.1
|
||||||
|
setproctitle>=1.3.5
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ window.addEventListener('load', () => {
|
|||||||
showdown.setOption('strikethrough', true);
|
showdown.setOption('strikethrough', true);
|
||||||
showdown.setOption('literalMidWordUnderscores', true);
|
showdown.setOption('literalMidWordUnderscores', true);
|
||||||
gameInfo.innerHTML += (new showdown.Converter()).makeHtml(results);
|
gameInfo.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||||
adjustHeaderWidth();
|
|
||||||
|
|
||||||
// Reset the id of all header divs to something nicer
|
// Reset the id of all header divs to something nicer
|
||||||
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
|
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('file-input').addEventListener('change', () => {
|
||||||
document.getElementById('host-game-form').submit();
|
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('literalMidWordUnderscores', true);
|
||||||
showdown.setOption('disableForced4SpacesIndentedSublists', true);
|
showdown.setOption('disableForced4SpacesIndentedSublists', true);
|
||||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||||
adjustHeaderWidth();
|
|
||||||
|
|
||||||
const title = document.querySelector('h1')
|
const title = document.querySelector('h1')
|
||||||
if (title) {
|
if (title) {
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ html{
|
|||||||
|
|
||||||
body{
|
body{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: calc(100vh - 110px);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
a{
|
a{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
{% import "macros.html" as macros %}
|
{% import "macros.html" as macros %}
|
||||||
|
{% set show_footer = True %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Page Not Found (404)</title>
|
<title>Page Not Found (404)</title>
|
||||||
@@ -13,5 +14,4 @@
|
|||||||
The page you're looking for doesn't exist.<br />
|
The page you're looking for doesn't exist.<br />
|
||||||
<a href="/">Click here to return to safety.</a>
|
<a href="/">Click here to return to safety.</a>
|
||||||
</div>
|
</div>
|
||||||
{% include 'islandFooter.html' %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
|
{% set show_footer = True %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Upload Multidata</title>
|
<title>Upload Multidata</title>
|
||||||
@@ -27,6 +28,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'islandFooter.html' %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
|
{% set show_footer = True %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Archipelago</title>
|
<title>Archipelago</title>
|
||||||
@@ -57,5 +58,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'islandFooter.html' %}
|
|
||||||
{% endblock %}
|
{% 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/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/cookieNotice.css") }}" />
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/globalStyles.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>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/cookieNotice.js") }}"></script>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Archipelago</title>
|
<title>Archipelago</title>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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() %}
|
{% block body %}
|
||||||
{% if messages %}
|
{% endblock %}
|
||||||
<div>
|
</main>
|
||||||
{% for message in messages | unique %}
|
|
||||||
<div class="user-message">{{ message }}</div>
|
{% if show_footer %}
|
||||||
{% endfor %}
|
{% include "islandFooter.html" %}
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
{% import "macros.html" as macros %}
|
{% import "macros.html" as macros %}
|
||||||
|
{% set show_footer = True %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Generation failed, please retry.</title>
|
<title>Generation failed, please retry.</title>
|
||||||
@@ -15,5 +16,4 @@
|
|||||||
{{ seed_error }}
|
{{ seed_error }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'islandFooter.html' %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
|
{% set show_footer = True %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Start Playing</title>
|
<title>Start Playing</title>
|
||||||
@@ -26,6 +27,4 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'islandFooter.html' %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
{% import "macros.html" as macros %}
|
{% import "macros.html" as macros %}
|
||||||
|
{% set show_footer = True %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>View Seed {{ seed.id|suuid }}</title>
|
<title>View Seed {{ seed.id|suuid }}</title>
|
||||||
@@ -50,5 +51,4 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'islandFooter.html' %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
{% import "macros.html" as macros %}
|
{% import "macros.html" as macros %}
|
||||||
|
{% set show_footer = True %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Generation in Progress</title>
|
<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") }}"/>
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/waitSeed.css") }}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -15,5 +18,34 @@
|
|||||||
Waiting for game to generate, this page auto-refreshes to check.
|
Waiting for game to generate, this page auto-refreshes to check.
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -14,23 +14,51 @@
|
|||||||
salmon: "FA8072" # typically trap item
|
salmon: "FA8072" # typically trap item
|
||||||
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
|
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
|
||||||
orange: "FF7700" # Used for command echo
|
orange: "FF7700" # Used for command echo
|
||||||
<Label>:
|
# KivyMD theming parameters
|
||||||
color: "FFFFFF"
|
theme_style: "Dark" # Light/Dark
|
||||||
<TabbedPanel>:
|
primary_palette: "Green" # Many options
|
||||||
tab_width: root.width / app.tab_count
|
dynamic_scheme_name: "TONAL_SPOT"
|
||||||
|
dynamic_scheme_contrast: 0.0
|
||||||
|
<MDLabel>:
|
||||||
|
color: self.theme_cls.primaryColor
|
||||||
<TooltipLabel>:
|
<TooltipLabel>:
|
||||||
text_size: self.width, None
|
adaptive_height: True
|
||||||
size_hint_y: None
|
|
||||||
height: self.texture_size[1]
|
|
||||||
font_size: dp(20)
|
font_size: dp(20)
|
||||||
markup: True
|
markup: True
|
||||||
|
halign: "left"
|
||||||
<SelectableLabel>:
|
<SelectableLabel>:
|
||||||
|
size_hint: 1, None
|
||||||
canvas.before:
|
canvas.before:
|
||||||
Color:
|
Color:
|
||||||
rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1)
|
rgba: (.0, 0.9, .1, .3) if self.selected else self.theme_cls.surfaceContainerLowColor
|
||||||
Rectangle:
|
Rectangle:
|
||||||
size: self.size
|
size: self.size
|
||||||
pos: self.pos
|
pos: self.pos
|
||||||
|
<MarkupDropdownItem>
|
||||||
|
orientation: "vertical"
|
||||||
|
|
||||||
|
MDLabel:
|
||||||
|
text: root.text
|
||||||
|
valign: "center"
|
||||||
|
padding_x: "12dp"
|
||||||
|
shorten: True
|
||||||
|
shorten_from: "right"
|
||||||
|
theme_text_color: "Custom"
|
||||||
|
markup: True
|
||||||
|
text_color:
|
||||||
|
app.theme_cls.onSurfaceVariantColor \
|
||||||
|
if not root.text_color else \
|
||||||
|
root.text_color
|
||||||
|
|
||||||
|
MDDivider:
|
||||||
|
md_bg_color:
|
||||||
|
( \
|
||||||
|
app.theme_cls.outlineVariantColor \
|
||||||
|
if not root.divider_color \
|
||||||
|
else root.divider_color \
|
||||||
|
) \
|
||||||
|
if root.divider else \
|
||||||
|
(0, 0, 0, 0)
|
||||||
<UILog>:
|
<UILog>:
|
||||||
messages: 1000 # amount of messages stored in client logs.
|
messages: 1000 # amount of messages stored in client logs.
|
||||||
cols: 1
|
cols: 1
|
||||||
@@ -49,7 +77,7 @@
|
|||||||
<HintLabel>:
|
<HintLabel>:
|
||||||
canvas.before:
|
canvas.before:
|
||||||
Color:
|
Color:
|
||||||
rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1) if self.striped else (0.18, 0.18, 0.18, 1)
|
rgba: (.0, 0.9, .1, .3) if self.selected else self.theme_cls.surfaceContainerHighColor if self.striped else self.theme_cls.surfaceContainerLowColor
|
||||||
Rectangle:
|
Rectangle:
|
||||||
size: self.size
|
size: self.size
|
||||||
pos: self.pos
|
pos: self.pos
|
||||||
@@ -152,3 +180,16 @@
|
|||||||
height: dp(30)
|
height: dp(30)
|
||||||
multiline: False
|
multiline: False
|
||||||
write_tab: False
|
write_tab: False
|
||||||
|
<ScrollBox>:
|
||||||
|
layout: layout
|
||||||
|
bar_width: "12dp"
|
||||||
|
scroll_wheel_distance: 40
|
||||||
|
do_scroll_x: False
|
||||||
|
scroll_type: ['bars', 'content']
|
||||||
|
|
||||||
|
MDBoxLayout:
|
||||||
|
id: layout
|
||||||
|
orientation: "vertical"
|
||||||
|
spacing: 10
|
||||||
|
size_hint_y: None
|
||||||
|
height: self.minimum_height
|
||||||
|
|||||||
142
data/launcher.kv
Normal file
142
data/launcher.kv
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<LauncherCard>:
|
||||||
|
id: main
|
||||||
|
style: "filled"
|
||||||
|
padding: "4dp"
|
||||||
|
size_hint: 1, None
|
||||||
|
height: "75dp"
|
||||||
|
context_button: context
|
||||||
|
|
||||||
|
MDRelativeLayout:
|
||||||
|
ApAsyncImage:
|
||||||
|
source: main.image
|
||||||
|
size: (48, 48)
|
||||||
|
size_hint_y: 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.75 if main.component.description else 0.65}
|
||||||
|
halign: "center"
|
||||||
|
font_style: "Title"
|
||||||
|
role: "medium"
|
||||||
|
theme_text_color: "Custom"
|
||||||
|
text_color: app.theme_cls.primaryColor
|
||||||
|
|
||||||
|
MDLabel:
|
||||||
|
text: main.component.description
|
||||||
|
pos_hint: {"center_x": 0.5, "center_y": 0.35}
|
||||||
|
halign: "center"
|
||||||
|
role: "small"
|
||||||
|
theme_text_color: "Custom"
|
||||||
|
text_color: app.theme_cls.primaryColor
|
||||||
|
|
||||||
|
MDIconButton:
|
||||||
|
component: main.component
|
||||||
|
icon: "star" if self.component.display_name in app.favorites else "star-outline"
|
||||||
|
style: "standard"
|
||||||
|
pos_hint:{"center_x": 0.85, "center_y": 0.8}
|
||||||
|
theme_text_color: "Custom"
|
||||||
|
text_color: app.theme_cls.primaryColor
|
||||||
|
on_release: app.set_favorite(self)
|
||||||
|
|
||||||
|
MDIconButton:
|
||||||
|
id: context
|
||||||
|
icon: "menu"
|
||||||
|
style: "standard"
|
||||||
|
pos_hint:{"center_x": 0.95, "center_y": 0.8}
|
||||||
|
theme_text_color: "Custom"
|
||||||
|
text_color: app.theme_cls.primaryColor
|
||||||
|
|
||||||
|
MDButton:
|
||||||
|
pos_hint:{"center_x": 0.9, "center_y": 0.25}
|
||||||
|
size_hint_y: None
|
||||||
|
height: "25dp"
|
||||||
|
component: main.component
|
||||||
|
on_release: app.component_action(self)
|
||||||
|
|
||||||
|
MDButtonText:
|
||||||
|
text: "Open"
|
||||||
|
|
||||||
|
|
||||||
|
#:import Type worlds.LauncherComponents.Type
|
||||||
|
MDFloatLayout:
|
||||||
|
id: top_screen
|
||||||
|
|
||||||
|
MDGridLayout:
|
||||||
|
id: grid
|
||||||
|
cols: 2
|
||||||
|
spacing: "5dp"
|
||||||
|
padding: "10dp"
|
||||||
|
|
||||||
|
MDGridLayout:
|
||||||
|
id: navigation
|
||||||
|
cols: 1
|
||||||
|
size_hint_x: 0.25
|
||||||
|
|
||||||
|
MDButton:
|
||||||
|
id: all
|
||||||
|
style: "text"
|
||||||
|
type: (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
|
||||||
|
on_release: app.filter_clients(self)
|
||||||
|
|
||||||
|
MDButtonIcon:
|
||||||
|
icon: "asterisk"
|
||||||
|
MDButtonText:
|
||||||
|
text: "All"
|
||||||
|
MDButton:
|
||||||
|
id: client
|
||||||
|
style: "text"
|
||||||
|
type: (Type.CLIENT, )
|
||||||
|
on_release: app.filter_clients(self)
|
||||||
|
|
||||||
|
MDButtonIcon:
|
||||||
|
icon: "controller"
|
||||||
|
MDButtonText:
|
||||||
|
text: "Client"
|
||||||
|
MDButton:
|
||||||
|
id: Tool
|
||||||
|
style: "text"
|
||||||
|
type: (Type.TOOL, )
|
||||||
|
on_release: app.filter_clients(self)
|
||||||
|
|
||||||
|
MDButtonIcon:
|
||||||
|
icon: "desktop-classic"
|
||||||
|
MDButtonText:
|
||||||
|
text: "Tool"
|
||||||
|
MDButton:
|
||||||
|
id: adjuster
|
||||||
|
style: "text"
|
||||||
|
type: (Type.ADJUSTER, )
|
||||||
|
on_release: app.filter_clients(self)
|
||||||
|
|
||||||
|
MDButtonIcon:
|
||||||
|
icon: "wrench"
|
||||||
|
MDButtonText:
|
||||||
|
text: "Adjuster"
|
||||||
|
MDButton:
|
||||||
|
id: misc
|
||||||
|
style: "text"
|
||||||
|
type: (Type.MISC, )
|
||||||
|
on_release: app.filter_clients(self)
|
||||||
|
|
||||||
|
MDButtonIcon:
|
||||||
|
icon: "dots-horizontal-circle-outline"
|
||||||
|
MDButtonText:
|
||||||
|
text: "Misc"
|
||||||
|
|
||||||
|
MDButton:
|
||||||
|
id: favorites
|
||||||
|
style: "text"
|
||||||
|
type: ("favorites", )
|
||||||
|
on_release: app.filter_clients(self)
|
||||||
|
|
||||||
|
MDButtonIcon:
|
||||||
|
icon: "star"
|
||||||
|
MDButtonText:
|
||||||
|
text: "Favorites"
|
||||||
|
|
||||||
|
MDNavigationDrawerDivider:
|
||||||
|
|
||||||
|
|
||||||
|
ScrollBox:
|
||||||
|
id: button_layout
|
||||||
@@ -60,7 +60,7 @@ These are "nice to have" features for a client, but they are not strictly requir
|
|||||||
if possible.
|
if possible.
|
||||||
|
|
||||||
* If your client appears in the Archipelago Launcher, you may define an icon for it that differentiates it from
|
* 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
|
## 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 locations, added to your regions
|
||||||
* A non-zero number of items **equal** to the number of locations, added to the multiworld itempool
|
* 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.
|
* 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
|
### 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
|
* If you need to place specific items, there are multiple ways to do so, but they should not be added to the
|
||||||
multiworld itempool.
|
multiworld itempool.
|
||||||
* It is not allowed to use `eval` for most reasons, chiefly due to security concerns.
|
* 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.
|
* It is discouraged to use PyYAML (i.e. `yaml.load`) directly due to security concerns.
|
||||||
* When possible, use `Utils.yaml_load` instead, as this defaults to the safe loader.
|
* 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),
|
* 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.
|
do **not** use `=` as this will overwrite all elements for all games in the seed.
|
||||||
* Instead, use `append`, `extend`, or `+=`.
|
* Instead, use `append`, `extend`, or `+=`.
|
||||||
|
|
||||||
### Notable Caveats
|
### 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.
|
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.
|
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.
|
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.
|
||||||
|
|||||||
@@ -470,7 +470,7 @@ The following operations can be applied to a datastorage key
|
|||||||
| right_shift | Applies a bitwise right-shift to the current value of the key by `value`. |
|
| right_shift | Applies a bitwise right-shift to the current value of the key by `value`. |
|
||||||
| remove | List only: removes the first instance of `value` found in the list. |
|
| remove | List only: removes the first instance of `value` found in the list. |
|
||||||
| pop | List or Dict: for lists it will remove the index of the `value` given. for dicts it removes the element with the specified key of `value`. |
|
| pop | List or Dict: for lists it will remove the index of the `value` given. for dicts it removes the element with the specified key of `value`. |
|
||||||
| update | Dict only: Updates the dictionary with the specified elements given in `value` creating new keys, or updating old ones if they previously existed. |
|
| update | List or Dict: Adds the elements of `value` to the container if they weren't already present. In the case of a Dict, already present keys will have their corresponding values updated. |
|
||||||
|
|
||||||
### SetNotify
|
### SetNotify
|
||||||
Used to register your current session for receiving all [SetReply](#SetReply) packages of certain keys to allow your client to keep track of changes.
|
Used to register your current session for receiving all [SetReply](#SetReply) packages of certain keys to allow your client to keep track of changes.
|
||||||
|
|||||||
@@ -606,8 +606,8 @@ from .items import get_item_type
|
|||||||
|
|
||||||
def set_rules(self) -> None:
|
def set_rules(self) -> None:
|
||||||
# For some worlds this step can be omitted if either a Logic mixin
|
# 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
|
# (see below) is used or it's easier to apply the rules from data during
|
||||||
# location generation or everything is in generate_basic
|
# location generation
|
||||||
|
|
||||||
# set a simple rule for an region
|
# set a simple rule for an region
|
||||||
set_rule(self.multiworld.get_entrance("Boss Door", self.player),
|
set_rule(self.multiworld.get_entrance("Boss Door", self.player),
|
||||||
|
|||||||
@@ -50,13 +50,15 @@ class EntranceLookup:
|
|||||||
_random: random.Random
|
_random: random.Random
|
||||||
_expands_graph_cache: dict[Entrance, bool]
|
_expands_graph_cache: dict[Entrance, bool]
|
||||||
_coupled: bool
|
_coupled: bool
|
||||||
|
_usable_exits: set[Entrance]
|
||||||
|
|
||||||
def __init__(self, rng: random.Random, coupled: bool):
|
def __init__(self, rng: random.Random, coupled: bool, usable_exits: set[Entrance]):
|
||||||
self.dead_ends = EntranceLookup.GroupLookup()
|
self.dead_ends = EntranceLookup.GroupLookup()
|
||||||
self.others = EntranceLookup.GroupLookup()
|
self.others = EntranceLookup.GroupLookup()
|
||||||
self._random = rng
|
self._random = rng
|
||||||
self._expands_graph_cache = {}
|
self._expands_graph_cache = {}
|
||||||
self._coupled = coupled
|
self._coupled = coupled
|
||||||
|
self._usable_exits = usable_exits
|
||||||
|
|
||||||
def _can_expand_graph(self, entrance: Entrance) -> bool:
|
def _can_expand_graph(self, entrance: Entrance) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -95,7 +97,8 @@ class EntranceLookup:
|
|||||||
# randomizable exits which are not reverse of the incoming entrance.
|
# randomizable exits which are not reverse of the incoming entrance.
|
||||||
# uncoupled mode is an exception because in this case going back in the door you just came in could
|
# uncoupled mode is an exception because in this case going back in the door you just came in could
|
||||||
# actually lead somewhere new
|
# actually lead somewhere new
|
||||||
if not exit_.connected_region and (not self._coupled or exit_.name != entrance.name):
|
if (not exit_.connected_region and (not self._coupled or exit_.name != entrance.name)
|
||||||
|
and exit_ in self._usable_exits):
|
||||||
self._expands_graph_cache[entrance] = True
|
self._expands_graph_cache[entrance] = True
|
||||||
return True
|
return True
|
||||||
elif exit_.connected_region and exit_.connected_region not in visited:
|
elif exit_.connected_region and exit_.connected_region not in visited:
|
||||||
@@ -333,7 +336,6 @@ def randomize_entrances(
|
|||||||
|
|
||||||
start_time = time.perf_counter()
|
start_time = time.perf_counter()
|
||||||
er_state = ERPlacementState(world, coupled)
|
er_state = ERPlacementState(world, coupled)
|
||||||
entrance_lookup = EntranceLookup(world.random, coupled)
|
|
||||||
# similar to fill, skip validity checks on entrances if the game is beatable on minimal accessibility
|
# similar to fill, skip validity checks on entrances if the game is beatable on minimal accessibility
|
||||||
perform_validity_check = True
|
perform_validity_check = True
|
||||||
|
|
||||||
@@ -349,6 +351,7 @@ def randomize_entrances(
|
|||||||
|
|
||||||
# used when membership checks are needed on the exit list, e.g. speculative sweep
|
# used when membership checks are needed on the exit list, e.g. speculative sweep
|
||||||
exits_set = set(exits)
|
exits_set = set(exits)
|
||||||
|
entrance_lookup = EntranceLookup(world.random, coupled, exits_set)
|
||||||
for entrance in er_targets:
|
for entrance in er_targets:
|
||||||
entrance_lookup.add(entrance)
|
entrance_lookup.add(entrance)
|
||||||
|
|
||||||
|
|||||||
467
kvui.py
467
kvui.py
@@ -35,8 +35,7 @@ from kivy.config import Config
|
|||||||
Config.set("input", "mouse", "mouse,disable_multitouch")
|
Config.set("input", "mouse", "mouse,disable_multitouch")
|
||||||
Config.set("kivy", "exit_on_escape", "0")
|
Config.set("kivy", "exit_on_escape", "0")
|
||||||
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
||||||
|
from kivymd.uix.divider import MDDivider
|
||||||
from kivy.app import App
|
|
||||||
from kivy.core.window import Window
|
from kivy.core.window import Window
|
||||||
from kivy.core.clipboard import Clipboard
|
from kivy.core.clipboard import Clipboard
|
||||||
from kivy.core.text.markup import MarkupLabel
|
from kivy.core.text.markup import MarkupLabel
|
||||||
@@ -46,30 +45,32 @@ from kivy.clock import Clock
|
|||||||
from kivy.factory import Factory
|
from kivy.factory import Factory
|
||||||
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
|
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
|
||||||
from kivy.metrics import dp
|
from kivy.metrics import dp
|
||||||
from kivy.effects.scroll import ScrollEffect
|
|
||||||
from kivy.uix.widget import Widget
|
from kivy.uix.widget import Widget
|
||||||
from kivy.uix.button import Button
|
|
||||||
from kivy.uix.gridlayout import GridLayout
|
|
||||||
from kivy.uix.layout import Layout
|
from kivy.uix.layout import Layout
|
||||||
from kivy.uix.textinput import TextInput
|
|
||||||
from kivy.uix.scrollview import ScrollView
|
|
||||||
from kivy.uix.recycleview import RecycleView
|
|
||||||
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
|
|
||||||
from kivy.uix.boxlayout import BoxLayout
|
|
||||||
from kivy.uix.floatlayout import FloatLayout
|
|
||||||
from kivy.uix.label import Label
|
|
||||||
from kivy.uix.progressbar import ProgressBar
|
|
||||||
from kivy.uix.dropdown import DropDown
|
|
||||||
from kivy.utils import escape_markup
|
from kivy.utils import escape_markup
|
||||||
from kivy.lang import Builder
|
from kivy.lang import Builder
|
||||||
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
||||||
from kivy.uix.behaviors import FocusBehavior
|
from kivy.uix.behaviors import FocusBehavior, ToggleButtonBehavior
|
||||||
from kivy.uix.recycleboxlayout import RecycleBoxLayout
|
from kivy.uix.recycleboxlayout import RecycleBoxLayout
|
||||||
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
|
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
|
||||||
from kivy.animation import Animation
|
from kivy.animation import Animation
|
||||||
from kivy.uix.popup import Popup
|
from kivy.uix.popup import Popup
|
||||||
from kivy.uix.dropdown import DropDown
|
|
||||||
from kivy.uix.image import AsyncImage
|
from kivy.uix.image import AsyncImage
|
||||||
|
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.menu import MDDropdownMenu
|
||||||
|
from kivymd.uix.menu.menu import MDDropdownTextItem
|
||||||
|
from kivymd.uix.dropdownitem import MDDropDownItem, MDDropDownItemText
|
||||||
|
from kivymd.uix.button import MDButton, MDButtonText, MDButtonIcon, MDIconButton
|
||||||
|
from kivymd.uix.label import MDLabel, MDIcon
|
||||||
|
from kivymd.uix.recycleview import MDRecycleView
|
||||||
|
from kivymd.uix.textfield.textfield import MDTextField
|
||||||
|
from kivymd.uix.progressindicator import MDLinearProgressIndicator
|
||||||
|
from kivymd.uix.scrollview import MDScrollView
|
||||||
|
from kivymd.uix.tooltip import MDTooltip, MDTooltipPlain
|
||||||
|
|
||||||
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
|
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
|
||||||
|
|
||||||
@@ -86,6 +87,85 @@ else:
|
|||||||
remove_between_brackets = re.compile(r"\[.*?]")
|
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 = getattr(text_colors, "dynamic_scheme_contrast", 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageIcon(MDButtonIcon, AsyncImage):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(args, kwargs)
|
||||||
|
self.image = ApAsyncImage(**kwargs)
|
||||||
|
self.add_widget(self.image)
|
||||||
|
|
||||||
|
def add_widget(self, widget, index=0, canvas=None):
|
||||||
|
return super(MDIcon, self).add_widget(widget)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageButton(MDIconButton):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
image_args = dict()
|
||||||
|
for kwarg in ("fit_mode", "image_size", "color", "source", "texture"):
|
||||||
|
val = kwargs.pop(kwarg, "None")
|
||||||
|
if val != "None":
|
||||||
|
image_args[kwarg.replace("image_", "")] = val
|
||||||
|
super().__init__()
|
||||||
|
self.image = ApAsyncImage(**image_args)
|
||||||
|
|
||||||
|
def set_center(button, center):
|
||||||
|
self.image.center_x = self.center_x
|
||||||
|
self.image.center_y = self.center_y
|
||||||
|
|
||||||
|
self.bind(center=set_center)
|
||||||
|
self.add_widget(self.image)
|
||||||
|
|
||||||
|
def add_widget(self, widget, index=0, canvas=None):
|
||||||
|
return super(MDIcon, self).add_widget(widget)
|
||||||
|
|
||||||
|
|
||||||
|
class ScrollBox(MDScrollView):
|
||||||
|
layout: MDBoxLayout = ObjectProperty(None)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# thanks kivymd
|
||||||
|
class ToggleButton(MDButton, ToggleButtonBehavior):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ToggleButton, self).__init__(*args, **kwargs)
|
||||||
|
self.bind(state=self._update_bg)
|
||||||
|
|
||||||
|
def _update_bg(self, _, state: str):
|
||||||
|
if self.disabled:
|
||||||
|
return
|
||||||
|
if self.theme_bg_color == "Primary":
|
||||||
|
self.theme_bg_color = "Custom"
|
||||||
|
|
||||||
|
if state == "down":
|
||||||
|
self.md_bg_color = self.theme_cls.primaryColor
|
||||||
|
for child in self.children:
|
||||||
|
if child.theme_text_color == "Primary":
|
||||||
|
child.theme_text_color = "Custom"
|
||||||
|
if child.theme_icon_color == "Primary":
|
||||||
|
child.theme_icon_color = "Custom"
|
||||||
|
child.text_color = self.theme_cls.onPrimaryColor
|
||||||
|
child.icon_color = self.theme_cls.onPrimaryColor
|
||||||
|
else:
|
||||||
|
self.md_bg_color = self.theme_cls.surfaceContainerLowestColor
|
||||||
|
for child in self.children:
|
||||||
|
if child.theme_text_color == "Primary":
|
||||||
|
child.theme_text_color = "Custom"
|
||||||
|
if child.theme_icon_color == "Primary":
|
||||||
|
child.theme_icon_color = "Custom"
|
||||||
|
child.text_color = self.theme_cls.primaryColor
|
||||||
|
child.icon_color = self.theme_cls.primaryColor
|
||||||
|
|
||||||
|
|
||||||
# I was surprised to find this didn't already exist in kivy :(
|
# I was surprised to find this didn't already exist in kivy :(
|
||||||
class HoverBehavior(object):
|
class HoverBehavior(object):
|
||||||
"""originally from https://stackoverflow.com/a/605348110"""
|
"""originally from https://stackoverflow.com/a/605348110"""
|
||||||
@@ -125,7 +205,7 @@ class HoverBehavior(object):
|
|||||||
Factory.register("HoverBehavior", HoverBehavior)
|
Factory.register("HoverBehavior", HoverBehavior)
|
||||||
|
|
||||||
|
|
||||||
class ToolTip(Label):
|
class ToolTip(MDTooltipPlain):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -133,49 +213,30 @@ class ServerToolTip(ToolTip):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ScrollBox(ScrollView):
|
class HovererableLabel(HoverBehavior, MDLabel):
|
||||||
layout: BoxLayout
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.layout = BoxLayout(size_hint_y=None)
|
|
||||||
self.layout.bind(minimum_height=self.layout.setter("height"))
|
|
||||||
self.add_widget(self.layout)
|
|
||||||
self.effect_cls = ScrollEffect
|
|
||||||
self.bar_width = dp(12)
|
|
||||||
self.scroll_type = ["content", "bars"]
|
|
||||||
|
|
||||||
|
|
||||||
class HovererableLabel(HoverBehavior, Label):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TooltipLabel(HovererableLabel):
|
class TooltipLabel(HovererableLabel, MDTooltip):
|
||||||
tooltip = None
|
tooltip_display_delay = 0.1
|
||||||
|
|
||||||
def create_tooltip(self, text, x, y):
|
def create_tooltip(self, text, x, y):
|
||||||
text = text.replace("<br>", "\n").replace("&", "&").replace("&bl;", "[").replace("&br;", "]")
|
text = text.replace("<br>", "\n").replace("&", "&").replace("&bl;", "[").replace("&br;", "]")
|
||||||
if self.tooltip:
|
|
||||||
# update
|
|
||||||
self.tooltip.children[0].text = text
|
|
||||||
else:
|
|
||||||
self.tooltip = FloatLayout()
|
|
||||||
tooltip_label = ToolTip(text=text)
|
|
||||||
self.tooltip.add_widget(tooltip_label)
|
|
||||||
fade_in_animation.start(self.tooltip)
|
|
||||||
App.get_running_app().root.add_widget(self.tooltip)
|
|
||||||
|
|
||||||
# handle left-side boundary to not render off-screen
|
|
||||||
x = max(x, 3 + self.tooltip.children[0].texture_size[0] / 2)
|
|
||||||
|
|
||||||
# position float layout
|
# position float layout
|
||||||
self.tooltip.x = x - self.tooltip.width / 2
|
center_x, center_y = self.to_window(self.center_x, self.center_y)
|
||||||
self.tooltip.y = y - self.tooltip.height / 2 + 48
|
self.shift_y = y - center_y
|
||||||
|
shift_x = center_x - x
|
||||||
|
if shift_x > 0:
|
||||||
|
self.shift_left = shift_x
|
||||||
|
else:
|
||||||
|
self.shift_right = shift_x
|
||||||
|
|
||||||
def remove_tooltip(self):
|
if self._tooltip:
|
||||||
if self.tooltip:
|
# update
|
||||||
App.get_running_app().root.remove_widget(self.tooltip)
|
self._tooltip.text = text
|
||||||
self.tooltip = None
|
else:
|
||||||
|
self._tooltip = ToolTip(text=text, pos_hint={})
|
||||||
|
self.display_tooltip()
|
||||||
|
|
||||||
def on_mouse_pos(self, window, pos):
|
def on_mouse_pos(self, window, pos):
|
||||||
if not self.get_root_window():
|
if not self.get_root_window():
|
||||||
@@ -202,26 +263,26 @@ class TooltipLabel(HovererableLabel):
|
|||||||
|
|
||||||
def on_leave(self):
|
def on_leave(self):
|
||||||
self.remove_tooltip()
|
self.remove_tooltip()
|
||||||
|
self._tooltip = None
|
||||||
|
|
||||||
|
|
||||||
class ServerLabel(HovererableLabel):
|
class ServerLabel(HovererableLabel, MDTooltip):
|
||||||
|
tooltip_display_delay = 0.1
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(HovererableLabel, self).__init__(*args, **kwargs)
|
super(HovererableLabel, self).__init__(*args, **kwargs)
|
||||||
self.layout = FloatLayout()
|
self._tooltip = ServerToolTip(text="Test")
|
||||||
self.popuplabel = ServerToolTip(text="Test")
|
|
||||||
self.layout.add_widget(self.popuplabel)
|
|
||||||
|
|
||||||
def on_enter(self):
|
def on_enter(self):
|
||||||
self.popuplabel.text = self.get_text()
|
self._tooltip.text = self.get_text()
|
||||||
App.get_running_app().root.add_widget(self.layout)
|
self.display_tooltip()
|
||||||
fade_in_animation.start(self.layout)
|
|
||||||
|
|
||||||
def on_leave(self):
|
def on_leave(self):
|
||||||
App.get_running_app().root.remove_widget(self.layout)
|
self.animation_tooltip_dismiss()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ctx(self) -> context_type:
|
def ctx(self) -> context_type:
|
||||||
return App.get_running_app().ctx
|
return MDApp.get_running_app().ctx
|
||||||
|
|
||||||
def get_text(self):
|
def get_text(self):
|
||||||
if self.ctx.server:
|
if self.ctx.server:
|
||||||
@@ -262,11 +323,11 @@ class ServerLabel(HovererableLabel):
|
|||||||
return "No current server connection. \nPlease connect to an Archipelago server."
|
return "No current server connection. \nPlease connect to an Archipelago server."
|
||||||
|
|
||||||
|
|
||||||
class MainLayout(GridLayout):
|
class MainLayout(MDGridLayout):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ContainerLayout(FloatLayout):
|
class ContainerLayout(MDFloatLayout):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -286,6 +347,11 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
|||||||
return super(SelectableLabel, self).refresh_view_attrs(
|
return super(SelectableLabel, self).refresh_view_attrs(
|
||||||
rv, index, data)
|
rv, index, data)
|
||||||
|
|
||||||
|
def on_size(self, instance_label, size: list) -> None:
|
||||||
|
super().on_size(instance_label, size)
|
||||||
|
if self.parent:
|
||||||
|
self.width = self.parent.width
|
||||||
|
|
||||||
def on_touch_down(self, touch):
|
def on_touch_down(self, touch):
|
||||||
""" Add selection on touch down """
|
""" Add selection on touch down """
|
||||||
if super(SelectableLabel, self).on_touch_down(touch):
|
if super(SelectableLabel, self).on_touch_down(touch):
|
||||||
@@ -297,9 +363,9 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
|||||||
# Not a fan of the following few lines, but they work.
|
# Not a fan of the following few lines, but they work.
|
||||||
temp = MarkupLabel(text=self.text).markup
|
temp = MarkupLabel(text=self.text).markup
|
||||||
text = "".join(part for part in temp if not part.startswith("["))
|
text = "".join(part for part in temp if not part.startswith("["))
|
||||||
cmdinput = App.get_running_app().textinput
|
cmdinput = MDApp.get_running_app().textinput
|
||||||
if not cmdinput.text:
|
if not cmdinput.text:
|
||||||
input_text = get_input_text_from_response(text, App.get_running_app().last_autofillable_command)
|
input_text = get_input_text_from_response(text, MDApp.get_running_app().last_autofillable_command)
|
||||||
if input_text is not None:
|
if input_text is not None:
|
||||||
cmdinput.text = input_text
|
cmdinput.text = input_text
|
||||||
|
|
||||||
@@ -310,30 +376,115 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
|||||||
""" Respond to the selection of items in the view. """
|
""" Respond to the selection of items in the view. """
|
||||||
self.selected = is_selected
|
self.selected = is_selected
|
||||||
|
|
||||||
|
|
||||||
class AutocompleteHintInput(TextInput):
|
class MarkupDropdownTextItem(MDDropdownTextItem):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class MarkupDropdown(MDDropdownMenu):
|
||||||
|
def on_items(self, instance, value: list) -> None:
|
||||||
|
"""
|
||||||
|
The method sets the class that will be used to create the menu item.
|
||||||
|
"""
|
||||||
|
|
||||||
|
items = []
|
||||||
|
viewclass = "MarkupDropdownTextItem"
|
||||||
|
|
||||||
|
for data in value:
|
||||||
|
if "viewclass" not in data:
|
||||||
|
if (
|
||||||
|
"leading_icon" not in data
|
||||||
|
and "trailing_icon" not in data
|
||||||
|
and "trailing_text" not in data
|
||||||
|
):
|
||||||
|
viewclass = "MarkupDropdownTextItem"
|
||||||
|
elif (
|
||||||
|
"leading_icon" in data
|
||||||
|
and "trailing_icon" not in data
|
||||||
|
and "trailing_text" not in data
|
||||||
|
):
|
||||||
|
viewclass = "MDDropdownLeadingIconItem"
|
||||||
|
elif (
|
||||||
|
"leading_icon" not in data
|
||||||
|
and "trailing_icon" in data
|
||||||
|
and "trailing_text" not in data
|
||||||
|
):
|
||||||
|
viewclass = "MDDropdownTrailingIconItem"
|
||||||
|
elif (
|
||||||
|
"leading_icon" not in data
|
||||||
|
and "trailing_icon" in data
|
||||||
|
and "trailing_text" in data
|
||||||
|
):
|
||||||
|
viewclass = "MDDropdownTrailingIconTextItem"
|
||||||
|
elif (
|
||||||
|
"leading_icon" in data
|
||||||
|
and "trailing_icon" in data
|
||||||
|
and "trailing_text" in data
|
||||||
|
):
|
||||||
|
viewclass = "MDDropdownLeadingTrailingIconTextItem"
|
||||||
|
elif (
|
||||||
|
"leading_icon" in data
|
||||||
|
and "trailing_icon" in data
|
||||||
|
and "trailing_text" not in data
|
||||||
|
):
|
||||||
|
viewclass = "MDDropdownLeadingTrailingIconItem"
|
||||||
|
elif (
|
||||||
|
"leading_icon" not in data
|
||||||
|
and "trailing_icon" not in data
|
||||||
|
and "trailing_text" in data
|
||||||
|
):
|
||||||
|
viewclass = "MDDropdownTrailingTextItem"
|
||||||
|
elif (
|
||||||
|
"leading_icon" in data
|
||||||
|
and "trailing_icon" not in data
|
||||||
|
and "trailing_text" in data
|
||||||
|
):
|
||||||
|
viewclass = "MDDropdownLeadingIconTrailingTextItem"
|
||||||
|
|
||||||
|
data["viewclass"] = viewclass
|
||||||
|
|
||||||
|
if "height" not in data:
|
||||||
|
data["height"] = dp(48)
|
||||||
|
|
||||||
|
items.append(data)
|
||||||
|
|
||||||
|
self._items = items
|
||||||
|
# Update items in view
|
||||||
|
if hasattr(self, "menu"):
|
||||||
|
self.menu.data = self._items
|
||||||
|
|
||||||
|
|
||||||
|
class AutocompleteHintInput(MDTextField):
|
||||||
min_chars = NumericProperty(3)
|
min_chars = NumericProperty(3)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.dropdown = DropDown()
|
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.bind(on_select=lambda instance, x: setattr(self, 'text', x))
|
||||||
self.bind(on_text_validate=self.on_message)
|
self.bind(on_text_validate=self.on_message)
|
||||||
|
self.bind(width=lambda instance, x: setattr(self.dropdown, "width", x))
|
||||||
|
|
||||||
def on_message(self, instance):
|
def on_message(self, instance):
|
||||||
App.get_running_app().commandprocessor("!hint "+instance.text)
|
MDApp.get_running_app().commandprocessor("!hint "+instance.text)
|
||||||
|
|
||||||
def on_text(self, instance, value):
|
def on_text(self, instance, value):
|
||||||
if len(value) >= self.min_chars:
|
if len(value) >= self.min_chars:
|
||||||
self.dropdown.clear_widgets()
|
self.dropdown.items.clear()
|
||||||
ctx: context_type = App.get_running_app().ctx
|
ctx: context_type = MDApp.get_running_app().ctx
|
||||||
if not ctx.game:
|
if not ctx.game:
|
||||||
return
|
return
|
||||||
item_names = ctx.item_names._game_store[ctx.game].values()
|
item_names = ctx.item_names._game_store[ctx.game].values()
|
||||||
|
|
||||||
def on_press(button: Button):
|
def on_press(text):
|
||||||
split_text = MarkupLabel(text=button.text).markup
|
split_text = MarkupLabel(text=text).markup
|
||||||
return self.dropdown.select("".join(text_frag for text_frag in split_text
|
return self.dropdown.select("".join(text_frag for text_frag in split_text
|
||||||
if not text_frag.startswith("[")))
|
if not text_frag.startswith("[")))
|
||||||
lowered = value.lower()
|
lowered = value.lower()
|
||||||
@@ -345,20 +496,29 @@ class AutocompleteHintInput(TextInput):
|
|||||||
else:
|
else:
|
||||||
text = escape_markup(item_name)
|
text = escape_markup(item_name)
|
||||||
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
|
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
|
||||||
btn = Button(text=text, size_hint_y=None, height=dp(30), markup=True)
|
self.dropdown.items.append({
|
||||||
btn.bind(on_release=on_press)
|
"text": text,
|
||||||
self.dropdown.add_widget(btn)
|
"on_release": lambda: on_press(text),
|
||||||
if not self.dropdown.attach_to:
|
"markup": True
|
||||||
self.dropdown.open(self)
|
})
|
||||||
|
if not self.dropdown.parent:
|
||||||
|
self.dropdown.open()
|
||||||
else:
|
else:
|
||||||
self.dropdown.dismiss()
|
self.dropdown.dismiss()
|
||||||
|
|
||||||
|
|
||||||
class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
status_icons = {
|
||||||
|
HintStatus.HINT_NO_PRIORITY: "information",
|
||||||
|
HintStatus.HINT_PRIORITY: "exclamation-thick",
|
||||||
|
HintStatus.HINT_AVOID: "alert"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HintLabel(RecycleDataViewBehavior, MDBoxLayout):
|
||||||
selected = BooleanProperty(False)
|
selected = BooleanProperty(False)
|
||||||
striped = BooleanProperty(False)
|
striped = BooleanProperty(False)
|
||||||
index = None
|
index = None
|
||||||
dropdown: DropDown
|
dropdown: MDDropdownMenu
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(HintLabel, self).__init__()
|
super(HintLabel, self).__init__()
|
||||||
@@ -369,29 +529,28 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
|||||||
self.entrance_text = ""
|
self.entrance_text = ""
|
||||||
self.status_text = ""
|
self.status_text = ""
|
||||||
self.hint = {}
|
self.hint = {}
|
||||||
for child in self.children:
|
|
||||||
child.bind(texture_size=self.set_height)
|
|
||||||
|
|
||||||
|
ctx = MDApp.get_running_app().ctx
|
||||||
|
menu_items = []
|
||||||
|
|
||||||
ctx = App.get_running_app().ctx
|
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
|
||||||
self.dropdown = DropDown()
|
name = status_names[status]
|
||||||
|
status_button = MDDropDownItem(MDDropDownItemText(text=name), size_hint_y=None, height=dp(50))
|
||||||
|
status_button.status = status
|
||||||
|
menu_items.append({
|
||||||
|
"text": name,
|
||||||
|
"leading_icon": status_icons[status],
|
||||||
|
"on_release": lambda x=status: select(self, x)
|
||||||
|
})
|
||||||
|
|
||||||
def set_value(button):
|
self.dropdown = MDDropdownMenu(caller=self.ids["status"], items=menu_items)
|
||||||
self.dropdown.select(button.status)
|
|
||||||
|
|
||||||
def select(instance, data):
|
def select(instance, data):
|
||||||
ctx.update_hint(self.hint["location"],
|
ctx.update_hint(self.hint["location"],
|
||||||
self.hint["finding_player"],
|
self.hint["finding_player"],
|
||||||
data)
|
data)
|
||||||
|
|
||||||
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
|
self.dropdown.bind(on_release=self.dropdown.dismiss)
|
||||||
name = status_names[status]
|
|
||||||
status_button = Button(text=name, size_hint_y=None, height=dp(50))
|
|
||||||
status_button.status = status
|
|
||||||
status_button.bind(on_release=set_value)
|
|
||||||
self.dropdown.add_widget(status_button)
|
|
||||||
|
|
||||||
self.dropdown.bind(on_select=select)
|
|
||||||
|
|
||||||
def set_height(self, instance, value):
|
def set_height(self, instance, value):
|
||||||
self.height = max([child.texture_size[1] for child in self.children])
|
self.height = max([child.texture_size[1] for child in self.children])
|
||||||
@@ -406,7 +565,6 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
|||||||
self.entrance_text = data["entrance"]["text"]
|
self.entrance_text = data["entrance"]["text"]
|
||||||
self.status_text = data["status"]["text"]
|
self.status_text = data["status"]["text"]
|
||||||
self.hint = data["status"]["hint"]
|
self.hint = data["status"]["hint"]
|
||||||
self.height = self.minimum_height
|
|
||||||
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
|
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
|
||||||
|
|
||||||
def on_touch_down(self, touch):
|
def on_touch_down(self, touch):
|
||||||
@@ -419,10 +577,10 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
|||||||
if status_label.collide_point(*touch.pos):
|
if status_label.collide_point(*touch.pos):
|
||||||
if self.hint["status"] == HintStatus.HINT_FOUND:
|
if self.hint["status"] == HintStatus.HINT_FOUND:
|
||||||
return
|
return
|
||||||
ctx = App.get_running_app().ctx
|
ctx = MDApp.get_running_app().ctx
|
||||||
if ctx.slot_concerns_self(self.hint["receiving_player"]): # If this player owns this hint
|
if ctx.slot_concerns_self(self.hint["receiving_player"]): # If this player owns this hint
|
||||||
# open a dropdown
|
# open a dropdown
|
||||||
self.dropdown.open(self.ids["status"])
|
self.dropdown.open()
|
||||||
elif self.selected:
|
elif self.selected:
|
||||||
self.parent.clear_selection()
|
self.parent.clear_selection()
|
||||||
else:
|
else:
|
||||||
@@ -431,8 +589,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
|||||||
if self.entrance_text != "Vanilla"
|
if self.entrance_text != "Vanilla"
|
||||||
else "", ". (", self.status_text.lower(), ")"))
|
else "", ". (", self.status_text.lower(), ")"))
|
||||||
temp = MarkupLabel(text).markup
|
temp = MarkupLabel(text).markup
|
||||||
text = "".join(
|
text = "".join(part for part in temp if not part.startswith("["))
|
||||||
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
|
|
||||||
Clipboard.copy(escape_markup(text).replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
|
Clipboard.copy(escape_markup(text).replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
|
||||||
return self.parent.select_with_touch(self.index, touch)
|
return self.parent.select_with_touch(self.index, touch)
|
||||||
else:
|
else:
|
||||||
@@ -455,7 +612,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
|||||||
else:
|
else:
|
||||||
parent.sort_key = key
|
parent.sort_key = key
|
||||||
parent.reversed = False
|
parent.reversed = False
|
||||||
App.get_running_app().update_hints()
|
MDApp.get_running_app().update_hints()
|
||||||
|
|
||||||
def apply_selection(self, rv, index, is_selected):
|
def apply_selection(self, rv, index, is_selected):
|
||||||
""" Respond to the selection of items in the view. """
|
""" Respond to the selection of items in the view. """
|
||||||
@@ -463,7 +620,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
|||||||
self.selected = is_selected
|
self.selected = is_selected
|
||||||
|
|
||||||
|
|
||||||
class ConnectBarTextInput(TextInput):
|
class ConnectBarTextInput(MDTextField):
|
||||||
def insert_text(self, substring, from_undo=False):
|
def insert_text(self, substring, from_undo=False):
|
||||||
s = substring.replace("\n", "").replace("\r", "")
|
s = substring.replace("\n", "").replace("\r", "")
|
||||||
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
|
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
|
||||||
@@ -473,7 +630,7 @@ def is_command_input(string: str) -> bool:
|
|||||||
return len(string) > 0 and string[0] in "/!"
|
return len(string) > 0 and string[0] in "/!"
|
||||||
|
|
||||||
|
|
||||||
class CommandPromptTextInput(TextInput):
|
class CommandPromptTextInput(MDTextField):
|
||||||
MAXIMUM_HISTORY_MESSAGES = 50
|
MAXIMUM_HISTORY_MESSAGES = 50
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
@@ -521,7 +678,7 @@ class CommandPromptTextInput(TextInput):
|
|||||||
|
|
||||||
|
|
||||||
class MessageBox(Popup):
|
class MessageBox(Popup):
|
||||||
class MessageBoxLabel(Label):
|
class MessageBoxLabel(MDLabel):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._label.refresh()
|
self._label.refresh()
|
||||||
@@ -539,14 +696,31 @@ class MessageBox(Popup):
|
|||||||
self.height += max(0, label.height - 18)
|
self.height += max(0, label.height - 18)
|
||||||
|
|
||||||
|
|
||||||
class GameManager(App):
|
class ClientTabs(MDTabsPrimary):
|
||||||
|
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.size_hint_y = 1
|
||||||
|
|
||||||
|
def remove_tab(self, tab, content=None):
|
||||||
|
if content is None:
|
||||||
|
content = tab.content
|
||||||
|
self.ids.container.remove_widget(tab)
|
||||||
|
self.carousel.remove_widget(content)
|
||||||
|
self.on_size(self, self.size)
|
||||||
|
|
||||||
|
|
||||||
|
class GameManager(ThemedApp):
|
||||||
logging_pairs = [
|
logging_pairs = [
|
||||||
("Client", "Archipelago"),
|
("Client", "Archipelago"),
|
||||||
]
|
]
|
||||||
base_title: str = "Archipelago Client"
|
base_title: str = "Archipelago Client"
|
||||||
last_autofillable_command: str
|
last_autofillable_command: str
|
||||||
|
|
||||||
main_area_container: GridLayout
|
main_area_container: MDGridLayout
|
||||||
""" subclasses can add more columns beside the tabs """
|
""" subclasses can add more columns beside the tabs """
|
||||||
|
|
||||||
def __init__(self, ctx: context_type):
|
def __init__(self, ctx: context_type):
|
||||||
@@ -581,18 +755,26 @@ class GameManager(App):
|
|||||||
return max(1, len(self.tabs.tab_list))
|
return max(1, len(self.tabs.tab_list))
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
def on_start(self):
|
||||||
|
def on_start(*args):
|
||||||
|
self.root.md_bg_color = self.theme_cls.backgroundColor
|
||||||
|
super().on_start()
|
||||||
|
Clock.schedule_once(on_start)
|
||||||
|
|
||||||
def build(self) -> Layout:
|
def build(self) -> Layout:
|
||||||
|
self.set_colors()
|
||||||
self.container = ContainerLayout()
|
self.container = ContainerLayout()
|
||||||
|
|
||||||
self.grid = MainLayout()
|
self.grid = MainLayout()
|
||||||
self.grid.cols = 1
|
self.grid.cols = 1
|
||||||
self.connect_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
self.connect_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70),
|
||||||
|
spacing=5, padding=(5, 10))
|
||||||
# top part
|
# top part
|
||||||
server_label = ServerLabel()
|
server_label = ServerLabel(halign="center")
|
||||||
self.connect_layout.add_widget(server_label)
|
self.connect_layout.add_widget(server_label)
|
||||||
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
|
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
|
||||||
size_hint_y=None,
|
size_hint_y=None, role="medium",
|
||||||
height=dp(30), multiline=False, write_tab=False)
|
height=dp(70), multiline=False, write_tab=False)
|
||||||
|
|
||||||
def connect_bar_validate(sender):
|
def connect_bar_validate(sender):
|
||||||
if not self.ctx.server:
|
if not self.ctx.server:
|
||||||
@@ -600,26 +782,31 @@ class GameManager(App):
|
|||||||
|
|
||||||
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
|
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
|
||||||
self.connect_layout.add_widget(self.server_connect_bar)
|
self.connect_layout.add_widget(self.server_connect_bar)
|
||||||
self.server_connect_button = Button(text="Connect", size=(dp(100), dp(30)), size_hint_y=None, size_hint_x=None)
|
self.server_connect_button = MDButton(MDButtonText(text="Connect"), style="filled", size=(dp(100), dp(70)),
|
||||||
|
size_hint_x=None, size_hint_y=None, radius=5, pos_hint={"center_y": 0.55})
|
||||||
self.server_connect_button.bind(on_press=self.connect_button_action)
|
self.server_connect_button.bind(on_press=self.connect_button_action)
|
||||||
|
self.server_connect_button.height = self.server_connect_bar.height
|
||||||
self.connect_layout.add_widget(self.server_connect_button)
|
self.connect_layout.add_widget(self.server_connect_button)
|
||||||
self.grid.add_widget(self.connect_layout)
|
self.grid.add_widget(self.connect_layout)
|
||||||
self.progressbar = ProgressBar(size_hint_y=None, height=3)
|
self.progressbar = MDLinearProgressIndicator(size_hint_y=None, height=3)
|
||||||
self.grid.add_widget(self.progressbar)
|
self.grid.add_widget(self.progressbar)
|
||||||
|
|
||||||
# middle part
|
# middle part
|
||||||
self.tabs = TabbedPanel(size_hint_y=1)
|
self.tabs = ClientTabs()
|
||||||
self.tabs.default_tab_text = "All"
|
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)
|
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
|
||||||
for logger_name, name in
|
for logger_name, name in
|
||||||
self.logging_pairs))
|
self.logging_pairs))
|
||||||
|
self.tabs.carousel.add_widget(self.tabs.default_tab_content)
|
||||||
|
|
||||||
for logger_name, display_name in self.logging_pairs:
|
for logger_name, display_name in self.logging_pairs:
|
||||||
bridge_logger = logging.getLogger(logger_name)
|
bridge_logger = logging.getLogger(logger_name)
|
||||||
panel = TabbedPanelItem(text=display_name)
|
self.log_panels[display_name] = UILog(bridge_logger)
|
||||||
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
|
|
||||||
if len(self.logging_pairs) > 1:
|
if len(self.logging_pairs) > 1:
|
||||||
|
panel = MDTabsItem(MDTabsItemText(text=display_name))
|
||||||
|
panel.content = self.log_panels[display_name]
|
||||||
# show Archipelago tab if other logging is present
|
# show Archipelago tab if other logging is present
|
||||||
|
self.tabs.carousel.add_widget(panel.content)
|
||||||
self.tabs.add_widget(panel)
|
self.tabs.add_widget(panel)
|
||||||
|
|
||||||
hint_panel = self.add_client_tab("Hints", HintLayout())
|
hint_panel = self.add_client_tab("Hints", HintLayout())
|
||||||
@@ -627,21 +814,20 @@ class GameManager(App):
|
|||||||
self.log_panels["Hints"] = hint_panel.content
|
self.log_panels["Hints"] = hint_panel.content
|
||||||
hint_panel.content.add_widget(self.hint_log)
|
hint_panel.content.add_widget(self.hint_log)
|
||||||
|
|
||||||
if len(self.logging_pairs) == 1:
|
self.main_area_container = MDGridLayout(size_hint_y=1, rows=1)
|
||||||
self.tabs.default_tab_text = "Archipelago"
|
|
||||||
|
|
||||||
self.main_area_container = GridLayout(size_hint_y=1, rows=1)
|
|
||||||
self.main_area_container.add_widget(self.tabs)
|
self.main_area_container.add_widget(self.tabs)
|
||||||
|
|
||||||
self.grid.add_widget(self.main_area_container)
|
self.grid.add_widget(self.main_area_container)
|
||||||
|
|
||||||
# bottom part
|
# bottom part
|
||||||
bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
bottom_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70), spacing=5, padding=(5, 10))
|
||||||
info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None)
|
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})
|
||||||
info_button.bind(on_release=self.command_button_action)
|
info_button.bind(on_release=self.command_button_action)
|
||||||
bottom_layout.add_widget(info_button)
|
bottom_layout.add_widget(info_button)
|
||||||
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
|
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
|
||||||
self.textinput.bind(on_text_validate=self.on_message)
|
self.textinput.bind(on_text_validate=self.on_message)
|
||||||
|
info_button.height = self.textinput.height
|
||||||
self.textinput.text_validate_unfocus = False
|
self.textinput.text_validate_unfocus = False
|
||||||
bottom_layout.add_widget(self.textinput)
|
bottom_layout.add_widget(self.textinput)
|
||||||
self.grid.add_widget(bottom_layout)
|
self.grid.add_widget(bottom_layout)
|
||||||
@@ -662,24 +848,26 @@ class GameManager(App):
|
|||||||
def add_client_tab(self, title: str, content: Widget) -> Widget:
|
def add_client_tab(self, title: str, content: Widget) -> Widget:
|
||||||
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content.
|
"""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."""
|
Returns the new tab widget, with the provided content being placed on the tab as content."""
|
||||||
new_tab = TabbedPanelItem(text=title)
|
new_tab = MDTabsItem(MDTabsItemText(text=title))
|
||||||
new_tab.content = content
|
new_tab.content = content
|
||||||
self.tabs.add_widget(new_tab)
|
self.tabs.add_widget(new_tab)
|
||||||
|
self.tabs.carousel.add_widget(new_tab.content)
|
||||||
return new_tab
|
return new_tab
|
||||||
|
|
||||||
def update_texts(self, dt):
|
def update_texts(self, dt):
|
||||||
if hasattr(self.tabs.content.children[0], "fix_heights"):
|
for slide in self.tabs.carousel.slides:
|
||||||
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream
|
if hasattr(slide, "fix_heights"):
|
||||||
|
slide.fix_heights() # TODO: remove this when Kivy fixes this upstream
|
||||||
if self.ctx.server:
|
if self.ctx.server:
|
||||||
self.title = self.base_title + " " + Utils.__version__ + \
|
self.title = self.base_title + " " + Utils.__version__ + \
|
||||||
f" | Connected to: {self.ctx.server_address} " \
|
f" | Connected to: {self.ctx.server_address} " \
|
||||||
f"{'.'.join(str(e) for e in self.ctx.server_version)}"
|
f"{'.'.join(str(e) for e in self.ctx.server_version)}"
|
||||||
self.server_connect_button.text = "Disconnect"
|
self.server_connect_button._button_text.text = "Disconnect"
|
||||||
self.server_connect_bar.readonly = True
|
self.server_connect_bar.readonly = True
|
||||||
self.progressbar.max = len(self.ctx.checked_locations) + len(self.ctx.missing_locations)
|
self.progressbar.max = len(self.ctx.checked_locations) + len(self.ctx.missing_locations)
|
||||||
self.progressbar.value = len(self.ctx.checked_locations)
|
self.progressbar.value = len(self.ctx.checked_locations)
|
||||||
else:
|
else:
|
||||||
self.server_connect_button.text = "Connect"
|
self.server_connect_button._button_text.text = "Connect"
|
||||||
self.server_connect_bar.readonly = False
|
self.server_connect_bar.readonly = False
|
||||||
self.title = self.base_title + " " + Utils.__version__
|
self.title = self.base_title + " " + Utils.__version__
|
||||||
self.progressbar.value = 0
|
self.progressbar.value = 0
|
||||||
@@ -742,8 +930,8 @@ class GameManager(App):
|
|||||||
|
|
||||||
def enable_energy_link(self):
|
def enable_energy_link(self):
|
||||||
if not hasattr(self, "energy_link_label"):
|
if not hasattr(self, "energy_link_label"):
|
||||||
self.energy_link_label = Label(text="Energy Link: Standby",
|
self.energy_link_label = MDLabel(text="Energy Link: Standby",
|
||||||
size_hint_x=None, width=150)
|
size_hint_x=None, width=150, halign="center")
|
||||||
self.connect_layout.add_widget(self.energy_link_label)
|
self.connect_layout.add_widget(self.energy_link_label)
|
||||||
|
|
||||||
def set_new_energy_link_value(self):
|
def set_new_energy_link_value(self):
|
||||||
@@ -779,8 +967,9 @@ class LogtoUI(logging.Handler):
|
|||||||
self.on_log(self.format(record))
|
self.on_log(self.format(record))
|
||||||
|
|
||||||
|
|
||||||
class UILog(RecycleView):
|
class UILog(MDRecycleView):
|
||||||
messages: typing.ClassVar[int] # comes from kv file
|
messages: typing.ClassVar[int] # comes from kv file
|
||||||
|
adaptive_height = True
|
||||||
|
|
||||||
def __init__(self, *loggers_to_handle, **kwargs):
|
def __init__(self, *loggers_to_handle, **kwargs):
|
||||||
super(UILog, self).__init__(**kwargs)
|
super(UILog, self).__init__(**kwargs)
|
||||||
@@ -807,13 +996,13 @@ class UILog(RecycleView):
|
|||||||
element.height = element.texture_size[1]
|
element.height = element.texture_size[1]
|
||||||
|
|
||||||
|
|
||||||
class HintLayout(BoxLayout):
|
class HintLayout(MDBoxLayout):
|
||||||
orientation = "vertical"
|
orientation = "vertical"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
boxlayout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
boxlayout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(55))
|
||||||
boxlayout.add_widget(Label(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(30)))
|
boxlayout.add_widget(MDLabel(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(55)))
|
||||||
boxlayout.add_widget(AutocompleteHintInput())
|
boxlayout.add_widget(AutocompleteHintInput())
|
||||||
self.add_widget(boxlayout)
|
self.add_widget(boxlayout)
|
||||||
|
|
||||||
@@ -846,8 +1035,7 @@ status_sort_weights: dict[HintStatus, int] = {
|
|||||||
HintStatus.HINT_PRIORITY: 4,
|
HintStatus.HINT_PRIORITY: 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class HintLog(MDRecycleView):
|
||||||
class HintLog(RecycleView):
|
|
||||||
header = {
|
header = {
|
||||||
"receiving": {"text": "[u]Receiving Player[/u]"},
|
"receiving": {"text": "[u]Receiving Player[/u]"},
|
||||||
"item": {"text": "[u]Item[/u]"},
|
"item": {"text": "[u]Item[/u]"},
|
||||||
@@ -858,7 +1046,7 @@ class HintLog(RecycleView):
|
|||||||
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
|
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
|
||||||
"striped": True,
|
"striped": True,
|
||||||
}
|
}
|
||||||
|
data: list[typing.Any]
|
||||||
sort_key: str = ""
|
sort_key: str = ""
|
||||||
reversed: bool = True
|
reversed: bool = True
|
||||||
|
|
||||||
@@ -871,7 +1059,7 @@ class HintLog(RecycleView):
|
|||||||
if not hints: # Fix the scrolling looking visually wrong in some edge cases
|
if not hints: # Fix the scrolling looking visually wrong in some edge cases
|
||||||
self.scroll_y = 1.0
|
self.scroll_y = 1.0
|
||||||
data = []
|
data = []
|
||||||
ctx = App.get_running_app().ctx
|
ctx = MDApp.get_running_app().ctx
|
||||||
for hint in hints:
|
for hint in hints:
|
||||||
if not hint.get("status"): # Allows connecting to old servers
|
if not hint.get("status"): # Allows connecting to old servers
|
||||||
hint["status"] = HintStatus.HINT_FOUND if hint["found"] else HintStatus.HINT_UNSPECIFIED
|
hint["status"] = HintStatus.HINT_FOUND if hint["found"] else HintStatus.HINT_UNSPECIFIED
|
||||||
@@ -935,7 +1123,8 @@ class ImageLoaderPkgutil(ImageLoaderBase):
|
|||||||
data = pkgutil.get_data(module, path)
|
data = pkgutil.get_data(module, path)
|
||||||
return self._bytes_to_data(data)
|
return self._bytes_to_data(data)
|
||||||
|
|
||||||
def _bytes_to_data(self, data: typing.Union[bytes, bytearray]) -> typing.List[ImageData]:
|
@staticmethod
|
||||||
|
def _bytes_to_data(data: typing.Union[bytes, bytearray]) -> typing.List[ImageData]:
|
||||||
loader = next(loader for loader in ImageLoader.loaders if loader.can_load_memory())
|
loader = next(loader for loader in ImageLoader.loaders if loader.can_load_memory())
|
||||||
return loader.load(loader, io.BytesIO(data))
|
return loader.load(loader, io.BytesIO(data))
|
||||||
|
|
||||||
|
|||||||
@@ -12,3 +12,6 @@ cython>=3.0.12
|
|||||||
cymem>=2.0.11
|
cymem>=2.0.11
|
||||||
orjson>=3.10.15
|
orjson>=3.10.15
|
||||||
typing_extensions>=4.12.2
|
typing_extensions>=4.12.2
|
||||||
|
pyshortcuts>=1.9.1
|
||||||
|
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
||||||
|
kivymd>=2.0.1.dev0
|
||||||
|
|||||||
7
setup.py
7
setup.py
@@ -629,12 +629,13 @@ cx_Freeze.setup(
|
|||||||
ext_modules=cythonize("_speedups.pyx"),
|
ext_modules=cythonize("_speedups.pyx"),
|
||||||
options={
|
options={
|
||||||
"build_exe": {
|
"build_exe": {
|
||||||
"packages": ["worlds", "kivy", "cymem", "websockets"],
|
"packages": ["worlds", "kivy", "cymem", "websockets", "kivymd"],
|
||||||
"includes": [],
|
"includes": [],
|
||||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||||
"pandas", "zstandard"],
|
"pandas"],
|
||||||
|
"zip_includes": [],
|
||||||
"zip_include_packages": ["*"],
|
"zip_include_packages": ["*"],
|
||||||
"zip_exclude_packages": ["worlds", "sc2"],
|
"zip_exclude_packages": ["worlds", "sc2", "kivymd"],
|
||||||
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
|
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
|
||||||
"include_msvcr": False,
|
"include_msvcr": False,
|
||||||
"replace_paths": ["*."],
|
"replace_paths": ["*."],
|
||||||
|
|||||||
@@ -65,8 +65,10 @@ class TestEntranceLookup(unittest.TestCase):
|
|||||||
"""tests that get_targets shuffles targets between groups when requested"""
|
"""tests that get_targets shuffles targets between groups when requested"""
|
||||||
multiworld = generate_test_multiworld()
|
multiworld = generate_test_multiworld()
|
||||||
generate_disconnected_region_grid(multiworld, 5)
|
generate_disconnected_region_grid(multiworld, 5)
|
||||||
|
exits_set = set([ex for region in multiworld.get_regions(1)
|
||||||
|
for ex in region.exits if not ex.connected_region])
|
||||||
|
|
||||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True)
|
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
|
||||||
er_targets = [entrance for region in multiworld.get_regions(1)
|
er_targets = [entrance for region in multiworld.get_regions(1)
|
||||||
for entrance in region.entrances if not entrance.parent_region]
|
for entrance in region.entrances if not entrance.parent_region]
|
||||||
for entrance in er_targets:
|
for entrance in er_targets:
|
||||||
@@ -86,8 +88,10 @@ class TestEntranceLookup(unittest.TestCase):
|
|||||||
"""tests that get_targets does not shuffle targets between groups when requested"""
|
"""tests that get_targets does not shuffle targets between groups when requested"""
|
||||||
multiworld = generate_test_multiworld()
|
multiworld = generate_test_multiworld()
|
||||||
generate_disconnected_region_grid(multiworld, 5)
|
generate_disconnected_region_grid(multiworld, 5)
|
||||||
|
exits_set = set([ex for region in multiworld.get_regions(1)
|
||||||
|
for ex in region.exits if not ex.connected_region])
|
||||||
|
|
||||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True)
|
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
|
||||||
er_targets = [entrance for region in multiworld.get_regions(1)
|
er_targets = [entrance for region in multiworld.get_regions(1)
|
||||||
for entrance in region.entrances if not entrance.parent_region]
|
for entrance in region.entrances if not entrance.parent_region]
|
||||||
for entrance in er_targets:
|
for entrance in er_targets:
|
||||||
@@ -99,6 +103,30 @@ class TestEntranceLookup(unittest.TestCase):
|
|||||||
group_order = [prev := group.randomization_group for group in retrieved_targets if prev != group.randomization_group]
|
group_order = [prev := group.randomization_group for group in retrieved_targets if prev != group.randomization_group]
|
||||||
self.assertEqual([ERTestGroups.TOP, ERTestGroups.BOTTOM], group_order)
|
self.assertEqual([ERTestGroups.TOP, ERTestGroups.BOTTOM], group_order)
|
||||||
|
|
||||||
|
def test_selective_dead_ends(self):
|
||||||
|
"""test that entrances that EntranceLookup has not been told to consider are ignored when finding dead-ends"""
|
||||||
|
multiworld = generate_test_multiworld()
|
||||||
|
generate_disconnected_region_grid(multiworld, 5)
|
||||||
|
exits_set = set([ex for region in multiworld.get_regions(1)
|
||||||
|
for ex in region.exits if not ex.connected_region
|
||||||
|
and ex.name != "region20_right" and ex.name != "region21_left"])
|
||||||
|
|
||||||
|
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
|
||||||
|
er_targets = [entrance for region in multiworld.get_regions(1)
|
||||||
|
for entrance in region.entrances if not entrance.parent_region and
|
||||||
|
entrance.name != "region20_right" and entrance.name != "region21_left"]
|
||||||
|
for entrance in er_targets:
|
||||||
|
lookup.add(entrance)
|
||||||
|
# region 20 is the bottom left corner of the grid, and therefore only has a right entrance from region 21
|
||||||
|
# and a top entrance from region 15; since we've told lookup to ignore the right entrance from region 21,
|
||||||
|
# the top entrance from region 15 should be considered a dead-end
|
||||||
|
dead_end_region = multiworld.get_region("region20", 1)
|
||||||
|
for dead_end in dead_end_region.entrances:
|
||||||
|
if dead_end.name == "region20_top":
|
||||||
|
break
|
||||||
|
# there should be only this one dead-end
|
||||||
|
self.assertTrue(dead_end in lookup.dead_ends)
|
||||||
|
self.assertEqual(len(lookup.dead_ends), 1)
|
||||||
|
|
||||||
class TestBakeTargetGroupLookup(unittest.TestCase):
|
class TestBakeTargetGroupLookup(unittest.TestCase):
|
||||||
def test_lookup_generation(self):
|
def test_lookup_generation(self):
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ from worlds.LauncherComponents import Component, SuffixIdentifier, Type, compone
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from SNIClient import SNIContext
|
from SNIClient import SNIContext
|
||||||
|
|
||||||
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"))
|
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"),
|
||||||
|
description="A client for connecting to SNES consoles via Super Nintendo Interface.")
|
||||||
components.append(component)
|
components.append(component)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,16 @@ class AutoLogicRegister(type):
|
|||||||
elif not item_name.startswith("__"):
|
elif not item_name.startswith("__"):
|
||||||
if hasattr(CollectionState, item_name):
|
if hasattr(CollectionState, item_name):
|
||||||
raise Exception(f"Name conflict on Logic Mixin {name} trying to overwrite {item_name}")
|
raise Exception(f"Name conflict on Logic Mixin {name} trying to overwrite {item_name}")
|
||||||
|
|
||||||
|
assert callable(function) or "init_mixin" in dct, (
|
||||||
|
f"{name} defined class variable {item_name} without also having init_mixin.\n\n"
|
||||||
|
"Explanation:\n"
|
||||||
|
"Class variables that will be mutated need to be inintialized as instance variables in init_mixin.\n"
|
||||||
|
"If your LogicMixin variables aren't actually mutable / you don't intend to mutate them, "
|
||||||
|
"there is no point in using LogixMixin.\n"
|
||||||
|
"LogicMixin exists to track custom state variables that change when items are collected/removed."
|
||||||
|
)
|
||||||
|
|
||||||
setattr(CollectionState, item_name, function)
|
setattr(CollectionState, item_name, function)
|
||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ class Component:
|
|||||||
"""
|
"""
|
||||||
display_name: str
|
display_name: str
|
||||||
"""Used as the GUI button label and the component name in the CLI args"""
|
"""Used as the GUI button label and the component name in the CLI args"""
|
||||||
|
description: str
|
||||||
|
"""Optional description displayed on the GUI underneath the display name"""
|
||||||
type: Type
|
type: Type
|
||||||
"""
|
"""
|
||||||
Enum "Type" classification of component intent, for filtering in the Launcher GUI
|
Enum "Type" classification of component intent, for filtering in the Launcher GUI
|
||||||
@@ -58,8 +60,9 @@ class Component:
|
|||||||
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
||||||
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
|
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
|
||||||
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None,
|
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None,
|
||||||
game_name: Optional[str] = None, supports_uri: Optional[bool] = False):
|
game_name: Optional[str] = None, supports_uri: Optional[bool] = False, description: str = "") -> None:
|
||||||
self.display_name = display_name
|
self.display_name = display_name
|
||||||
|
self.description = description
|
||||||
self.script_name = script_name
|
self.script_name = script_name
|
||||||
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
|
|||||||
@@ -238,14 +238,12 @@ class AdventureWorld(World):
|
|||||||
|
|
||||||
def create_regions(self) -> None:
|
def create_regions(self) -> None:
|
||||||
create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
|
create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
|
||||||
|
|
||||||
set_rules = set_rules
|
|
||||||
|
|
||||||
def generate_basic(self) -> None:
|
|
||||||
self.multiworld.get_location("Chalice Home", self.player).place_locked_item(
|
self.multiworld.get_location("Chalice Home", self.player).place_locked_item(
|
||||||
self.create_event("Victory", ItemClassification.progression))
|
self.create_event("Victory", ItemClassification.progression))
|
||||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||||
|
|
||||||
|
set_rules = set_rules
|
||||||
|
|
||||||
def pre_fill(self):
|
def pre_fill(self):
|
||||||
# Place empty items in filler locations here, to limit
|
# Place empty items in filler locations here, to limit
|
||||||
# the number of exported empty items and the density of stuff in overworld.
|
# the number of exported empty items and the density of stuff in overworld.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from typing import List, TYPE_CHECKING, Dict, Any
|
|||||||
from schema import Schema, Optional
|
from schema import Schema, Optional
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from worlds.AutoWorld import PerGameCommonOptions
|
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:
|
if TYPE_CHECKING:
|
||||||
from . import HatInTimeWorld
|
from . import HatInTimeWorld
|
||||||
@@ -625,6 +625,8 @@ class ParadeTrapWeight(Range):
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AHITOptions(PerGameCommonOptions):
|
class AHITOptions(PerGameCommonOptions):
|
||||||
|
start_inventory_from_pool: StartInventoryPool
|
||||||
|
|
||||||
EndGoal: EndGoal
|
EndGoal: EndGoal
|
||||||
ActRandomizer: ActRandomizer
|
ActRandomizer: ActRandomizer
|
||||||
ActPlando: ActPlando
|
ActPlando: ActPlando
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld
|
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, \
|
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 .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region
|
||||||
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \
|
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \
|
||||||
get_total_locations
|
get_total_locations
|
||||||
@@ -78,6 +78,9 @@ class HatInTimeWorld(World):
|
|||||||
self.nyakuza_thug_items: Dict[str, int] = {}
|
self.nyakuza_thug_items: Dict[str, int] = {}
|
||||||
self.badge_seller_count: int = 0
|
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):
|
def generate_early(self):
|
||||||
adjust_options(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?
|
- 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.
|
- 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!
|
- 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!
|
- 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.
|
- 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?!
|
- 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
|
## Enabling the tuner
|
||||||
|
|
||||||
Depending on how you installed Civ 6 you will have to navigate to one of the following:
|
In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
|
||||||
|
|
||||||
- `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.
|
|
||||||
|
|
||||||
## Mod Installation
|
## Mod Installation
|
||||||
|
|
||||||
1. Download and unzip the latest release of the mod from [GitHub](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
|
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.
|
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:
|
5. Your finished mod folder should look something like this:
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import TYPE_CHECKING, Set
|
from typing import TYPE_CHECKING, Set, Optional
|
||||||
from .locations import BASE_ID, get_location_names_to_ids
|
from .locations import BASE_ID, get_location_names_to_ids
|
||||||
from .items import cvcotm_item_info, MAJORS_CLASSIFICATIONS
|
from .items import cvcotm_item_info, MAJORS_CLASSIFICATIONS
|
||||||
from .locations import cvcotm_location_info
|
from .locations import cvcotm_location_info
|
||||||
@@ -91,6 +91,7 @@ class CastlevaniaCotMClient(BizHawkClient):
|
|||||||
patch_suffix = ".apcvcotm"
|
patch_suffix = ".apcvcotm"
|
||||||
sent_initial_packets: bool
|
sent_initial_packets: bool
|
||||||
self_induced_death: bool
|
self_induced_death: bool
|
||||||
|
time_of_sent_death: Optional[float]
|
||||||
local_checked_locations: Set[int]
|
local_checked_locations: Set[int]
|
||||||
client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
|
client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
|
||||||
killed_dracula_2: bool
|
killed_dracula_2: bool
|
||||||
@@ -139,6 +140,7 @@ class CastlevaniaCotMClient(BizHawkClient):
|
|||||||
self.sent_initial_packets = False
|
self.sent_initial_packets = False
|
||||||
self.local_checked_locations = set()
|
self.local_checked_locations = set()
|
||||||
self.self_induced_death = False
|
self.self_induced_death = False
|
||||||
|
self.time_of_sent_death = None
|
||||||
self.client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
|
self.client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
|
||||||
self.killed_dracula_2 = False
|
self.killed_dracula_2 = False
|
||||||
self.won_battle_arena = False
|
self.won_battle_arena = False
|
||||||
@@ -156,14 +158,16 @@ class CastlevaniaCotMClient(BizHawkClient):
|
|||||||
return
|
return
|
||||||
if ctx.slot is None:
|
if ctx.slot is None:
|
||||||
return
|
return
|
||||||
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
|
if "DeathLink" in args["tags"] and args["data"]["time"] != self.time_of_sent_death:
|
||||||
if "cause" in args["data"]:
|
if "cause" in args["data"]:
|
||||||
cause = args["data"]["cause"]
|
cause = args["data"]["cause"]
|
||||||
|
# If the other game sent a death with a blank string for the cause, use the default death message.
|
||||||
if cause == "":
|
if cause == "":
|
||||||
cause = f"{args['data']['source']} killed you without a word!"
|
cause = f"{args['data']['source']} killed you without a word!"
|
||||||
if len(cause) > ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT:
|
if len(cause) > ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT:
|
||||||
cause = cause[:ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT]
|
cause = cause[:ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT]
|
||||||
else:
|
else:
|
||||||
|
# If the other game sent a death with no cause at all, use the default death message.
|
||||||
cause = f"{args['data']['source']} killed you without a word!"
|
cause = f"{args['data']['source']} killed you without a word!"
|
||||||
|
|
||||||
# Highlight the player that killed us in the game's orange text.
|
# Highlight the player that killed us in the game's orange text.
|
||||||
@@ -259,8 +263,13 @@ class CastlevaniaCotMClient(BizHawkClient):
|
|||||||
else:
|
else:
|
||||||
area_of_death = DEATHLINK_AREA_NAMES[area]
|
area_of_death = DEATHLINK_AREA_NAMES[area]
|
||||||
|
|
||||||
|
# Send the death.
|
||||||
await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished in {area_of_death}. Dracula has won!")
|
await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished in {area_of_death}. Dracula has won!")
|
||||||
|
|
||||||
|
# Record the time in which the death was sent so when we receive the packet we can tell it wasn't our
|
||||||
|
# own death. ctx.on_deathlink overwrites it later, so it MUST be grabbed now.
|
||||||
|
self.time_of_sent_death = ctx.last_death_link
|
||||||
|
|
||||||
# Update the Dracula II and Battle Arena events already being done on past separate sessions for if the
|
# Update the Dracula II and Battle Arena events already being done on past separate sessions for if the
|
||||||
# player is running the Battle Arena and Dracula goal.
|
# player is running the Battle Arena and Dracula goal.
|
||||||
if f"castlevania_cotm_events_{ctx.team}_{ctx.slot}" in ctx.stored_data:
|
if f"castlevania_cotm_events_{ctx.team}_{ctx.slot}" in ctx.stored_data:
|
||||||
|
|||||||
@@ -930,7 +930,7 @@ location_tables: Dict[str, List[DS3LocationData]] = {
|
|||||||
"Great Swamp Ring", miniboss=True), # Giant Crab drop
|
"Great Swamp Ring", miniboss=True), # Giant Crab drop
|
||||||
DS3LocationData("RS: Blue Sentinels - Horace", "Blue Sentinels",
|
DS3LocationData("RS: Blue Sentinels - Horace", "Blue Sentinels",
|
||||||
missable=True, npc=True), # Horace quest
|
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",
|
DS3LocationData("RS: Fading Soul - woods by Crucifixion Woods bonfire", "Fading Soul",
|
||||||
static='03,0:53300210::'),
|
static='03,0:53300210::'),
|
||||||
|
|
||||||
|
|||||||
@@ -98,14 +98,14 @@ def create_trap_items(world, world_options: Options.DLCQuestOptions, trap_needed
|
|||||||
return traps
|
return traps
|
||||||
|
|
||||||
|
|
||||||
def create_items(world, world_options: Options.DLCQuestOptions, locations_count: int, random: Random):
|
def create_items(world, world_options: Options.DLCQuestOptions, locations_count: int, excluded_items: list[str], random: Random):
|
||||||
created_items = []
|
created_items = []
|
||||||
if world_options.campaign == Options.Campaign.option_basic or world_options.campaign == Options.Campaign.option_both:
|
if world_options.campaign == Options.Campaign.option_basic or world_options.campaign == Options.Campaign.option_both:
|
||||||
create_items_basic(world_options, created_items, world)
|
create_items_basic(world_options, created_items, world, excluded_items)
|
||||||
|
|
||||||
if (world_options.campaign == Options.Campaign.option_live_freemium_or_die or
|
if (world_options.campaign == Options.Campaign.option_live_freemium_or_die or
|
||||||
world_options.campaign == Options.Campaign.option_both):
|
world_options.campaign == Options.Campaign.option_both):
|
||||||
create_items_lfod(world_options, created_items, world)
|
create_items_lfod(world_options, created_items, world, excluded_items)
|
||||||
|
|
||||||
trap_items = create_trap_items(world, world_options, locations_count - len(created_items), random)
|
trap_items = create_trap_items(world, world_options, locations_count - len(created_items), random)
|
||||||
created_items += trap_items
|
created_items += trap_items
|
||||||
@@ -113,8 +113,12 @@ def create_items(world, world_options: Options.DLCQuestOptions, locations_count:
|
|||||||
return created_items
|
return created_items
|
||||||
|
|
||||||
|
|
||||||
def create_items_lfod(world_options, created_items, world):
|
def create_items_lfod(world_options, created_items, world, excluded_items):
|
||||||
for item in items_by_group[Group.Freemium]:
|
for item in items_by_group[Group.Freemium]:
|
||||||
|
if item.name in excluded_items:
|
||||||
|
excluded_items.remove(item)
|
||||||
|
continue
|
||||||
|
|
||||||
if item.has_any_group(Group.DLC):
|
if item.has_any_group(Group.DLC):
|
||||||
created_items.append(world.create_item(item))
|
created_items.append(world.create_item(item))
|
||||||
if item.has_any_group(Group.Item) and world_options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
if item.has_any_group(Group.Item) and world_options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||||
@@ -128,8 +132,12 @@ def create_items_lfod(world_options, created_items, world):
|
|||||||
create_coin(world_options, created_items, world, 889, 200, Group.Freemium)
|
create_coin(world_options, created_items, world, 889, 200, Group.Freemium)
|
||||||
|
|
||||||
|
|
||||||
def create_items_basic(world_options, created_items, world):
|
def create_items_basic(world_options, created_items, world, excluded_items):
|
||||||
for item in items_by_group[Group.DLCQuest]:
|
for item in items_by_group[Group.DLCQuest]:
|
||||||
|
if item.name in excluded_items:
|
||||||
|
excluded_items.remove(item.name)
|
||||||
|
continue
|
||||||
|
|
||||||
if item.has_any_group(Group.DLC):
|
if item.has_any_group(Group.DLC):
|
||||||
created_items.append(world.create_item(item))
|
created_items.append(world.create_item(item))
|
||||||
if item.has_any_group(Group.Item) and world_options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
if item.has_any_group(Group.Item) and world_options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||||
|
|||||||
@@ -66,10 +66,10 @@ class DLCqworld(World):
|
|||||||
for location in self.multiworld.get_locations(self.player)
|
for location in self.multiworld.get_locations(self.player)
|
||||||
if not location.advancement])
|
if not location.advancement])
|
||||||
|
|
||||||
items_to_exclude = [excluded_items
|
items_to_exclude = [excluded_items.name
|
||||||
for excluded_items in self.multiworld.precollected_items[self.player]]
|
for excluded_items in self.multiworld.precollected_items[self.player]]
|
||||||
|
|
||||||
created_items = create_items(self, self.options, locations_count + len(items_to_exclude), self.multiworld.random)
|
created_items = create_items(self, self.options, locations_count, items_to_exclude, self.multiworld.random)
|
||||||
|
|
||||||
self.multiworld.itempool += created_items
|
self.multiworld.itempool += created_items
|
||||||
|
|
||||||
@@ -84,9 +84,7 @@ class DLCqworld(World):
|
|||||||
else:
|
else:
|
||||||
early_items[self.player]["Movement Pack"] = 1
|
early_items[self.player]["Movement Pack"] = 1
|
||||||
|
|
||||||
for item in items_to_exclude:
|
|
||||||
if item in self.multiworld.itempool:
|
|
||||||
self.multiworld.itempool.remove(item)
|
|
||||||
|
|
||||||
def precollect_coinsanity(self):
|
def precollect_coinsanity(self):
|
||||||
if self.options.campaign == Options.Campaign.option_basic:
|
if self.options.campaign == Options.Campaign.option_basic:
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import unittest
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from Options import NamedRange
|
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 . 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):
|
def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||||
@@ -38,6 +39,8 @@ class TestGenerateDynamicOptions(DLCQuestTestBase):
|
|||||||
basic_checks(self, multiworld)
|
basic_checks(self, multiworld)
|
||||||
|
|
||||||
def test_given_option_truple_when_generate_then_basic_checks(self):
|
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)
|
num_options = len(options_to_include)
|
||||||
for option1_index in range(0, num_options):
|
for option1_index in range(0, num_options):
|
||||||
for option2_index in range(option1_index + 1, num_options):
|
for option2_index in range(option1_index + 1, num_options):
|
||||||
@@ -59,6 +62,8 @@ class TestGenerateDynamicOptions(DLCQuestTestBase):
|
|||||||
basic_checks(self, multiworld)
|
basic_checks(self, multiworld)
|
||||||
|
|
||||||
def test_given_option_quartet_when_generate_then_basic_checks(self):
|
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)
|
num_options = len(options_to_include)
|
||||||
for option1_index in range(0, num_options):
|
for option1_index in range(0, num_options):
|
||||||
for option2_index in range(option1_index + 1, num_options):
|
for option2_index in range(option1_index + 1, num_options):
|
||||||
|
|||||||
@@ -1,19 +1,26 @@
|
|||||||
from typing import ClassVar
|
import os
|
||||||
|
|
||||||
from typing import Dict, FrozenSet, Tuple, Any
|
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
from typing import ClassVar
|
||||||
|
from typing import Dict, FrozenSet, Tuple, Any
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from test.bases import WorldTestBase
|
from test.bases import WorldTestBase
|
||||||
from .. import DLCqworld
|
|
||||||
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
|
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
|
||||||
from worlds.AutoWorld import call_all
|
from worlds.AutoWorld import call_all
|
||||||
|
from .. import DLCqworld
|
||||||
|
|
||||||
|
|
||||||
class DLCQuestTestBase(WorldTestBase):
|
class DLCQuestTestBase(WorldTestBase):
|
||||||
game = "DLCQuest"
|
game = "DLCQuest"
|
||||||
world: DLCqworld
|
world: DLCqworld
|
||||||
player: ClassVar[int] = 1
|
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):
|
def world_setup(self, *args, **kwargs):
|
||||||
super().world_setup(*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, \
|
from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \
|
||||||
StartInventoryPool, PerGameCommonOptions, OptionGroup
|
StartInventoryPool, PerGameCommonOptions, OptionGroup
|
||||||
|
|
||||||
|
|
||||||
# schema helpers
|
# schema helpers
|
||||||
class FloatRange:
|
class FloatRange:
|
||||||
def __init__(self, low, high):
|
def __init__(self, low, high):
|
||||||
self._low = low
|
self._low = low
|
||||||
self._high = high
|
self._high = high
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value) -> float:
|
||||||
if not isinstance(value, (float, int)):
|
if not isinstance(value, (float, int)):
|
||||||
raise SchemaError(f"should be instance of float or int, but was {value!r}")
|
raise SchemaError(f"should be instance of float or int, but was {value!r}")
|
||||||
if not self._low <= value <= self._high:
|
if not self._low <= value <= self._high:
|
||||||
raise SchemaError(f"{value} is not between {self._low} and {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)))
|
LuaBool = Or(bool, And(int, lambda n: n in (0, 1)))
|
||||||
|
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ class GrubHuntGoal(NamedRange):
|
|||||||
display_name = "Grub Hunt Goal"
|
display_name = "Grub Hunt Goal"
|
||||||
range_start = 1
|
range_start = 1
|
||||||
range_end = 46
|
range_end = 46
|
||||||
special_range_names = {"all": -1}
|
special_range_names = {"all": -1, "forty_six": 46}
|
||||||
default = 46
|
default = 46
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ class MagpieBridge:
|
|||||||
ws = None
|
ws = None
|
||||||
features = []
|
features = []
|
||||||
slot_data = {}
|
slot_data = {}
|
||||||
|
has_sent_slot_data = False
|
||||||
|
|
||||||
def use_entrance_tracker(self):
|
def use_entrance_tracker(self):
|
||||||
return "entrances" in self.features \
|
return "entrances" in self.features \
|
||||||
@@ -199,7 +200,7 @@ class MagpieBridge:
|
|||||||
logger.info(
|
logger.info(
|
||||||
f"Connected, supported features: {message['features']}")
|
f"Connected, supported features: {message['features']}")
|
||||||
self.features = message["features"]
|
self.features = message["features"]
|
||||||
|
|
||||||
await self.send_handshAck()
|
await self.send_handshAck()
|
||||||
|
|
||||||
if message["type"] == "sendFull":
|
if message["type"] == "sendFull":
|
||||||
@@ -207,8 +208,6 @@ class MagpieBridge:
|
|||||||
await self.send_all_inventory()
|
await self.send_all_inventory()
|
||||||
if "checks" in self.features:
|
if "checks" in self.features:
|
||||||
await self.send_all_checks()
|
await self.send_all_checks()
|
||||||
if "slot_data" in self.features and self.slot_data:
|
|
||||||
await self.send_slot_data(self.slot_data)
|
|
||||||
if self.use_entrance_tracker():
|
if self.use_entrance_tracker():
|
||||||
await self.send_gps(diff=False)
|
await self.send_gps(diff=False)
|
||||||
|
|
||||||
@@ -220,7 +219,7 @@ class MagpieBridge:
|
|||||||
if the_id == "0x2A7":
|
if the_id == "0x2A7":
|
||||||
return "0x2A1-1"
|
return "0x2A1-1"
|
||||||
return the_id
|
return the_id
|
||||||
|
|
||||||
async def send_handshAck(self):
|
async def send_handshAck(self):
|
||||||
if not self.ws:
|
if not self.ws:
|
||||||
return
|
return
|
||||||
@@ -288,17 +287,17 @@ class MagpieBridge:
|
|||||||
|
|
||||||
return await self.gps_tracker.send_entrances(self.ws, diff)
|
return await self.gps_tracker.send_entrances(self.ws, diff)
|
||||||
|
|
||||||
async def send_slot_data(self, slot_data):
|
async def send_slot_data(self):
|
||||||
if not self.ws:
|
if not self.ws:
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.debug("Sending slot_data to magpie.")
|
logger.debug("Sending slot_data to magpie.")
|
||||||
message = {
|
message = {
|
||||||
"type": "slot_data",
|
"type": "slot_data",
|
||||||
"slot_data": slot_data
|
"slot_data": self.slot_data
|
||||||
}
|
}
|
||||||
|
|
||||||
await self.ws.send(json.dumps(message))
|
await self.ws.send(json.dumps(message))
|
||||||
|
self.has_sent_slot_data = True
|
||||||
|
|
||||||
async def serve(self):
|
async def serve(self):
|
||||||
async with websockets.serve(lambda w: self.handler(w), "", 17026, logger=logger):
|
async with websockets.serve(lambda w: self.handler(w), "", 17026, logger=logger):
|
||||||
|
|||||||
@@ -589,4 +589,6 @@ class LinksAwakeningWorld(World):
|
|||||||
for option, value in dataclasses.asdict(self.options).items() if option in slot_options_display_name
|
for option, value in dataclasses.asdict(self.options).items() if option in slot_options_display_name
|
||||||
})
|
})
|
||||||
|
|
||||||
|
slot_data.update({"entrance_mapping": self.ladxr_logic.world_setup.entrance_mapping})
|
||||||
|
|
||||||
return slot_data
|
return slot_data
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ class LingoWorld(World):
|
|||||||
"death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels",
|
"death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels",
|
||||||
"enable_pilgrimage", "sunwarp_access", "mastery_achievements", "level_2_requirement", "location_checks",
|
"enable_pilgrimage", "sunwarp_access", "mastery_achievements", "level_2_requirement", "location_checks",
|
||||||
"early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps",
|
"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 = {
|
slot_data = {
|
||||||
|
|||||||
@@ -34,12 +34,32 @@ ITEMS_BY_GROUP: Dict[str, List[str]] = {}
|
|||||||
|
|
||||||
TRAP_ITEMS: List[str] = ["Slowness Trap", "Iceland Trap", "Atbash Trap"]
|
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():
|
def load_item_data():
|
||||||
global ALL_ITEM_TABLE, ITEMS_BY_GROUP
|
|
||||||
|
|
||||||
for color in ["Black", "Red", "Blue", "Yellow", "Green", "Orange", "Gray", "Brown", "Purple"]:
|
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, [])
|
ItemType.COLOR, False, [])
|
||||||
ITEMS_BY_GROUP.setdefault("Colors", []).append(color)
|
ITEMS_BY_GROUP.setdefault("Colors", []).append(color)
|
||||||
|
|
||||||
@@ -53,16 +73,16 @@ def load_item_data():
|
|||||||
door_groups.add(door.door_group)
|
door_groups.add(door.door_group)
|
||||||
|
|
||||||
ALL_ITEM_TABLE[door.item_name] = \
|
ALL_ITEM_TABLE[door.item_name] = \
|
||||||
ItemData(get_door_item_id(room_name, door_name), ItemClassification.progression, ItemType.NORMAL,
|
ItemData(get_door_item_id(room_name, door_name), get_prog_item_classification(door.item_name),
|
||||||
door.has_doors, door.painting_ids)
|
ItemType.NORMAL, door.has_doors, door.painting_ids)
|
||||||
ITEMS_BY_GROUP.setdefault("Doors", []).append(door.item_name)
|
ITEMS_BY_GROUP.setdefault("Doors", []).append(door.item_name)
|
||||||
|
|
||||||
if door.item_group is not None:
|
if door.item_group is not None:
|
||||||
ITEMS_BY_GROUP.setdefault(door.item_group, []).append(door.item_name)
|
ITEMS_BY_GROUP.setdefault(door.item_group, []).append(door.item_name)
|
||||||
|
|
||||||
for group in door_groups:
|
for group in door_groups:
|
||||||
ALL_ITEM_TABLE[group] = ItemData(get_door_group_item_id(group),
|
ALL_ITEM_TABLE[group] = ItemData(get_door_group_item_id(group), get_prog_item_classification(group),
|
||||||
ItemClassification.progression, ItemType.NORMAL, True, [])
|
ItemType.NORMAL, True, [])
|
||||||
ITEMS_BY_GROUP.setdefault("Doors", []).append(group)
|
ITEMS_BY_GROUP.setdefault("Doors", []).append(group)
|
||||||
|
|
||||||
panel_groups: Set[str] = set()
|
panel_groups: Set[str] = set()
|
||||||
@@ -72,11 +92,12 @@ def load_item_data():
|
|||||||
panel_groups.add(panel_door.panel_group)
|
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),
|
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)
|
ITEMS_BY_GROUP.setdefault("Panels", []).append(panel_door.item_name)
|
||||||
|
|
||||||
for group in panel_groups:
|
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, [])
|
ItemType.NORMAL, False, [])
|
||||||
ITEMS_BY_GROUP.setdefault("Panels", []).append(group)
|
ITEMS_BY_GROUP.setdefault("Panels", []).append(group)
|
||||||
|
|
||||||
@@ -101,7 +122,7 @@ def load_item_data():
|
|||||||
|
|
||||||
for item_name in PROGRESSIVE_ITEMS:
|
for item_name in PROGRESSIVE_ITEMS:
|
||||||
ALL_ITEM_TABLE[item_name] = ItemData(get_progressive_item_id(item_name),
|
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.
|
# Initialize the item data at module scope.
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ LOCATIONS_BY_GROUP: Dict[str, List[str]] = {}
|
|||||||
|
|
||||||
|
|
||||||
def load_location_data():
|
def load_location_data():
|
||||||
global ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP
|
|
||||||
|
|
||||||
for room_name, panels in PANELS_BY_ROOM.items():
|
for room_name, panels in PANELS_BY_ROOM.items():
|
||||||
for panel_name, panel in panels.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
|
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):
|
def load_static_data(ll1_path, ids_path):
|
||||||
global PAINTING_EXITS, SPECIAL_ITEM_IDS, PANEL_LOCATION_IDS, DOOR_LOCATION_IDS, DOOR_ITEM_IDS, \
|
global PAINTING_EXITS
|
||||||
DOOR_GROUP_ITEM_IDS, PROGRESSIVE_ITEM_IDS, PANEL_DOOR_ITEM_IDS, PANEL_GROUP_ITEM_IDS
|
|
||||||
|
|
||||||
# Load in all item and location IDs. These are broken up into groups based on the type of item/location.
|
# 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:
|
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:
|
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
|
entrance_type = EntranceType.NORMAL
|
||||||
if "painting" in door_obj and door_obj["painting"]:
|
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):
|
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()
|
panels: List[RoomAndPanel] = list()
|
||||||
for panel in panel_door_data["panels"]:
|
for panel in panel_door_data["panels"]:
|
||||||
if isinstance(panel, dict):
|
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):
|
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.
|
# required_room can either be a single room or a list of rooms.
|
||||||
if "required_room" in panel_data:
|
if "required_room" in panel_data:
|
||||||
if isinstance(panel_data["required_room"], list):
|
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):
|
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
|
# 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.
|
# generated from the room and door name.
|
||||||
if "item_name" in door_data:
|
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):
|
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.
|
# Read in information about this painting and store it in an object.
|
||||||
painting_id = painting_data["id"]
|
painting_id = painting_data["id"]
|
||||||
|
|
||||||
@@ -468,8 +459,6 @@ def process_painting(room_name, painting_data):
|
|||||||
|
|
||||||
|
|
||||||
def process_sunwarp(room_name, sunwarp_data):
|
def process_sunwarp(room_name, sunwarp_data):
|
||||||
global SUNWARP_ENTRANCES, SUNWARP_EXITS
|
|
||||||
|
|
||||||
if sunwarp_data["direction"] == "enter":
|
if sunwarp_data["direction"] == "enter":
|
||||||
SUNWARP_ENTRANCES[sunwarp_data["dots"] - 1] = room_name
|
SUNWARP_ENTRANCES[sunwarp_data["dots"] - 1] = room_name
|
||||||
else:
|
else:
|
||||||
@@ -477,8 +466,6 @@ def process_sunwarp(room_name, sunwarp_data):
|
|||||||
|
|
||||||
|
|
||||||
def process_progressive_door(room_name, progression_name, progression_doors):
|
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 are configured as a list of doors.
|
||||||
PROGRESSIVE_ITEMS.add(progression_name)
|
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):
|
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 are configured as a list of panel doors.
|
||||||
PROGRESSIVE_ITEMS.add(progression_name)
|
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):
|
def process_room(room_name, room_data):
|
||||||
global ALL_ROOMS
|
|
||||||
|
|
||||||
room_obj = Room(room_name, [])
|
room_obj = Room(room_name, [])
|
||||||
|
|
||||||
if "entrances" in room_data:
|
if "entrances" in room_data:
|
||||||
|
|||||||
@@ -46,8 +46,16 @@ class MessengerWeb(WebWorld):
|
|||||||
"setup/en",
|
"setup/en",
|
||||||
["alwaysintreble"],
|
["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):
|
class MessengerWorld(World):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# The Messenger
|
# The Messenger
|
||||||
|
|
||||||
## Quick Links
|
## Quick Links
|
||||||
|
|
||||||
- [Setup](/tutorial/The%20Messenger/setup/en)
|
- [Setup](/tutorial/The%20Messenger/setup/en)
|
||||||
- [Options Page](/games/The%20Messenger/player-options)
|
- [Options Page](/games/The%20Messenger/player-options)
|
||||||
- [Courier Github](https://github.com/Brokemia/Courier)
|
- [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?
|
## Where can I find items?
|
||||||
|
|
||||||
You can find items wherever items can be picked up in the original game. This includes:
|
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
|
* Shopkeeper dialog where the player originally gains movement items
|
||||||
* Quest Item pickups
|
* Quest Item pickups
|
||||||
* Music Box notes
|
* 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.
|
for it.
|
||||||
|
|
||||||
The groups you can use for The Messenger are:
|
The groups you can use for The Messenger are:
|
||||||
|
|
||||||
* Notes - This covers the music notes
|
* Notes - This covers the music notes
|
||||||
* Keys - An alternative name for the music notes
|
* Keys - An alternative name for the music notes
|
||||||
* Crest - The Sun and Moon Crests
|
* Crest - The Sun and Moon Crests
|
||||||
@@ -64,16 +67,29 @@ The groups you can use for The Messenger are:
|
|||||||
be entered in game.
|
be entered in game.
|
||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
|
|
||||||
* Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item
|
* 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
|
* 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.
|
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
|
* 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
|
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
|
* 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?
|
## 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
|
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)
|
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):
|
class PortalPlando(PlandoConnections):
|
||||||
"""
|
"""
|
||||||
Plando connections to be used with portal shuffle. Direction is ignored.
|
Plando connections to be used with portal shuffle.
|
||||||
List of valid connections can be found here: https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/messenger/portals.py#L12.
|
Documentation on using this can be found in The Messenger plando guide.
|
||||||
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
|
|
||||||
"""
|
"""
|
||||||
display_name = "Portal Plando Connections"
|
display_name = "Portal Plando Connections"
|
||||||
portals = [f"{portal} Portal" for portal in PORTALS]
|
portals = [f"{portal} Portal" for portal in PORTALS]
|
||||||
@@ -40,14 +31,7 @@ class PortalPlando(PlandoConnections):
|
|||||||
class TransitionPlando(PlandoConnections):
|
class TransitionPlando(PlandoConnections):
|
||||||
"""
|
"""
|
||||||
Plando connections to be used with transition shuffle.
|
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.
|
Documentation on using this can be found in The Messenger plando guide.
|
||||||
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
|
|
||||||
"""
|
"""
|
||||||
display_name = "Transition Plando Connections"
|
display_name = "Transition Plando Connections"
|
||||||
entrances = frozenset(RANDOMIZED_CONNECTIONS.keys())
|
entrances = frozenset(RANDOMIZED_CONNECTIONS.keys())
|
||||||
@@ -147,7 +131,9 @@ class MusicBox(DefaultOnToggle):
|
|||||||
|
|
||||||
|
|
||||||
class NotesNeeded(Range):
|
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"
|
display_name = "Notes Needed"
|
||||||
range_start = 1
|
range_start = 1
|
||||||
range_end = 6
|
range_end = 6
|
||||||
|
|||||||
@@ -148,12 +148,13 @@ def set_rules(world: "MLSSWorld", excluded):
|
|||||||
and StateLogic.canDash(state, world.player)
|
and StateLogic.canDash(state, world.player)
|
||||||
and StateLogic.canCrash(state, world.player)
|
and StateLogic.canCrash(state, world.player)
|
||||||
)
|
)
|
||||||
add_rule(
|
if world.options.chuckle_beans != 0:
|
||||||
world.get_location(LocationName.BowsersCastleWendyLarryHallwayDigspot),
|
add_rule(
|
||||||
lambda state: StateLogic.ultra(state, world.player)
|
world.get_location(LocationName.BowsersCastleWendyLarryHallwayDigspot),
|
||||||
and StateLogic.fire(state, world.player)
|
lambda state: StateLogic.ultra(state, world.player)
|
||||||
and StateLogic.canCrash(state, world.player)
|
and StateLogic.fire(state, world.player)
|
||||||
)
|
and StateLogic.canCrash(state, world.player)
|
||||||
|
)
|
||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(LocationName.BowsersCastleBeforeFawfulFightBlock1),
|
world.get_location(LocationName.BowsersCastleBeforeFawfulFightBlock1),
|
||||||
lambda state: StateLogic.canDig(state, world.player)
|
lambda state: StateLogic.canDig(state, world.player)
|
||||||
|
|||||||
@@ -1580,16 +1580,22 @@ def create_regions(world):
|
|||||||
|
|
||||||
world.random.shuffle(world.item_pool)
|
world.random.shuffle(world.item_pool)
|
||||||
if not world.options.key_items_only:
|
if not world.options.key_items_only:
|
||||||
if "Player's House 2F - Player's PC" in world.options.exclude_locations:
|
def acceptable_item(item):
|
||||||
acceptable_item = lambda item: item.excludable
|
return ("Badge" not in item.name and "Trap" not in item.name and item.name != "Pokedex"
|
||||||
elif "Player's House 2F - Player's PC" in world.options.priority_locations:
|
and "Coins" not in item.name and "Progressive" not in item.name
|
||||||
acceptable_item = lambda item: item.advancement
|
and ("Player's House 2F - Player's PC" not in world.options.exclude_locations or item.excludable)
|
||||||
else:
|
and ("Player's House 2F - Player's PC" in world.options.exclude_locations or
|
||||||
acceptable_item = lambda item: True
|
"Player's House 2F - Player's PC" not in world.options.priority_locations or item.advancement))
|
||||||
for i, item in enumerate(world.item_pool):
|
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)
|
world.pc_item = world.item_pool.pop(i)
|
||||||
break
|
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] \
|
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
|
+ [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 kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser
|
||||||
from kivy.app import App
|
from kivy.app import App
|
||||||
from kivy.clock import Clock
|
from kivy.clock import Clock
|
||||||
from kivy.uix.tabbedpanel import TabbedPanelItem
|
|
||||||
from kivy.uix.gridlayout import GridLayout
|
from kivy.uix.gridlayout import GridLayout
|
||||||
from kivy.lang import Builder
|
from kivy.lang import Builder
|
||||||
from kivy.uix.label import Label
|
from kivy.uix.label import Label
|
||||||
from kivy.uix.button import Button
|
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.uix.scrollview import ScrollView
|
||||||
from kivy.properties import StringProperty
|
from kivy.properties import StringProperty
|
||||||
|
|
||||||
@@ -26,30 +25,22 @@ class HoverableButton(HoverBehavior, Button):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MissionButton(HoverableButton):
|
class MissionButton(HoverableButton, MDTooltip):
|
||||||
tooltip_text = StringProperty("Test")
|
tooltip_text = StringProperty("Test")
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(HoverableButton, self).__init__(*args, **kwargs)
|
super(HoverableButton, self).__init__(**kwargs)
|
||||||
self.layout = FloatLayout()
|
self._tooltip = ServerToolTip(text=self.text, markup=True)
|
||||||
self.popuplabel = ServerToolTip(text=self.text, markup=True)
|
self._tooltip.padding = [5, 2, 5, 2]
|
||||||
self.popuplabel.padding = [5, 2, 5, 2]
|
|
||||||
self.layout.add_widget(self.popuplabel)
|
|
||||||
|
|
||||||
def on_enter(self):
|
def on_enter(self):
|
||||||
self.popuplabel.text = self.tooltip_text
|
self._tooltip.text = self.tooltip_text
|
||||||
|
|
||||||
if self.ctx.current_tooltip:
|
if self.tooltip_text != "":
|
||||||
App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
|
self.display_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
|
|
||||||
|
|
||||||
def on_leave(self):
|
def on_leave(self):
|
||||||
self.ctx.ui.clear_tooltip()
|
self.remove_tooltip()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ctx(self) -> SC2Context:
|
def ctx(self) -> SC2Context:
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from typing import BinaryIO, Optional
|
|||||||
import Utils
|
import Utils
|
||||||
from worlds.Files import APDeltaPatch
|
from worlds.Files import APDeltaPatch
|
||||||
|
|
||||||
|
|
||||||
USHASH = '6e9c94511d04fac6e0a1e582c170be3a'
|
USHASH = '6e9c94511d04fac6e0a1e582c170be3a'
|
||||||
|
|
||||||
|
|
||||||
@@ -20,9 +19,9 @@ class SoEDeltaPatch(APDeltaPatch):
|
|||||||
|
|
||||||
|
|
||||||
def get_base_rom_path(file_name: Optional[str] = None) -> str:
|
def get_base_rom_path(file_name: Optional[str] = None) -> str:
|
||||||
options = Utils.get_options()
|
|
||||||
if not file_name:
|
if not file_name:
|
||||||
file_name = options["soe_options"]["rom_file"]
|
from . import SoEWorld
|
||||||
|
file_name = SoEWorld.settings.rom_file
|
||||||
if not file_name:
|
if not file_name:
|
||||||
raise ValueError("Missing soe_options -> rom_file from host.yaml")
|
raise ValueError("Missing soe_options -> rom_file from host.yaml")
|
||||||
if not os.path.exists(file_name):
|
if not os.path.exists(file_name):
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ class StardewValleyWorld(World):
|
|||||||
|
|
||||||
def create_items(self):
|
def create_items(self):
|
||||||
self.precollect_starting_season()
|
self.precollect_starting_season()
|
||||||
self.precollect_farm_type_items()
|
self.precollect_building_items()
|
||||||
items_to_exclude = [excluded_items
|
items_to_exclude = [excluded_items
|
||||||
for excluded_items in self.multiworld.precollected_items[self.player]
|
for excluded_items in self.multiworld.precollected_items[self.player]
|
||||||
if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK,
|
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))
|
starting_season = self.create_item(self.random.choice(season_pool))
|
||||||
self.multiworld.push_precollected(starting_season)
|
self.multiworld.push_precollected(starting_season)
|
||||||
|
|
||||||
def precollect_farm_type_items(self):
|
def precollect_building_items(self):
|
||||||
if self.options.farm_type == FarmType.option_meadowlands and self.options.building_progression & BuildingProgression.option_progressive:
|
building_progression = self.content.features.building_progression
|
||||||
self.multiworld.push_precollected(self.create_item("Progressive Coop"))
|
# 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 setup_logic_events(self):
|
||||||
def register_event(name: str, region: str, rule: StardewRule):
|
def register_event(name: str, region: str, rule: StardewRule):
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from . import content_packs
|
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 .game_content import ContentPack, StardewContent, StardewFeatures
|
||||||
from .unpacking import unpack_content
|
from .unpacking import unpack_content
|
||||||
from .. import options
|
from .. import options
|
||||||
|
from ..strings.building_names import Building
|
||||||
|
|
||||||
|
|
||||||
def create_content(player_options: options.StardewValleyOptions) -> StardewContent:
|
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:
|
if player_options.special_order_locations & options.SpecialOrderLocations.value_qi:
|
||||||
active_packs.append(content_packs.qi_board_content_pack)
|
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])
|
active_packs.append(content_packs.by_mod[mod])
|
||||||
|
|
||||||
return active_packs
|
return active_packs
|
||||||
@@ -29,6 +30,7 @@ def choose_content_packs(player_options: options.StardewValleyOptions):
|
|||||||
def choose_features(player_options: options.StardewValleyOptions) -> StardewFeatures:
|
def choose_features(player_options: options.StardewValleyOptions) -> StardewFeatures:
|
||||||
return StardewFeatures(
|
return StardewFeatures(
|
||||||
choose_booksanity(player_options.booksanity),
|
choose_booksanity(player_options.booksanity),
|
||||||
|
choose_building_progression(player_options.building_progression, player_options.farm_type),
|
||||||
choose_cropsanity(player_options.cropsanity),
|
choose_cropsanity(player_options.cropsanity),
|
||||||
choose_fishsanity(player_options.fishsanity),
|
choose_fishsanity(player_options.fishsanity),
|
||||||
choose_friendsanity(player_options.friendsanity, player_options.friendsanity_heart_size),
|
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)}")
|
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 = {
|
skill_progression_by_option = {
|
||||||
options.SkillProgression.option_vanilla: skill_progression.SkillProgressionVanilla(),
|
options.SkillProgression.option_vanilla: skill_progression.SkillProgressionVanilla(),
|
||||||
options.SkillProgression.option_progressive: skill_progression.SkillProgressionProgressive(),
|
options.SkillProgression.option_progressive: skill_progression.SkillProgressionProgressive(),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from . import booksanity
|
from . import booksanity
|
||||||
|
from . import building_progression
|
||||||
from . import cropsanity
|
from . import cropsanity
|
||||||
from . import fishsanity
|
from . import fishsanity
|
||||||
from . import friendsanity
|
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 dataclasses import dataclass, field
|
||||||
from typing import Dict, Iterable, Set, Any, Mapping, Type, Tuple, Union
|
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.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.skill import Skill
|
||||||
from ..data.villagers_data import Villager
|
from ..data.villagers_data import Villager
|
||||||
|
|
||||||
@@ -20,16 +21,17 @@ class StardewContent:
|
|||||||
game_items: Dict[str, GameItem] = field(default_factory=dict)
|
game_items: Dict[str, GameItem] = field(default_factory=dict)
|
||||||
fishes: Dict[str, FishItem] = field(default_factory=dict)
|
fishes: Dict[str, FishItem] = field(default_factory=dict)
|
||||||
villagers: Dict[str, Villager] = 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)
|
skills: Dict[str, Skill] = field(default_factory=dict)
|
||||||
quests: Dict[str, Any] = 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 item in self.game_items.values():
|
||||||
for source in item.sources:
|
for source in item.sources:
|
||||||
if isinstance(source, types):
|
if isinstance(source, types):
|
||||||
yield source
|
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 = self.game_items.setdefault(item_name, GameItem(item_name))
|
||||||
item.add_sources(sources)
|
item.add_sources(sources)
|
||||||
|
|
||||||
@@ -50,6 +52,7 @@ class StardewContent:
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class StardewFeatures:
|
class StardewFeatures:
|
||||||
booksanity: booksanity.BooksanityFeature
|
booksanity: booksanity.BooksanityFeature
|
||||||
|
building_progression: building_progression.BuildingProgressionFeature
|
||||||
cropsanity: cropsanity.CropsanityFeature
|
cropsanity: cropsanity.CropsanityFeature
|
||||||
fishsanity: fishsanity.FishsanityFeature
|
fishsanity: fishsanity.FishsanityFeature
|
||||||
friendsanity: friendsanity.FriendsanityFeature
|
friendsanity: friendsanity.FriendsanityFeature
|
||||||
@@ -70,13 +73,13 @@ class ContentPack:
|
|||||||
# def item_hook
|
# 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."""
|
"""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):
|
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):
|
def shop_source_hook(self, content: StardewContent):
|
||||||
...
|
...
|
||||||
@@ -86,12 +89,12 @@ class ContentPack:
|
|||||||
def fish_hook(self, content: StardewContent):
|
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):
|
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):
|
def artisan_good_hook(self, content: StardewContent):
|
||||||
...
|
...
|
||||||
@@ -101,6 +104,11 @@ class ContentPack:
|
|||||||
def villager_hook(self, content: StardewContent):
|
def villager_hook(self, content: StardewContent):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
farm_buildings: Iterable[Building] = ()
|
||||||
|
|
||||||
|
def farm_building_hook(self, content: StardewContent):
|
||||||
|
...
|
||||||
|
|
||||||
skills: Iterable[Skill] = ()
|
skills: Iterable[Skill] = ()
|
||||||
|
|
||||||
def skill_hook(self, content: StardewContent):
|
def skill_hook(self, content: StardewContent):
|
||||||
|
|||||||
@@ -1,7 +1,25 @@
|
|||||||
from ..game_content import ContentPack
|
from ..game_content import ContentPack
|
||||||
from ..mod_registry import register_mod_content_pack
|
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 ...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(
|
register_mod_content_pack(ContentPack(
|
||||||
ModNames.tractor,
|
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 .game_content import StardewContent, ContentPack, StardewFeatures
|
||||||
from .vanilla.base import base_game as base_game_content_pack
|
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:
|
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
|
content.villagers[villager.name] = villager
|
||||||
pack.villager_hook(content)
|
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:
|
for skill in pack.skills:
|
||||||
content.skills[skill.name] = skill
|
content.skills[skill.name] = skill
|
||||||
pack.skill_hook(content)
|
pack.skill_hook(content)
|
||||||
@@ -73,7 +77,7 @@ def register_pack(content: StardewContent, pack: ContentPack):
|
|||||||
|
|
||||||
|
|
||||||
def register_sources_and_call_hook(content: StardewContent,
|
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]):
|
hook: Callable[[StardewContent], None]):
|
||||||
for item_name, sources in sources_by_item_name.items():
|
for item_name, sources in sources_by_item_name.items():
|
||||||
item = content.game_items.setdefault(item_name, GameItem(item_name))
|
item = content.game_items.setdefault(item_name, GameItem(item_name))
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
from ..game_content import ContentPack
|
from ..game_content import ContentPack
|
||||||
from ...data import villagers_data, fish_data
|
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.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource
|
||||||
from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement
|
from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement
|
||||||
from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
|
from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
|
||||||
|
from ...strings.artisan_good_names import ArtisanGood
|
||||||
from ...strings.book_names import Book
|
from ...strings.book_names import Book
|
||||||
|
from ...strings.building_names import Building as BuildingNames
|
||||||
from ...strings.crop_names import Fruit
|
from ...strings.crop_names import Fruit
|
||||||
from ...strings.fish_names import WaterItem
|
from ...strings.fish_names import WaterItem
|
||||||
from ...strings.food_names import Beverage, Meal
|
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.fruit_tree_names import Sapling
|
||||||
from ...strings.generic_names import Generic
|
from ...strings.generic_names import Generic
|
||||||
from ...strings.material_names import Material
|
from ...strings.material_names import Material
|
||||||
|
from ...strings.metal_names import MetalBar
|
||||||
from ...strings.region_names import Region, LogicRegion
|
from ...strings.region_names import Region, LogicRegion
|
||||||
from ...strings.season_names import Season
|
from ...strings.season_names import Season
|
||||||
from ...strings.seed_names import Seed, TreeSeed
|
from ...strings.seed_names import Seed, TreeSeed
|
||||||
@@ -229,10 +233,10 @@ pelican_town = ContentPack(
|
|||||||
ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
|
ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
|
||||||
Book.mapping_cave_systems: (
|
Book.mapping_cave_systems: (
|
||||||
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
|
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
|
||||||
CompoundSource(sources=(
|
GenericSource(regions=(Region.adventurer_guild_bedroom,)),
|
||||||
GenericSource(regions=(Region.adventurer_guild_bedroom,)),
|
# Disabling the shop source for better game design.
|
||||||
ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),
|
# ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),
|
||||||
))),
|
),
|
||||||
Book.monster_compendium: (
|
Book.monster_compendium: (
|
||||||
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
|
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
|
||||||
CustomRuleSource(create_rule=lambda logic: logic.monster.can_kill_many(Generic.any)),
|
CustomRuleSource(create_rule=lambda logic: logic.monster.can_kill_many(Generic.any)),
|
||||||
@@ -385,5 +389,204 @@ pelican_town = ContentPack(
|
|||||||
villagers_data.vincent,
|
villagers_data.vincent,
|
||||||
villagers_data.willy,
|
villagers_data.willy,
|
||||||
villagers_data.wizard,
|
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 dataclasses import dataclass
|
||||||
|
|
||||||
from .game_item import ItemSource
|
from .game_item import Source
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class MachineSource(ItemSource):
|
class MachineSource(Source):
|
||||||
item: str # this should be optional (worm bin)
|
item: str # this should be optional (worm bin)
|
||||||
machine: str
|
machine: str
|
||||||
# seasons
|
# 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)
|
@dataclass(frozen=True)
|
||||||
class ItemSource(ABC):
|
class Source(ABC):
|
||||||
add_tags: ClassVar[Tuple[ItemTag]] = ()
|
add_tags: ClassVar[Tuple[ItemTag]] = ()
|
||||||
|
|
||||||
other_requirements: Tuple[Requirement, ...] = field(kw_only=True, default_factory=tuple)
|
other_requirements: Tuple[Requirement, ...] = field(kw_only=True, default_factory=tuple)
|
||||||
@@ -38,23 +38,18 @@ class ItemSource(ABC):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class GenericSource(ItemSource):
|
class GenericSource(Source):
|
||||||
regions: Tuple[str, ...] = ()
|
regions: Tuple[str, ...] = ()
|
||||||
"""No region means it's available everywhere."""
|
"""No region means it's available everywhere."""
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CustomRuleSource(ItemSource):
|
class CustomRuleSource(Source):
|
||||||
"""Hopefully once everything is migrated to sources, we won't need these custom logic anymore."""
|
"""Hopefully once everything is migrated to sources, we won't need these custom logic anymore."""
|
||||||
create_rule: Callable[[Any], StardewRule]
|
create_rule: Callable[[Any], StardewRule]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
class Tag(Source):
|
||||||
class CompoundSource(ItemSource):
|
|
||||||
sources: Tuple[ItemSource, ...] = ()
|
|
||||||
|
|
||||||
|
|
||||||
class Tag(ItemSource):
|
|
||||||
"""Not a real source, just a way to add tags to an item. Will be removed from the item sources during unpacking."""
|
"""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, ...]
|
tag: Tuple[ItemTag, ...]
|
||||||
|
|
||||||
@@ -69,10 +64,10 @@ class Tag(ItemSource):
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class GameItem:
|
class GameItem:
|
||||||
name: str
|
name: str
|
||||||
sources: List[ItemSource] = field(default_factory=list)
|
sources: List[Source] = field(default_factory=list)
|
||||||
tags: Set[ItemTag] = field(default_factory=set)
|
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)
|
self.sources.extend(source for source in sources if type(source) is not Tag)
|
||||||
for source in sources:
|
for source in sources:
|
||||||
self.add_tags(source.add_tags)
|
self.add_tags(source.add_tags)
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Tuple, Sequence, Mapping
|
from typing import Tuple, Sequence, Mapping
|
||||||
|
|
||||||
from .game_item import ItemSource, ItemTag
|
from .game_item import Source, ItemTag
|
||||||
from ..strings.season_names import Season
|
from ..strings.season_names import Season
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class ForagingSource(ItemSource):
|
class ForagingSource(Source):
|
||||||
regions: Tuple[str, ...]
|
regions: Tuple[str, ...]
|
||||||
seasons: Tuple[str, ...] = Season.all
|
seasons: Tuple[str, ...] = Season.all
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class SeasonalForagingSource(ItemSource):
|
class SeasonalForagingSource(Source):
|
||||||
season: str
|
season: str
|
||||||
days: Sequence[int]
|
days: Sequence[int]
|
||||||
regions: Tuple[str, ...]
|
regions: Tuple[str, ...]
|
||||||
@@ -22,17 +22,17 @@ class SeasonalForagingSource(ItemSource):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class FruitBatsSource(ItemSource):
|
class FruitBatsSource(Source):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class MushroomCaveSource(ItemSource):
|
class MushroomCaveSource(Source):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class HarvestFruitTreeSource(ItemSource):
|
class HarvestFruitTreeSource(Source):
|
||||||
add_tags = (ItemTag.CROPSANITY,)
|
add_tags = (ItemTag.CROPSANITY,)
|
||||||
|
|
||||||
sapling: str
|
sapling: str
|
||||||
@@ -46,7 +46,7 @@ class HarvestFruitTreeSource(ItemSource):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class HarvestCropSource(ItemSource):
|
class HarvestCropSource(Source):
|
||||||
add_tags = (ItemTag.CROPSANITY,)
|
add_tags = (ItemTag.CROPSANITY,)
|
||||||
|
|
||||||
seed: str
|
seed: str
|
||||||
@@ -61,5 +61,5 @@ class HarvestCropSource(ItemSource):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class ArtifactSpotSource(ItemSource):
|
class ArtifactSpotSource(Source):
|
||||||
amount: int
|
amount: int
|
||||||
|
|||||||
@@ -509,6 +509,7 @@ id,name,classification,groups,mod_name
|
|||||||
561,Fishing Bar Size Bonus,filler,PLAYER_BUFF,
|
561,Fishing Bar Size Bonus,filler,PLAYER_BUFF,
|
||||||
562,Quality Bonus,filler,PLAYER_BUFF,
|
562,Quality Bonus,filler,PLAYER_BUFF,
|
||||||
563,Glow Bonus,filler,PLAYER_BUFF,
|
563,Glow Bonus,filler,PLAYER_BUFF,
|
||||||
|
564,Pet Bowl,progression,BUILDING,
|
||||||
4001,Burnt Trap,trap,TRAP,
|
4001,Burnt Trap,trap,TRAP,
|
||||||
4002,Darkness Trap,trap,TRAP,
|
4002,Darkness Trap,trap,TRAP,
|
||||||
4003,Frozen Trap,trap,TRAP,
|
4003,Frozen Trap,trap,TRAP,
|
||||||
|
|||||||
|
@@ -21,6 +21,11 @@ class SkillRequirement(Requirement):
|
|||||||
level: int
|
level: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RegionRequirement(Requirement):
|
||||||
|
region: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class SeasonRequirement(Requirement):
|
class SeasonRequirement(Requirement):
|
||||||
season: str
|
season: str
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Tuple, Optional
|
from typing import Tuple, Optional
|
||||||
|
|
||||||
from .game_item import ItemSource
|
from .game_item import Source
|
||||||
from ..strings.season_names import Season
|
from ..strings.season_names import Season
|
||||||
|
|
||||||
ItemPrice = Tuple[int, str]
|
ItemPrice = Tuple[int, str]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class ShopSource(ItemSource):
|
class ShopSource(Source):
|
||||||
shop_region: str
|
shop_region: str
|
||||||
money_price: Optional[int] = None
|
money_price: Optional[int] = None
|
||||||
items_price: Optional[Tuple[ItemPrice, ...]] = None
|
items_price: Optional[Tuple[ItemPrice, ...]] = None
|
||||||
@@ -20,20 +20,20 @@ class ShopSource(ItemSource):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class MysteryBoxSource(ItemSource):
|
class MysteryBoxSource(Source):
|
||||||
amount: int
|
amount: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class ArtifactTroveSource(ItemSource):
|
class ArtifactTroveSource(Source):
|
||||||
amount: int
|
amount: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class PrizeMachineSource(ItemSource):
|
class PrizeMachineSource(Source):
|
||||||
amount: int
|
amount: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class FishingTreasureChestSource(ItemSource):
|
class FishingTreasureChestSource(Source):
|
||||||
amount: int
|
amount: int
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions,
|
|||||||
|
|
||||||
add_seasonal_candidates(early_candidates, options)
|
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)
|
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 Coop")
|
||||||
early_candidates.append("Progressive Barn")
|
early_candidates.append("Progressive Barn")
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from .data.game_item import ItemTag
|
|||||||
from .logic.logic_event import all_events
|
from .logic.logic_event import all_events
|
||||||
from .mods.mod_data import ModNames
|
from .mods.mod_data import ModNames
|
||||||
from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \
|
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
|
Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs
|
||||||
from .strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName
|
from .strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName
|
||||||
from .strings.ap_names.ap_weapon_names import APWeapon
|
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_tools(item_factory, content, items)
|
||||||
create_skills(item_factory, content, items)
|
create_skills(item_factory, content, items)
|
||||||
create_wizard_buildings(item_factory, options, 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("Railroad Boulder Removed"))
|
||||||
items.append(item_factory(CommunityUpgrade.fruit_bats))
|
items.append(item_factory(CommunityUpgrade.fruit_bats))
|
||||||
items.append(item_factory(CommunityUpgrade.mushroom_boxes))
|
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"))
|
items.append(item_factory("Woods Obelisk"))
|
||||||
|
|
||||||
|
|
||||||
def create_carpenter_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
|
def create_carpenter_buildings(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]):
|
||||||
building_option = options.building_progression
|
building_progression = content.features.building_progression
|
||||||
if not building_option & BuildingProgression.option_progressive:
|
if not building_progression.is_progressive:
|
||||||
return
|
return
|
||||||
items.append(item_factory("Progressive Coop"))
|
|
||||||
items.append(item_factory("Progressive Coop"))
|
for building in content.farm_buildings.values():
|
||||||
items.append(item_factory("Progressive Coop"))
|
item_name, _ = building_progression.to_progressive_item(building.name)
|
||||||
items.append(item_factory("Progressive Barn"))
|
items.append(item_factory(item_name))
|
||||||
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"))
|
|
||||||
|
|
||||||
|
|
||||||
def create_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
|
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 .data.museum_data import all_museum_items
|
||||||
from .mods.mod_data import ModNames
|
from .mods.mod_data import ModNames
|
||||||
from .options import ExcludeGingerIsland, ArcadeMachineLocations, SpecialOrderLocations, Museumsanity, \
|
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 .options import StardewValleyOptions, Craftsanity, Chefsanity, Cooksanity, Shipsanity, Monstersanity
|
||||||
from .strings.goal_names import Goal
|
from .strings.goal_names import Goal
|
||||||
from .strings.quest_names import ModQuest, Quest
|
from .strings.quest_names import ModQuest, Quest
|
||||||
@@ -261,6 +261,19 @@ def extend_baby_locations(randomized_locations: List[LocationData]):
|
|||||||
randomized_locations.extend(baby_locations)
|
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):
|
def extend_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random):
|
||||||
if options.festival_locations == FestivalLocations.option_disabled:
|
if options.festival_locations == FestivalLocations.option_disabled:
|
||||||
return
|
return
|
||||||
@@ -485,10 +498,7 @@ def create_locations(location_collector: StardewLocationCollector,
|
|||||||
if skill_progression.is_mastery_randomized(skill):
|
if skill_progression.is_mastery_randomized(skill):
|
||||||
randomized_locations.append(location_table[skill.mastery_name])
|
randomized_locations.append(location_table[skill.mastery_name])
|
||||||
|
|
||||||
if options.building_progression & BuildingProgression.option_progressive:
|
extend_building_locations(randomized_locations, content)
|
||||||
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])
|
|
||||||
|
|
||||||
if options.arcade_machine_locations != ArcadeMachineLocations.option_disabled:
|
if options.arcade_machine_locations != ArcadeMachineLocations.option_disabled:
|
||||||
randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE_VICTORY])
|
randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE_VICTORY])
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ class LogicRegistry:
|
|||||||
self.museum_rules: Dict[str, StardewRule] = {}
|
self.museum_rules: Dict[str, StardewRule] = {}
|
||||||
self.festival_rules: Dict[str, StardewRule] = {}
|
self.festival_rules: Dict[str, StardewRule] = {}
|
||||||
self.quest_rules: Dict[str, StardewRule] = {}
|
self.quest_rules: Dict[str, StardewRule] = {}
|
||||||
self.building_rules: Dict[str, StardewRule] = {}
|
|
||||||
self.special_order_rules: Dict[str, StardewRule] = {}
|
self.special_order_rules: Dict[str, StardewRule] = {}
|
||||||
|
|
||||||
self.sve_location_rules: Dict[str, StardewRule] = {}
|
self.sve_location_rules: Dict[str, StardewRule] = {}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
|
import typing
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Dict, Union
|
from typing import Union
|
||||||
|
|
||||||
from Utils import cache_self1
|
from Utils import cache_self1
|
||||||
from .base_logic import BaseLogic, BaseLogicMixin
|
from .base_logic import BaseLogic, BaseLogicMixin
|
||||||
from .has_logic import HasLogicMixin
|
from .has_logic import HasLogicMixin
|
||||||
from .money_logic import MoneyLogicMixin
|
|
||||||
from .received_logic import ReceivedLogicMixin
|
from .received_logic import ReceivedLogicMixin
|
||||||
from .region_logic import RegionLogicMixin
|
from .region_logic import RegionLogicMixin
|
||||||
from ..options import BuildingProgression
|
from ..stardew_rule import StardewRule, true_
|
||||||
from ..stardew_rule import StardewRule, True_, False_, Has
|
|
||||||
from ..strings.artisan_good_names import ArtisanGood
|
|
||||||
from ..strings.building_names import Building
|
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
|
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):
|
class BuildingLogicMixin(BaseLogicMixin):
|
||||||
@@ -25,78 +25,38 @@ class BuildingLogicMixin(BaseLogicMixin):
|
|||||||
self.building = BuildingLogic(*args, **kwargs)
|
self.building = BuildingLogic(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, MoneyLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin]]):
|
class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SourceLogicMixin]]):
|
||||||
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)
|
|
||||||
|
|
||||||
@cache_self1
|
@cache_self1
|
||||||
def has_building(self, building: str) -> StardewRule:
|
def can_build(self, building_name: str) -> StardewRule:
|
||||||
# Shipping bin is special. The mod auto-builds it when received, no need to go to Robin.
|
building = self.content.farm_buildings.get(building_name)
|
||||||
if building is Building.shipping_bin:
|
assert building is not None, f"Building {building_name} not found."
|
||||||
if not self.options.building_progression & BuildingProgression.option_progressive:
|
|
||||||
return True_()
|
source_rule = self.logic.source.has_access_to_any(building.sources)
|
||||||
return self.logic.received(building)
|
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
|
carpenter_rule = self.logic.building.can_construct_buildings
|
||||||
if not self.options.building_progression & BuildingProgression.option_progressive:
|
item, count = building_progression.to_progressive_item(building_name)
|
||||||
return Has(building, self.registry.building_rules, has_group) & carpenter_rule
|
return self.logic.received(item, count) & 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
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def can_construct_buildings(self) -> StardewRule:
|
def can_construct_buildings(self) -> StardewRule:
|
||||||
return self.logic.region.can_reach(Region.carpenter)
|
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 ..data.recipe_source import CutsceneSource, ShopTradeSource
|
||||||
from ..options import Chefsanity
|
from ..options import Chefsanity
|
||||||
from ..stardew_rule import StardewRule, True_, False_
|
from ..stardew_rule import StardewRule, True_, False_
|
||||||
|
from ..strings.building_names import Building
|
||||||
from ..strings.region_names import LogicRegion
|
from ..strings.region_names import LogicRegion
|
||||||
from ..strings.skill_names import Skill
|
from ..strings.skill_names import Skill
|
||||||
from ..strings.tv_channel_names import Channel
|
from ..strings.tv_channel_names import Channel
|
||||||
@@ -32,7 +33,7 @@ class CookingLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogi
|
|||||||
BuildingLogicMixin, RelationshipLogicMixin, SkillLogicMixin, CookingLogicMixin]]):
|
BuildingLogicMixin, RelationshipLogicMixin, SkillLogicMixin, CookingLogicMixin]]):
|
||||||
@cached_property
|
@cached_property
|
||||||
def can_cook_in_kitchen(self) -> StardewRule:
|
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
|
# Should be cached
|
||||||
def can_cook(self, recipe: CookingRecipe = None) -> StardewRule:
|
def can_cook(self, recipe: CookingRecipe = None) -> StardewRule:
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class GoalLogic(BaseLogic[StardewLogic]):
|
|||||||
self.logic.museum.can_complete_museum(),
|
self.logic.museum.can_complete_museum(),
|
||||||
# Catching every fish not expected
|
# Catching every fish not expected
|
||||||
# Shipping every item 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(5, 8), # 5 Friends
|
||||||
self.logic.relationship.has_hearts_with_n(10, 8), # 10 friends
|
self.logic.relationship.has_hearts_with_n(10, 8), # 10 friends
|
||||||
self.logic.pet.has_pet_hearts(5), # Max Pet
|
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.currency_names import Currency
|
||||||
from ..strings.fish_names import WaterChest
|
from ..strings.fish_names import WaterChest
|
||||||
from ..strings.geode_names import Geode
|
from ..strings.geode_names import Geode
|
||||||
|
from ..strings.material_names import Material
|
||||||
from ..strings.region_names import Region
|
from ..strings.region_names import Region
|
||||||
from ..strings.tool_names import Tool
|
from ..strings.tool_names import Tool
|
||||||
|
|
||||||
@@ -21,9 +22,14 @@ if TYPE_CHECKING:
|
|||||||
else:
|
else:
|
||||||
ToolLogicMixin = object
|
ToolLogicMixin = object
|
||||||
|
|
||||||
MIN_ITEMS = 10
|
MIN_MEDIUM_ITEMS = 10
|
||||||
MAX_ITEMS = 999
|
MAX_MEDIUM_ITEMS = 999
|
||||||
PERCENT_REQUIRED_FOR_MAX_ITEM = 24
|
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):
|
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.
|
# 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)
|
time_rule = self.logic.time.has_lived_months(quantity // 14)
|
||||||
return self.logic.and_(opening_rule, mystery_box_rule,
|
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:
|
def can_grind_artifact_troves(self, quantity: int) -> StardewRule:
|
||||||
opening_rule = self.logic.region.can_reach(Region.blacksmith)
|
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.
|
# Assuming twelve per month if the player does not grind it.
|
||||||
self.logic.time.has_lived_months(quantity // 12))
|
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
|
@cache_self1
|
||||||
def can_grind_item(self, quantity: int) -> StardewRule:
|
def can_grind_medium_item(self, quantity: int) -> StardewRule:
|
||||||
if quantity <= MIN_ITEMS:
|
if quantity <= MIN_MEDIUM_ITEMS:
|
||||||
return self.logic.true_
|
return self.logic.true_
|
||||||
|
|
||||||
quantity = min(quantity, MAX_ITEMS)
|
quantity = min(quantity, MAX_MEDIUM_ITEMS)
|
||||||
price = max(1, quantity * PERCENT_REQUIRED_FOR_MAX_ITEM // MAX_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)
|
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),
|
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.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.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.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.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,
|
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_()
|
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.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.initialize_rules()
|
||||||
self.quest.update_rules(self.mod.quest.get_modded_quest_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:
|
if typing.TYPE_CHECKING:
|
||||||
from .shipping_logic import ShippingLogicMixin
|
from .shipping_logic import ShippingLogicMixin
|
||||||
|
else:
|
||||||
assert ShippingLogicMixin
|
ShippingLogicMixin = object
|
||||||
|
|
||||||
qi_gem_rewards = ("100 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems", "25 Qi Gems",
|
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")
|
"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,
|
class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SeasonLogicMixin,
|
||||||
GrindLogicMixin, 'ShippingLogicMixin']]):
|
GrindLogicMixin, ShippingLogicMixin]]):
|
||||||
|
|
||||||
@cache_self1
|
@cache_self1
|
||||||
def can_have_earned_total(self, amount: int) -> StardewRule:
|
def can_have_earned_total(self, amount: int) -> StardewRule:
|
||||||
@@ -80,7 +80,7 @@ GrindLogicMixin, 'ShippingLogicMixin']]):
|
|||||||
item_rules = []
|
item_rules = []
|
||||||
if source.items_price is not None:
|
if source.items_price is not None:
|
||||||
for price, item in source.items_price:
|
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)
|
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 ..data.villagers_data import Villager
|
||||||
from ..stardew_rule import StardewRule, True_, false_, true_
|
from ..stardew_rule import StardewRule, True_, false_, true_
|
||||||
from ..strings.ap_names.mods.mod_items import SVEQuestItem
|
from ..strings.ap_names.mods.mod_items import SVEQuestItem
|
||||||
|
from ..strings.building_names import Building
|
||||||
from ..strings.generic_names import Generic
|
from ..strings.generic_names import Generic
|
||||||
from ..strings.gift_names import Gift
|
from ..strings.gift_names import Gift
|
||||||
from ..strings.quest_names import ModQuest
|
|
||||||
from ..strings.region_names import Region
|
from ..strings.region_names import Region
|
||||||
from ..strings.season_names import Season
|
from ..strings.season_names import Season
|
||||||
from ..strings.villager_names import NPC, ModNPC
|
from ..strings.villager_names import NPC, ModNPC
|
||||||
@@ -63,7 +63,7 @@ ReceivedLogicMixin, HasLogicMixin, ModLogicMixin]]):
|
|||||||
if not self.content.features.friendsanity.is_enabled:
|
if not self.content.features.friendsanity.is_enabled:
|
||||||
return self.logic.relationship.can_reproduce(number_children)
|
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:
|
def can_reproduce(self, number_children: int = 1) -> StardewRule:
|
||||||
assert number_children >= 0, "Can't have a negative amount of children."
|
assert number_children >= 0, "Can't have a negative amount of children."
|
||||||
@@ -71,7 +71,7 @@ ReceivedLogicMixin, HasLogicMixin, ModLogicMixin]]):
|
|||||||
return True_()
|
return True_()
|
||||||
|
|
||||||
baby_rules = [self.logic.relationship.can_get_married(),
|
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_hearts_with_any_bachelor(12),
|
||||||
self.logic.relationship.has_children(number_children - 1)]
|
self.logic.relationship.has_children(number_children - 1)]
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from .fishing_logic import FishingLogicMixin
|
|||||||
from .has_logic import HasLogicMixin
|
from .has_logic import HasLogicMixin
|
||||||
from .quest_logic import QuestLogicMixin
|
from .quest_logic import QuestLogicMixin
|
||||||
from .received_logic import ReceivedLogicMixin
|
from .received_logic import ReceivedLogicMixin
|
||||||
|
from .region_logic import RegionLogicMixin
|
||||||
from .relationship_logic import RelationshipLogicMixin
|
from .relationship_logic import RelationshipLogicMixin
|
||||||
from .season_logic import SeasonLogicMixin
|
from .season_logic import SeasonLogicMixin
|
||||||
from .skill_logic import SkillLogicMixin
|
from .skill_logic import SkillLogicMixin
|
||||||
@@ -16,7 +17,7 @@ from .tool_logic import ToolLogicMixin
|
|||||||
from .walnut_logic import WalnutLogicMixin
|
from .walnut_logic import WalnutLogicMixin
|
||||||
from ..data.game_item import Requirement
|
from ..data.game_item import Requirement
|
||||||
from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, CombatRequirement, QuestRequirement, \
|
from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, CombatRequirement, QuestRequirement, \
|
||||||
RelationshipRequirement, FishingRequirement, WalnutRequirement
|
RelationshipRequirement, FishingRequirement, WalnutRequirement, RegionRequirement
|
||||||
|
|
||||||
|
|
||||||
class RequirementLogicMixin(BaseLogicMixin):
|
class RequirementLogicMixin(BaseLogicMixin):
|
||||||
@@ -26,7 +27,7 @@ class RequirementLogicMixin(BaseLogicMixin):
|
|||||||
|
|
||||||
|
|
||||||
class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin,
|
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]):
|
def meet_all_requirements(self, requirements: Iterable[Requirement]):
|
||||||
if not requirements:
|
if not requirements:
|
||||||
@@ -45,6 +46,10 @@ SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, Relationshi
|
|||||||
def _(self, requirement: SkillRequirement):
|
def _(self, requirement: SkillRequirement):
|
||||||
return self.logic.skill.has_level(requirement.skill, requirement.level)
|
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
|
@meet_requirement.register
|
||||||
def _(self, requirement: BookRequirement):
|
def _(self, requirement: BookRequirement):
|
||||||
return self.logic.book.has_book_power(requirement.book)
|
return self.logic.book.has_book_power(requirement.book)
|
||||||
@@ -76,5 +81,3 @@ SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, Relationshi
|
|||||||
@meet_requirement.register
|
@meet_requirement.register
|
||||||
def _(self, requirement: FishingRequirement):
|
def _(self, requirement: FishingRequirement):
|
||||||
return self.logic.fishing.can_fish_at(requirement.region)
|
return self.logic.fishing.can_fish_at(requirement.region)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from .region_logic import RegionLogicMixin
|
|||||||
from .requirement_logic import RequirementLogicMixin
|
from .requirement_logic import RequirementLogicMixin
|
||||||
from .tool_logic import ToolLogicMixin
|
from .tool_logic import ToolLogicMixin
|
||||||
from ..data.artisan import MachineSource
|
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, \
|
from ..data.harvest import ForagingSource, FruitBatsSource, MushroomCaveSource, SeasonalForagingSource, \
|
||||||
HarvestCropSource, HarvestFruitTreeSource, ArtifactSpotSource
|
HarvestCropSource, HarvestFruitTreeSource, ArtifactSpotSource
|
||||||
from ..data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
|
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,
|
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):
|
def has_access_to_item(self, item: GameItem):
|
||||||
rules = []
|
rules = []
|
||||||
@@ -36,14 +36,10 @@ class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogic
|
|||||||
rules.append(self.logic.source.has_access_to_any(item.sources))
|
rules.append(self.logic.source.has_access_to_any(item.sources))
|
||||||
return self.logic.and_(*rules)
|
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)
|
return self.logic.or_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements)
|
||||||
for source in sources))
|
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
|
@functools.singledispatchmethod
|
||||||
def has_access_to(self, source: Any):
|
def has_access_to(self, source: Any):
|
||||||
raise ValueError(f"Sources of type{type(source)} have no rule registered.")
|
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):
|
def _(self, source: CustomRuleSource):
|
||||||
return source.create_rule(self.logic)
|
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
|
@has_access_to.register
|
||||||
def _(self, source: ForagingSource):
|
def _(self, source: ForagingSource):
|
||||||
return self.logic.harvesting.can_forage_from(source)
|
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 .deepwoods_logic import DeepWoodsLogicMixin
|
||||||
from .elevator_logic import ModElevatorLogicMixin
|
from .elevator_logic import ModElevatorLogicMixin
|
||||||
from .item_logic import ModItemLogicMixin
|
from .item_logic import ModItemLogicMixin
|
||||||
@@ -16,6 +15,6 @@ class ModLogicMixin(BaseLogicMixin):
|
|||||||
self.mod = ModLogic(*args, **kwargs)
|
self.mod = ModLogic(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ModLogic(ModElevatorLogicMixin, MagicLogicMixin, ModSkillLogicMixin, ModItemLogicMixin, ModQuestLogicMixin, ModBuildingLogicMixin,
|
class ModLogic(ModElevatorLogicMixin, MagicLogicMixin, ModSkillLogicMixin, ModItemLogicMixin, ModQuestLogicMixin,
|
||||||
ModSpecialOrderLogicMixin, DeepWoodsLogicMixin, SVELogicMixin):
|
ModSpecialOrderLogicMixin, DeepWoodsLogicMixin, SVELogicMixin):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ class BuildingProgression(Choice):
|
|||||||
Progressive: You will receive the buildings and will be able to build the first one of each type for free,
|
Progressive: You will receive the buildings and will be able to build the first one of each type for free,
|
||||||
once it is received. If you want more of the same building, it will cost the vanilla price.
|
once it is received. If you want more of the same building, it will cost the vanilla price.
|
||||||
Cheap: Buildings will have a 50% discount
|
Cheap: Buildings will have a 50% discount
|
||||||
Very Cheap: Buildings will an 80% discount
|
Very Cheap: Buildings will have an 80% discount
|
||||||
"""
|
"""
|
||||||
internal_name = "building_progression"
|
internal_name = "building_progression"
|
||||||
display_name = "Building Progression"
|
display_name = "Building Progression"
|
||||||
@@ -435,7 +435,7 @@ class Museumsanity(Choice):
|
|||||||
class Monstersanity(Choice):
|
class Monstersanity(Choice):
|
||||||
"""Locations for slaying monsters?
|
"""Locations for slaying monsters?
|
||||||
None: There are no checks for slaying monsters
|
None: There are no checks for slaying monsters
|
||||||
One per category: Every category visible at the adventure guild gives one check
|
One per Category: Every category visible at the adventure guild gives one check
|
||||||
One per Monster: Every unique monster gives one check
|
One per Monster: Every unique monster gives one check
|
||||||
Monster Eradication Goals: The Monster Eradication Goals each contain one check
|
Monster Eradication Goals: The Monster Eradication Goals each contain one check
|
||||||
Short Monster Eradication Goals: The Monster Eradication Goals each contain one check, but are reduced by 60%
|
Short Monster Eradication Goals: The Monster Eradication Goals each contain one check, but are reduced by 60%
|
||||||
@@ -498,7 +498,7 @@ class Cooksanity(Choice):
|
|||||||
class Chefsanity(NamedRange):
|
class Chefsanity(NamedRange):
|
||||||
"""Locations for learning cooking recipes?
|
"""Locations for learning cooking recipes?
|
||||||
Vanilla: All cooking recipes are learned normally
|
Vanilla: All cooking recipes are learned normally
|
||||||
Queen of Sauce: Every Queen of sauce episode is a check, all queen of sauce recipes are items
|
Queen of Sauce: Every Queen of Sauce episode is a check, all Queen of Sauce recipes are items
|
||||||
Purchases: Every purchasable recipe is a check
|
Purchases: Every purchasable recipe is a check
|
||||||
Friendship: Recipes obtained from friendship are checks
|
Friendship: Recipes obtained from friendship are checks
|
||||||
Skills: Recipes obtained from skills are checks
|
Skills: Recipes obtained from skills are checks
|
||||||
@@ -589,7 +589,7 @@ class Booksanity(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class Walnutsanity(OptionSet):
|
class Walnutsanity(OptionSet):
|
||||||
"""Shuffle walnuts?
|
"""Shuffle Walnuts?
|
||||||
Puzzles: Walnuts obtained from solving a special puzzle or winning a minigame
|
Puzzles: Walnuts obtained from solving a special puzzle or winning a minigame
|
||||||
Bushes: Walnuts that are in a bush and can be collected by clicking it
|
Bushes: Walnuts that are in a bush and can be collected by clicking it
|
||||||
Dig Spots: Walnuts that are underground and must be digged up. Includes Journal scrap walnuts
|
Dig Spots: Walnuts that are underground and must be digged up. Includes Journal scrap walnuts
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user