diff --git a/AdventureClient.py b/AdventureClient.py index 206c55df9abd..24c6a4c4fc58 100644 --- a/AdventureClient.py +++ b/AdventureClient.py @@ -112,7 +112,7 @@ def on_package(self, cmd: str, args: dict): if ': !' not in msg: self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "ReceivedItems": - msg = f"Received {', '.join([self.item_names.lookup_in_slot(item.item) for item in args['items']])}" + msg = f"Received {', '.join([self.item_names.lookup_in_game(item.item) for item in args['items']])}" self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "Retrieved": if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]: diff --git a/CommonClient.py b/CommonClient.py index 8f1e64c0591b..f8d1fcb7a221 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -23,7 +23,7 @@ from MultiServer import CommandProcessor from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot, - RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes) + RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, SlotType) from Utils import Version, stream_input, async_start from worlds import network_data_package, AutoWorldRegister import os @@ -225,6 +225,9 @@ def lookup_in_game(self, code: int, game_name: typing.Optional[str] = None) -> s def lookup_in_slot(self, code: int, slot: typing.Optional[int] = None) -> str: """Returns the name for an item/location id in the context of a specific slot or own slot if `slot` is omitted. + + Use of `lookup_in_slot` should not be used when not connected to a server. If looking in own game, set + `ctx.game` and use `lookup_in_game` method instead. """ if slot is None: slot = self.ctx.slot @@ -859,7 +862,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict): ctx.team = args["team"] ctx.slot = args["slot"] # int keys get lost in JSON transfer - ctx.slot_info = {int(pid): data for pid, data in args["slot_info"].items()} + ctx.slot_info = {0: NetworkSlot("Archipelago", "Archipelago", SlotType.player)} + ctx.slot_info.update({int(pid): data for pid, data in args["slot_info"].items()}) ctx.hint_points = args.get("hint_points", 0) ctx.consume_players_package(args["players"]) ctx.stored_data_notification_keys.add(f"_read_hints_{ctx.team}_{ctx.slot}") diff --git a/Generate.py b/Generate.py index 1fbb9e76a483..d7dd6523e7f1 100644 --- a/Generate.py +++ b/Generate.py @@ -65,7 +65,7 @@ def get_seed_name(random_source) -> str: return f"{random_source.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits) -def main(args=None): +def main(args=None) -> Tuple[argparse.Namespace, int]: # __name__ == "__main__" check so unittests that already imported worlds don't trip this. if __name__ == "__main__" and "worlds" in sys.modules: raise Exception("Worlds system should not be loaded before logging init.") @@ -237,8 +237,7 @@ def main(args=None): with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f: yaml.dump(important, f) - from Main import main as ERmain - return ERmain(erargs, seed) + return erargs, seed def read_weights_yamls(path) -> Tuple[Any, ...]: @@ -547,7 +546,9 @@ def roll_alttp_settings(ret: argparse.Namespace, weights): if __name__ == '__main__': import atexit confirmation = atexit.register(input, "Press enter to close.") - multiworld = main() + erargs, seed = main() + from Main import main as ERmain + multiworld = ERmain(erargs, seed) if __debug__: import gc import sys diff --git a/MultiServer.py b/MultiServer.py index dc5e3d21ac89..f59855fca6a4 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1352,7 +1352,7 @@ def _cmd_remaining(self) -> bool: if self.ctx.remaining_mode == "enabled": remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1365,7 +1365,7 @@ def _cmd_remaining(self) -> bool: if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") diff --git a/Options.py b/Options.py index 2d3ef99d6491..b5fb25ea34a0 100644 --- a/Options.py +++ b/Options.py @@ -53,8 +53,8 @@ def __new__(mcs, name, bases, attrs): attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()}) options.update(new_options) # apply aliases, without name_lookup - aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if - name.startswith("alias_")} + aliases = attrs["aliases"] = {name[6:].lower(): option_id for name, option_id in attrs.items() if + name.startswith("alias_")} assert ( name in {"Option", "VerifyKeys"} or # base abstract classes don't need default @@ -126,10 +126,28 @@ class Option(typing.Generic[T], metaclass=AssembleOptions): # can be weighted between selections supports_weighting = True + rich_text_doc: typing.Optional[bool] = None + """Whether the WebHost should render the Option's docstring as rich text. + + If this is True, the Option's docstring is interpreted as reStructuredText_, + the standard Python markup format. In the WebHost, it's rendered to HTML so + that lists, emphasis, and other rich text features are displayed properly. + + If this is False, the docstring is instead interpreted as plain text, and + displayed as-is on the WebHost with whitespace preserved. + + If this is None, it inherits the value of `World.rich_text_options_doc`. For + backwards compatibility, this defaults to False, but worlds are encouraged to + set it to True and use reStructuredText for their Option documentation. + + .. _reStructuredText: https://docutils.sourceforge.io/rst.html + """ + # filled by AssembleOptions: name_lookup: typing.ClassVar[typing.Dict[T, str]] # type: ignore # https://github.com/python/typing/discussions/1460 the reason for this type: ignore options: typing.ClassVar[typing.Dict[str, int]] + aliases: typing.ClassVar[typing.Dict[str, int]] def __repr__(self) -> str: return f"{self.__class__.__name__}({self.current_option_name})" @@ -1127,10 +1145,13 @@ def __len__(self) -> int: class Accessibility(Choice): """Set rules for reachability of your items/locations. - Locations: ensure everything can be reached and acquired. - Items: ensure all logically relevant items can be acquired. - Minimal: ensure what is needed to reach your goal can be acquired.""" + + - **Locations:** ensure everything can be reached and acquired. + - **Items:** ensure all logically relevant items can be acquired. + - **Minimal:** ensure what is needed to reach your goal can be acquired. + """ display_name = "Accessibility" + rich_text_doc = True option_locations = 0 option_items = 1 option_minimal = 2 @@ -1139,14 +1160,15 @@ class Accessibility(Choice): class ProgressionBalancing(NamedRange): - """ - A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. + """A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. + A lower setting means more getting stuck. A higher setting means less getting stuck. """ default = 50 range_start = 0 range_end = 99 display_name = "Progression Balancing" + rich_text_doc = True special_range_names = { "disabled": 0, "normal": 50, @@ -1211,29 +1233,36 @@ def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, class LocalItems(ItemSet): """Forces these items to be in their native world.""" display_name = "Local Items" + rich_text_doc = True class NonLocalItems(ItemSet): """Forces these items to be outside their native world.""" display_name = "Non-local Items" + rich_text_doc = True class StartInventory(ItemDict): """Start with these items.""" verify_item_name = True display_name = "Start Inventory" + rich_text_doc = True class StartInventoryPool(StartInventory): """Start with these items and don't place them in the world. - The game decides what the replacement items will be.""" + + The game decides what the replacement items will be. + """ verify_item_name = True display_name = "Start Inventory from Pool" + rich_text_doc = True class StartHints(ItemSet): - """Start with these item's locations prefilled into the !hint command.""" + """Start with these item's locations prefilled into the ``!hint`` command.""" display_name = "Start Hints" + rich_text_doc = True class LocationSet(OptionSet): @@ -1242,28 +1271,33 @@ class LocationSet(OptionSet): class StartLocationHints(LocationSet): - """Start with these locations and their item prefilled into the !hint command""" + """Start with these locations and their item prefilled into the ``!hint`` command.""" display_name = "Start Location Hints" + rich_text_doc = True class ExcludeLocations(LocationSet): - """Prevent these locations from having an important item""" + """Prevent these locations from having an important item.""" display_name = "Excluded Locations" + rich_text_doc = True class PriorityLocations(LocationSet): - """Prevent these locations from having an unimportant item""" + """Prevent these locations from having an unimportant item.""" display_name = "Priority Locations" + rich_text_doc = True class DeathLink(Toggle): """When you die, everyone dies. Of course the reverse is true too.""" display_name = "Death Link" + rich_text_doc = True class ItemLinks(OptionList): """Share part of your item pool with other players.""" display_name = "Item Links" + rich_text_doc = True default = [] schema = Schema([ { @@ -1330,6 +1364,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P class Removed(FreeText): """This Option has been Removed.""" + rich_text_doc = True default = "" visibility = Visibility.none @@ -1432,14 +1467,18 @@ def dictify_range(option: Range): return data, notes + def yaml_dump_scalar(scalar) -> str: + # yaml dump may add end of document marker and newlines. + return yaml.dump(scalar).replace("...\n", "").strip() + for game_name, world in AutoWorldRegister.world_types.items(): if not world.hidden or generate_hidden: - grouped_options = get_option_groups(world) + option_groups = get_option_groups(world) with open(local_path("data", "options.yaml")) as f: file_data = f.read() res = Template(file_data).render( - option_groups=grouped_options, - __version__=__version__, game=game_name, yaml_dump=yaml.dump, + option_groups=option_groups, + __version__=__version__, game=game_name, yaml_dump=yaml_dump_scalar, dictify_range=dictify_range, ) diff --git a/UndertaleClient.py b/UndertaleClient.py index cdc21c561ab8..dfacee148abc 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -29,7 +29,7 @@ def _cmd_resync(self): def _cmd_patch(self): """Patch the game. Only use this command if /auto_patch fails.""" if isinstance(self.ctx, UndertaleContext): - os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True) + os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True) self.ctx.patch_game() self.output("Patched.") @@ -43,7 +43,7 @@ def _cmd_savepath(self, directory: str): def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None): """Patch the game automatically.""" if isinstance(self.ctx, UndertaleContext): - os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True) + os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True) tempInstall = steaminstall if not os.path.isfile(os.path.join(tempInstall, "data.win")): tempInstall = None @@ -62,7 +62,7 @@ def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None): for file_name in os.listdir(tempInstall): if file_name != "steam_api.dll": shutil.copy(os.path.join(tempInstall, file_name), - os.path.join(os.getcwd(), "Undertale", file_name)) + Utils.user_path("Undertale", file_name)) self.ctx.patch_game() self.output("Patching successful!") @@ -111,12 +111,12 @@ def __init__(self, server_address, password): self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE") def patch_game(self): - with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "rb") as f: + with open(Utils.user_path("Undertale", "data.win"), "rb") as f: patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff")) - with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "wb") as f: + with open(Utils.user_path("Undertale", "data.win"), "wb") as f: f.write(patchedFile) - os.makedirs(name=os.path.join(os.getcwd(), "Undertale", "Custom Sprites"), exist_ok=True) - with open(os.path.expandvars(os.path.join(os.getcwd(), "Undertale", "Custom Sprites", + os.makedirs(name=Utils.user_path("Undertale", "Custom Sprites"), exist_ok=True) + with open(os.path.expandvars(Utils.user_path("Undertale", "Custom Sprites", "Which Character.txt")), "w") as f: f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only " "line other than this one.\n", "frisk"]) @@ -247,8 +247,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict): with open(os.path.join(ctx.save_game_folder, filename), "w") as f: toDraw = "" for i in range(20): - if i < len(str(ctx.item_names.lookup_in_slot(l.item))): - toDraw += str(ctx.item_names.lookup_in_slot(l.item))[i] + if i < len(str(ctx.item_names.lookup_in_game(l.item))): + toDraw += str(ctx.item_names.lookup_in_game(l.item))[i] else: break f.write(toDraw) diff --git a/WargrooveClient.py b/WargrooveClient.py index c5fdeb3532f5..39da044d659c 100644 --- a/WargrooveClient.py +++ b/WargrooveClient.py @@ -176,7 +176,7 @@ def on_package(self, cmd: str, args: dict): if not os.path.isfile(path): open(path, 'w').close() # Announcing commander unlocks - item_name = self.item_names.lookup_in_slot(network_item.item) + item_name = self.item_names.lookup_in_game(network_item.item) if item_name in faction_table.keys(): for commander in faction_table[item_name]: logger.info(f"{commander.name} has been unlocked!") @@ -197,7 +197,7 @@ def on_package(self, cmd: str, args: dict): open(print_path, 'w').close() with open(print_path, 'w') as f: f.write("Received " + - self.item_names.lookup_in_slot(network_item.item) + + self.item_names.lookup_in_game(network_item.item) + " from " + self.player_names[network_item.player]) f.close() @@ -342,7 +342,7 @@ def update_commander_data(self): faction_items = 0 faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()] for network_item in self.items_received: - if self.item_names.lookup_in_slot(network_item.item) in faction_item_names: + if self.item_names.lookup_in_game(network_item.item) in faction_item_names: faction_items += 1 starting_groove = (faction_items - 1) * self.starting_groove_multiplier # Must be an integer larger than 0 diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 9f70165b61e5..ccffc40b384d 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -325,10 +325,12 @@ def _done(self, task: asyncio.Future): def run(self): while 1: next_room = rooms_to_run.get(block=True, timeout=None) + gc.collect(0) task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop) self._tasks.append(task) task.add_done_callback(self._done) logging.info(f"Starting room {next_room} on {name}.") + del task # delete reference to task object starter = Starter() starter.daemon = True diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index 5072f113bd54..01c1ad84a707 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -1,6 +1,6 @@ import datetime import os -from typing import List, Dict, Union +from typing import Any, IO, Dict, Iterator, List, Tuple, Union import jinja2.exceptions from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory @@ -97,25 +97,37 @@ def new_room(seed: UUID): return redirect(url_for("host_room", room=room.id)) -def _read_log(path: str): - if os.path.exists(path): - with open(path, encoding="utf-8-sig") as log: - yield from log - else: - yield f"Logfile {path} does not exist. " \ - f"Likely a crash during spinup of multiworld instance or it is still spinning up." +def _read_log(log: IO[Any], offset: int = 0) -> Iterator[bytes]: + marker = log.read(3) # skip optional BOM + if marker != b'\xEF\xBB\xBF': + log.seek(0, os.SEEK_SET) + log.seek(offset, os.SEEK_CUR) + yield from log + log.close() # free file handle as soon as possible @app.route('/log/') -def display_log(room: UUID): +def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]: room = Room.get(id=room) if room is None: return abort(404) if room.owner == session["_id"]: file_path = os.path.join("logs", str(room.id) + ".txt") - if os.path.exists(file_path): - return Response(_read_log(file_path), mimetype="text/plain;charset=UTF-8") - return "Log File does not exist." + try: + log = open(file_path, "rb") + range_header = request.headers.get("Range") + if range_header: + range_type, range_values = range_header.split('=') + start, end = map(str.strip, range_values.split('-', 1)) + if range_type != "bytes" or end != "": + return "Unsupported range", 500 + # NOTE: we skip Content-Range in the response here, which isn't great but works for our JS + return Response(_read_log(log, int(start)), mimetype="text/plain", status=206) + return Response(_read_log(log), mimetype="text/plain") + except FileNotFoundError: + return Response(f"Logfile {file_path} does not exist. " + f"Likely a crash during spinup of multiworld instance or it is still spinning up.", + mimetype="text/plain") return "Access Denied", 403 @@ -139,7 +151,22 @@ def host_room(room: UUID): with db_session: room.last_activity = now # will trigger a spinup, if it's not already running - return render_template("hostRoom.html", room=room, should_refresh=should_refresh) + def get_log(max_size: int = 1024000) -> str: + try: + with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log: + raw_size = 0 + fragments: List[str] = [] + for block in _read_log(log): + if raw_size + len(block) > max_size: + fragments.append("…") + break + raw_size += len(block) + fragments.append(block.decode("utf-8")) + return "".join(fragments) + except FileNotFoundError: + return "" + + return render_template("hostRoom.html", room=room, should_refresh=should_refresh, get_log=get_log) @app.route('/favicon.ico') diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 53c3a6151b82..33339daa1983 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -3,6 +3,7 @@ import os from textwrap import dedent from typing import Dict, Union +from docutils.core import publish_parts import yaml from flask import redirect, render_template, request, Response @@ -66,6 +67,22 @@ def filter_dedent(text: str) -> str: return dedent(text).strip("\n ") +@app.template_filter("rst_to_html") +def filter_rst_to_html(text: str) -> str: + """Converts reStructuredText (such as a Python docstring) to HTML.""" + if text.startswith(" ") or text.startswith("\t"): + text = dedent(text) + elif "\n" in text: + lines = text.splitlines() + text = lines[0] + "\n" + dedent("\n".join(lines[1:])) + + return publish_parts(text, writer_name='html', settings=None, settings_overrides={ + 'raw_enable': False, + 'file_insertion_enabled': False, + 'output_encoding': 'unicode' + })['body'] + + @app.template_test("ordered") def test_ordered(obj): return isinstance(obj, collections.abc.Sequence) diff --git a/WebHostLib/robots.py b/WebHostLib/robots.py index 410a92c8238c..93c735c71015 100644 --- a/WebHostLib/robots.py +++ b/WebHostLib/robots.py @@ -8,7 +8,8 @@ def robots(): # If this host is not official, do not allow search engine crawling if not app.config["ASSET_RIGHTS"]: - return app.send_static_file('robots.txt') + # filename changed in case the path is intercepted and served by an outside service + return app.send_static_file('robots_file.txt') # Send 404 if the host has affirmed this to be the official WebHost abort(404) diff --git a/WebHostLib/static/robots.txt b/WebHostLib/static/robots_file.txt similarity index 100% rename from WebHostLib/static/robots.txt rename to WebHostLib/static/robots_file.txt diff --git a/WebHostLib/static/styles/tooltip.css b/WebHostLib/static/styles/tooltip.css index 02992b188bc9..dc9026ce6c3d 100644 --- a/WebHostLib/static/styles/tooltip.css +++ b/WebHostLib/static/styles/tooltip.css @@ -12,12 +12,12 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, */ /* Base styles for the element that has a tooltip */ -[data-tooltip], .tooltip { +[data-tooltip], .tooltip-container { position: relative; } /* Base styles for the entire tooltip */ -[data-tooltip]:before, [data-tooltip]:after, .tooltip:before, .tooltip:after { +[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip { position: absolute; visibility: hidden; opacity: 0; @@ -39,14 +39,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, pointer-events: none; } -[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after{ +[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before, +.tooltip-container:hover .tooltip { visibility: visible; opacity: 1; word-break: break-word; } /** Directional arrow styles */ -.tooltip:before, [data-tooltip]:before { +[data-tooltip]:before, .tooltip-container:before { z-index: 10000; border: 6px solid transparent; background: transparent; @@ -54,7 +55,7 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, } /** Content styles */ -.tooltip:after, [data-tooltip]:after { +[data-tooltip]:after, .tooltip { width: 260px; z-index: 10000; padding: 8px; @@ -63,24 +64,26 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, background-color: hsla(0, 0%, 20%, 0.9); color: #fff; content: attr(data-tooltip); - white-space: pre-wrap; font-size: 14px; line-height: 1.2; } -[data-tooltip]:before, [data-tooltip]:after{ +[data-tooltip]:after { + white-space: pre-wrap; +} + +[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip { visibility: hidden; opacity: 0; pointer-events: none; } -[data-tooltip]:before, [data-tooltip]:after, .tooltip:before, .tooltip:after, -.tooltip-top:before, .tooltip-top:after { +[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip { bottom: 100%; left: 50%; } -[data-tooltip]:before, .tooltip:before, .tooltip-top:before { +[data-tooltip]:before, .tooltip-container:before { margin-left: -6px; margin-bottom: -12px; border-top-color: #000; @@ -88,19 +91,19 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, } /** Horizontally align tooltips on the top and bottom */ -[data-tooltip]:after, .tooltip:after, .tooltip-top:after { +[data-tooltip]:after, .tooltip { margin-left: -80px; } -[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after, -.tooltip-top:hover:before, .tooltip-top:hover:after { +[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before, +.tooltip-container:hover .tooltip { -webkit-transform: translateY(-12px); -moz-transform: translateY(-12px); transform: translateY(-12px); } /** Tooltips on the left */ -.tooltip-left:before, .tooltip-left:after { +.tooltip-left:before, [data-tooltip].tooltip-left:after, .tooltip-left .tooltip { right: 100%; bottom: 50%; left: auto; @@ -115,14 +118,14 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, border-left-color: hsla(0, 0%, 20%, 0.9); } -.tooltip-left:hover:before, .tooltip-left:hover:after { +.tooltip-left:hover:before, [data-tooltip].tooltip-left:hover:after, .tooltip-left:hover .tooltip { -webkit-transform: translateX(-12px); -moz-transform: translateX(-12px); transform: translateX(-12px); } /** Tooltips on the bottom */ -.tooltip-bottom:before, .tooltip-bottom:after { +.tooltip-bottom:before, [data-tooltip].tooltip-bottom:after, .tooltip-bottom .tooltip { top: 100%; bottom: auto; left: 50%; @@ -136,14 +139,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, border-bottom-color: hsla(0, 0%, 20%, 0.9); } -.tooltip-bottom:hover:before, .tooltip-bottom:hover:after { +.tooltip-bottom:hover:before, [data-tooltip].tooltip-bottom:hover:after, +.tooltip-bottom:hover .tooltip { -webkit-transform: translateY(12px); -moz-transform: translateY(12px); transform: translateY(12px); } /** Tooltips on the right */ -.tooltip-right:before, .tooltip-right:after { +.tooltip-right:before, [data-tooltip].tooltip-right:after, .tooltip-right .tooltip { bottom: 50%; left: 100%; } @@ -156,7 +160,8 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, border-right-color: hsla(0, 0%, 20%, 0.9); } -.tooltip-right:hover:before, .tooltip-right:hover:after { +.tooltip-right:hover:before, [data-tooltip].tooltip-right:hover:after, +.tooltip-right:hover .tooltip { -webkit-transform: translateX(12px); -moz-transform: translateX(12px); transform: translateX(12px); @@ -168,7 +173,16 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, } /** Center content vertically for tooltips ont he left and right */ -.tooltip-left:after, .tooltip-right:after { +[data-tooltip].tooltip-left:after, [data-tooltip].tooltip-right:after, +.tooltip-left .tooltip, .tooltip-right .tooltip { margin-left: 0; margin-bottom: -16px; } + +.tooltip ul, .tooltip ol { + padding-left: 1rem; +} + +.tooltip :last-child { + margin-bottom: 0; +} diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 2bbfe4ad0169..fa8e26c2cbf8 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -44,7 +44,7 @@ {{ macros.list_patches_room(room) }} {% if room.owner == session["_id"] %}
-
+
-
- {% endif %}
diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html index 76cf07699456..415739b861a1 100644 --- a/WebHostLib/templates/playerOptions/macros.html +++ b/WebHostLib/templates/playerOptions/macros.html @@ -111,7 +111,7 @@ {% endmacro %} -{% macro ItemDict(option_name, option, world) %} +{% macro ItemDict(option_name, option) %} {{ OptionTitle(option_name, option) }}
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %} @@ -135,7 +135,7 @@
{% endmacro %} -{% macro LocationSet(option_name, option, world) %} +{% macro LocationSet(option_name, option) %} {{ OptionTitle(option_name, option) }}
{% for group_name in world.location_name_groups.keys()|sort %} @@ -158,7 +158,7 @@
{% endmacro %} -{% macro ItemSet(option_name, option, world) %} +{% macro ItemSet(option_name, option) %} {{ OptionTitle(option_name, option) }}
{% for group_name in world.item_name_groups.keys()|sort %} @@ -196,7 +196,18 @@ {% macro OptionTitle(option_name, option) %} {% endmacro %} diff --git a/WebHostLib/templates/playerOptions/playerOptions.html b/WebHostLib/templates/playerOptions/playerOptions.html index 2506cf9619e6..aeb6e864a54b 100644 --- a/WebHostLib/templates/playerOptions/playerOptions.html +++ b/WebHostLib/templates/playerOptions/playerOptions.html @@ -1,5 +1,5 @@ {% extends 'pageWrapper.html' %} -{% import 'playerOptions/macros.html' as inputs %} +{% import 'playerOptions/macros.html' as inputs with context %} {% block head %} {{ world_name }} Options @@ -94,16 +94,16 @@

Player Options

{{ inputs.FreeText(option_name, option) }} {% elif issubclass(option, Options.ItemDict) and option.verify_item_name %} - {{ inputs.ItemDict(option_name, option, world) }} + {{ inputs.ItemDict(option_name, option) }} {% elif issubclass(option, Options.OptionList) and option.valid_keys %} {{ inputs.OptionList(option_name, option) }} {% elif issubclass(option, Options.LocationSet) and option.verify_location_name %} - {{ inputs.LocationSet(option_name, option, world) }} + {{ inputs.LocationSet(option_name, option) }} {% elif issubclass(option, Options.ItemSet) and option.verify_item_name %} - {{ inputs.ItemSet(option_name, option, world) }} + {{ inputs.ItemSet(option_name, option) }} {% elif issubclass(option, Options.OptionSet) and option.valid_keys %} {{ inputs.OptionSet(option_name, option) }} @@ -134,16 +134,16 @@

Player Options

{{ inputs.FreeText(option_name, option) }} {% elif issubclass(option, Options.ItemDict) and option.verify_item_name %} - {{ inputs.ItemDict(option_name, option, world) }} + {{ inputs.ItemDict(option_name, option) }} {% elif issubclass(option, Options.OptionList) and option.valid_keys %} {{ inputs.OptionList(option_name, option) }} {% elif issubclass(option, Options.LocationSet) and option.verify_location_name %} - {{ inputs.LocationSet(option_name, option, world) }} + {{ inputs.LocationSet(option_name, option) }} {% elif issubclass(option, Options.ItemSet) and option.verify_item_name %} - {{ inputs.ItemSet(option_name, option, world) }} + {{ inputs.ItemSet(option_name, option) }} {% elif issubclass(option, Options.OptionSet) and option.valid_keys %} {{ inputs.OptionSet(option_name, option) }} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 36ebaacbcb0b..8e567afc35fb 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1366,28 +1366,28 @@ def render_Starcraft2_tracker(tracker_data: TrackerData, team: int, player: int) organics_icon_base_url = "https://0rganics.org/archipelago/sc2wol/" icons = { - "Starting Minerals": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-mineral-protoss.png", - "Starting Vespene": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-gas-terran.png", + "Starting Minerals": github_icon_base_url + "blizzard/icon-mineral-nobg.png", + "Starting Vespene": github_icon_base_url + "blizzard/icon-gas-terran-nobg.png", "Starting Supply": github_icon_base_url + "blizzard/icon-supply-terran_nobg.png", - "Terran Infantry Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel1.png", - "Terran Infantry Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel2.png", - "Terran Infantry Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel3.png", - "Terran Infantry Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel1.png", - "Terran Infantry Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel2.png", - "Terran Infantry Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel3.png", - "Terran Vehicle Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel1.png", - "Terran Vehicle Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel2.png", - "Terran Vehicle Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel3.png", - "Terran Vehicle Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel1.png", - "Terran Vehicle Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel2.png", - "Terran Vehicle Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel3.png", - "Terran Ship Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel1.png", - "Terran Ship Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel2.png", - "Terran Ship Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel3.png", - "Terran Ship Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel1.png", - "Terran Ship Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel2.png", - "Terran Ship Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel3.png", + "Terran Infantry Weapons Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryweaponslevel1.png", + "Terran Infantry Weapons Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryweaponslevel2.png", + "Terran Infantry Weapons Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryweaponslevel3.png", + "Terran Infantry Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryarmorlevel1.png", + "Terran Infantry Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryarmorlevel2.png", + "Terran Infantry Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryarmorlevel3.png", + "Terran Vehicle Weapons Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleweaponslevel1.png", + "Terran Vehicle Weapons Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleweaponslevel2.png", + "Terran Vehicle Weapons Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleweaponslevel3.png", + "Terran Vehicle Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleplatinglevel1.png", + "Terran Vehicle Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleplatinglevel2.png", + "Terran Vehicle Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleplatinglevel3.png", + "Terran Ship Weapons Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-shipweaponslevel1.png", + "Terran Ship Weapons Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-shipweaponslevel2.png", + "Terran Ship Weapons Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-shipweaponslevel3.png", + "Terran Ship Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-shipplatinglevel1.png", + "Terran Ship Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-shipplatinglevel2.png", + "Terran Ship Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-shipplatinglevel3.png", "Bunker": "https://static.wikia.nocookie.net/starcraft/images/c/c5/Bunker_SC2_Icon1.jpg", "Missile Turret": "https://static.wikia.nocookie.net/starcraft/images/5/5f/MissileTurret_SC2_Icon1.jpg", diff --git a/Zelda1Client.py b/Zelda1Client.py index 6d7af0a94dcf..1154804fbf56 100644 --- a/Zelda1Client.py +++ b/Zelda1Client.py @@ -152,7 +152,7 @@ def get_payload(ctx: ZeldaContext): def reconcile_shops(ctx: ZeldaContext): - checked_location_names = [ctx.location_names.lookup_in_slot(location) for location in ctx.checked_locations] + checked_location_names = [ctx.location_names.lookup_in_game(location) for location in ctx.checked_locations] shops = [location for location in checked_location_names if "Shop" in location] left_slots = [shop for shop in shops if "Left" in shop] middle_slots = [shop for shop in shops if "Middle" in shop] @@ -190,7 +190,7 @@ async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone= locations_checked = [] location = None for location in ctx.missing_locations: - location_name = ctx.location_names.lookup_in_slot(location) + location_name = ctx.location_names.lookup_in_game(location) if location_name in Locations.overworld_locations and zone == "overworld": status = locations_array[Locations.major_location_offsets[location_name]] diff --git a/data/options.yaml b/data/options.yaml index 8eea75a7cb02..ee8866627d52 100644 --- a/data/options.yaml +++ b/data/options.yaml @@ -68,21 +68,21 @@ requires: {%- elif option.options -%} {%- for suboption_option_id, sub_option_name in option.name_lookup.items() %} - {{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %} + {{ yaml_dump(sub_option_name) }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %} {%- endfor -%} {%- if option.name_lookup[option.default] not in option.options %} - {{ option.default }}: 50 + {{ yaml_dump(option.default) }}: 50 {%- endif -%} {%- elif option.default is string %} - {{ option.default }}: 50 + {{ yaml_dump(option.default) }}: 50 {%- elif option.default is iterable and option.default is not mapping %} {{ option.default | list }} {%- else %} - {{ yaml_dump(option.default) | trim | indent(4, first=false) }} + {{ yaml_dump(option.default) | indent(4, first=false) }} {%- endif -%} {{ "\n" }} {%- endfor %} diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index f27a9797ec20..e0a4794544bb 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -90,9 +90,6 @@ # Lingo /worlds/lingo/ @hatkirby -# Links Awakening DX -/worlds/ladx/ @zig-for - # Lufia II Ancient Cave /worlds/lufia2ac/ @el-u /worlds/lufia2ac/docs/ @wordfcuk @el-u @@ -221,6 +218,8 @@ # Final Fantasy (1) # /worlds/ff1/ +# Links Awakening DX +# /worlds/ladx/ ## Disabled Unmaintained Worlds diff --git a/docs/options api.md b/docs/options api.md index cba383232b67..7e479809ee6a 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -85,6 +85,50 @@ class ExampleWorld(World): options: ExampleGameOptions ``` +### Option Documentation + +Options' [docstrings] are used as their user-facing documentation. They're displayed on the WebHost setup page when a +user hovers over the yellow "(?)" icon, and included in the YAML templates generated for each game. + +[docstrings]: /docs/world%20api.md#docstrings + +The WebHost can display Option documentation either as plain text with all whitespace preserved (other than the base +indentation), or as HTML generated from the standard Python [reStructuredText] format. Although plain text is the +default for backwards compatibility, world authors are encouraged to write their Option documentation as +reStructuredText and enable rich text rendering by setting `World.rich_text_options_doc = True`. + +[reStructuredText]: https://docutils.sourceforge.io/rst.html + +```python +from worlds.AutoWorld import WebWorld + + +class ExampleWebWorld(WebWorld): + # Render all this world's options as rich text. + rich_text_options_doc = True +``` + +You can set a single option to use rich or plain text by setting +`Option.rich_text_doc`. + +```python +from Options import Toggle, Range, Choice, PerGameCommonOptions + + +class Difficulty(Choice): + """Sets overall game difficulty. + + - **Easy:** All enemies die in one hit. + - **Normal:** Enemies and the player both have normal health bars. + - **Hard:** The player dies in one hit.""" + display_name = "Difficulty" + rich_text_doc = True + option_easy = 0 + option_normal = 1 + option_hard = 2 + default = 1 +``` + ### Option Groups Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment diff --git a/docs/world api.md b/docs/world api.md index 37638c3c66cc..6551f2260416 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -56,6 +56,12 @@ webhost: * `options_page` can be changed to a link instead of an AP-generated options page. +* `rich_text_options_doc` controls whether [Option documentation] uses plain text (`False`) or rich text (`True`). It + defaults to `False`, but world authors are encouraged to set it to `True` for nicer-looking documentation that looks + good on both the WebHost and the YAML template. + + [Option documentation]: /docs/options%20api.md#option-documentation + * `theme` to be used for your game-specific AP pages. Available themes: | dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone | @@ -450,8 +456,9 @@ In addition, the following methods can be implemented and are called in this ord called to place player's regions and their locations into the MultiWorld's regions list. If it's hard to separate, this can be done during `generate_early` or `create_items` as well. * `create_items(self)` - called to place player's items into the MultiWorld's itempool. After this step all regions - and items have to be in the MultiWorld's regions and itempool, and these lists should not be modified afterward. + called to place player's items into the MultiWorld's itempool. By the end of this step all regions, locations and + items have to be in the MultiWorld's regions and itempool. You cannot add or remove items, locations, or regions + after this step. Locations cannot be moved to different regions after this step. * `set_rules(self)` called to set access and item rules on locations and entrances. * `generate_basic(self)` diff --git a/inno_setup.iss b/inno_setup.iss index a2b9f8990b61..67b998eeb29a 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -224,7 +224,7 @@ Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{ Root: HKCR; Subkey: ".apworld"; ValueData: "{#MyAppName}worlddata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}worlddata"; ValueData: "Archipelago World Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}worlddata\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; +Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey; Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: ""; diff --git a/test/bases.py b/test/bases.py index ee9fbcb683b7..5c2d241cbbfe 100644 --- a/test/bases.py +++ b/test/bases.py @@ -292,12 +292,12 @@ def test_all_state_can_reach_everything(self): """Ensure all state can reach everything and complete the game with the defined options""" if not (self.run_default_tests and self.constructed): return - with self.subTest("Game", game=self.game): + with self.subTest("Game", game=self.game, seed=self.multiworld.seed): excluded = self.multiworld.worlds[self.player].options.exclude_locations.value state = self.multiworld.get_all_state(False) for location in self.multiworld.get_locations(): if location.name not in excluded: - with self.subTest("Location should be reached", location=location): + with self.subTest("Location should be reached", location=location.name): reachable = location.can_reach(state) self.assertTrue(reachable, f"{location.name} unreachable") with self.subTest("Beatable"): @@ -308,7 +308,7 @@ def test_empty_state_can_reach_something(self): """Ensure empty state can reach at least one location with the defined options""" if not (self.run_default_tests and self.constructed): return - with self.subTest("Game", game=self.game): + with self.subTest("Game", game=self.game, seed=self.multiworld.seed): state = CollectionState(self.multiworld) locations = self.multiworld.get_reachable_locations(state, self.player) self.assertGreater(len(locations), 0, @@ -329,7 +329,7 @@ def fulfills_accessibility() -> bool: for n in range(len(locations) - 1, -1, -1): if locations[n].can_reach(state): sphere.append(locations.pop(n)) - self.assertTrue(sphere or self.multiworld.accessibility[1] == "minimal", + self.assertTrue(sphere or self.multiworld.worlds[1].options.accessibility == "minimal", f"Unreachable locations: {locations}") if not sphere: break diff --git a/test/general/test_reachability.py b/test/general/test_reachability.py index e57c398b7beb..4b71762f77fe 100644 --- a/test/general/test_reachability.py +++ b/test/general/test_reachability.py @@ -41,15 +41,15 @@ def test_default_all_state_can_reach_everything(self): state = multiworld.get_all_state(False) for location in multiworld.get_locations(): if location.name not in excluded: - with self.subTest("Location should be reached", location=location): + with self.subTest("Location should be reached", location=location.name): self.assertTrue(location.can_reach(state), f"{location.name} unreachable") for region in multiworld.get_regions(): if region.name in unreachable_regions: - with self.subTest("Region should be unreachable", region=region): + with self.subTest("Region should be unreachable", region=region.name): self.assertFalse(region.can_reach(state)) else: - with self.subTest("Region should be reached", region=region): + with self.subTest("Region should be reached", region=region.name): self.assertTrue(region.can_reach(state)) with self.subTest("Completion Condition"): diff --git a/test/hosting/generate.py b/test/hosting/generate.py index 356cbcca25a0..d5d39dc95ee0 100644 --- a/test/hosting/generate.py +++ b/test/hosting/generate.py @@ -26,6 +26,7 @@ def _generate_local_inner(games: Iterable[str], with TemporaryDirectory() as players_dir: with TemporaryDirectory() as output_dir: import Generate + import Main for n, game in enumerate(games, 1): player_path = Path(players_dir) / f"{n}.yaml" @@ -42,7 +43,7 @@ def _generate_local_inner(games: Iterable[str], sys.argv = [sys.argv[0], "--seed", str(hash(tuple(games))), "--player_files_path", players_dir, "--outputpath", output_dir] - Generate.main() + Main.main(*Generate.main()) output_files = list(Path(output_dir).glob('*.zip')) assert len(output_files) == 1 final_file = dest / output_files[0].name diff --git a/test/programs/test_generate.py b/test/programs/test_generate.py index 887a417ec9f9..9281c9c753cd 100644 --- a/test/programs/test_generate.py +++ b/test/programs/test_generate.py @@ -9,6 +9,7 @@ from tempfile import TemporaryDirectory import Generate +import Main class TestGenerateMain(unittest.TestCase): @@ -58,7 +59,7 @@ def test_generate_absolute(self): '--player_files_path', str(self.abs_input_dir), '--outputpath', self.output_tempdir.name] print(f'Testing Generate.py {sys.argv} in {os.getcwd()}') - Generate.main() + Main.main(*Generate.main()) self.assertOutput(self.output_tempdir.name) @@ -67,7 +68,7 @@ def test_generate_relative(self): '--player_files_path', str(self.rel_input_dir), '--outputpath', self.output_tempdir.name] print(f'Testing Generate.py {sys.argv} in {os.getcwd()}') - Generate.main() + Main.main(*Generate.main()) self.assertOutput(self.output_tempdir.name) @@ -86,7 +87,7 @@ def test_generate_yaml(self): sys.argv = [sys.argv[0], '--seed', '0', '--outputpath', self.output_tempdir.name] print(f'Testing Generate.py {sys.argv} in {os.getcwd()}, player_files_path={self.yaml_input_dir}') - Generate.main() + Main.main(*Generate.main()) finally: user_path.cached_path = user_path_backup diff --git a/test/webhost/__init__.py b/test/webhost/__init__.py index e69de29bb2d1..2eb340722a3a 100644 --- a/test/webhost/__init__.py +++ b/test/webhost/__init__.py @@ -0,0 +1,36 @@ +import unittest +import typing +from uuid import uuid4 + +from flask import Flask +from flask.testing import FlaskClient + + +class TestBase(unittest.TestCase): + app: typing.ClassVar[Flask] + client: FlaskClient + + @classmethod + def setUpClass(cls) -> None: + from WebHostLib import app as raw_app + from WebHost import get_app + + raw_app.config["PONY"] = { + "provider": "sqlite", + "filename": ":memory:", + "create_db": True, + } + raw_app.config.update({ + "TESTING": True, + "DEBUG": True, + }) + try: + cls.app = get_app() + except AssertionError as e: + # since we only have 1 global app object, this might fail, but luckily all tests use the same config + if "register_blueprint" not in e.args[0]: + raise + cls.app = raw_app + + def setUp(self) -> None: + self.client = self.app.test_client() diff --git a/test/webhost/test_api_generate.py b/test/webhost/test_api_generate.py index bd78edd9c700..591c61d74880 100644 --- a/test/webhost/test_api_generate.py +++ b/test/webhost/test_api_generate.py @@ -1,31 +1,16 @@ import io -import unittest import json import yaml +from . import TestBase -class TestDocs(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - from WebHostLib import app as raw_app - from WebHost import get_app - raw_app.config["PONY"] = { - "provider": "sqlite", - "filename": ":memory:", - "create_db": True, - } - raw_app.config.update({ - "TESTING": True, - }) - app = get_app() - - cls.client = app.test_client() - def test_correct_error_empty_request(self): +class TestAPIGenerate(TestBase): + def test_correct_error_empty_request(self) -> None: response = self.client.post("/api/generate") self.assertIn("No options found. Expected file attachment or json weights.", response.text) - def test_generation_queued_weights(self): + def test_generation_queued_weights(self) -> None: options = { "Tester1": { @@ -43,7 +28,7 @@ def test_generation_queued_weights(self): self.assertTrue(json_data["text"].startswith("Generation of seed ")) self.assertTrue(json_data["text"].endswith(" started successfully.")) - def test_generation_queued_file(self): + def test_generation_queued_file(self) -> None: options = { "game": "Archipelago", "name": "Tester", diff --git a/test/webhost/test_host_room.py b/test/webhost/test_host_room.py new file mode 100644 index 000000000000..e9dae41dd06f --- /dev/null +++ b/test/webhost/test_host_room.py @@ -0,0 +1,192 @@ +import os +from uuid import UUID, uuid4, uuid5 + +from flask import url_for + +from . import TestBase + + +class TestHostFakeRoom(TestBase): + room_id: UUID + log_filename: str + + def setUp(self) -> None: + from pony.orm import db_session + from Utils import user_path + from WebHostLib.models import Room, Seed + + super().setUp() + + with self.client.session_transaction() as session: + session["_id"] = uuid4() + with db_session: + # create an empty seed and a room from it + seed = Seed(multidata=b"", owner=session["_id"]) + room = Room(seed=seed, owner=session["_id"], tracker=uuid4()) + self.room_id = room.id + self.log_filename = user_path("logs", f"{self.room_id}.txt") + + def tearDown(self) -> None: + from pony.orm import db_session, select + from WebHostLib.models import Command, Room + + with db_session: + for command in select(command for command in Command if command.room.id == self.room_id): # type: ignore + command.delete() + room: Room = Room.get(id=self.room_id) + room.seed.delete() + room.delete() + + try: + os.unlink(self.log_filename) + except FileNotFoundError: + pass + + def test_display_log_missing_full(self) -> None: + """ + Verify that we get a 200 response even if log is missing. + This is required to not get an error for fetch. + """ + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get(url_for("display_log", room=self.room_id)) + self.assertEqual(response.status_code, 200) + + def test_display_log_missing_range(self) -> None: + """ + Verify that we get a full response for missing log even if we asked for range. + This is required for the JS logic to differentiate between log update and log error message. + """ + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get(url_for("display_log", room=self.room_id), headers={ + "Range": "bytes=100-" + }) + self.assertEqual(response.status_code, 200) + + def test_display_log_denied(self) -> None: + """Verify that only the owner can see the log.""" + other_client = self.app.test_client() + with self.app.app_context(), self.app.test_request_context(): + response = other_client.get(url_for("display_log", room=self.room_id)) + self.assertEqual(response.status_code, 403) + + def test_display_log_missing_room(self) -> None: + """Verify log for missing room gives an error as opposed to missing log for existing room.""" + missing_room_id = uuid5(uuid4(), "") # rooms are always uuid4, so this can't exist + other_client = self.app.test_client() + with self.app.app_context(), self.app.test_request_context(): + response = other_client.get(url_for("display_log", room=missing_room_id)) + self.assertEqual(response.status_code, 404) + + def test_display_log_full(self) -> None: + """Verify full log response.""" + with open(self.log_filename, "w", encoding="utf-8") as f: + text = "x" * 200 + f.write(text) + + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get(url_for("display_log", room=self.room_id)) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.get_data(True), text) + + def test_display_log_range(self) -> None: + """Verify that Range header in request gives a range in response.""" + with open(self.log_filename, "w", encoding="utf-8") as f: + f.write(" " * 100) + text = "x" * 100 + f.write(text) + + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get(url_for("display_log", room=self.room_id), headers={ + "Range": "bytes=100-" + }) + self.assertEqual(response.status_code, 206) + self.assertEqual(response.get_data(True), text) + + def test_display_log_range_bom(self) -> None: + """Verify that a BOM in the log file is skipped for range.""" + with open(self.log_filename, "w", encoding="utf-8-sig") as f: + f.write(" " * 100) + text = "x" * 100 + f.write(text) + self.assertEqual(f.tell(), 203) # including BOM + + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get(url_for("display_log", room=self.room_id), headers={ + "Range": "bytes=100-" + }) + self.assertEqual(response.status_code, 206) + self.assertEqual(response.get_data(True), text) + + def test_host_room_missing(self) -> None: + """Verify that missing room gives a 404 response.""" + missing_room_id = uuid5(uuid4(), "") # rooms are always uuid4, so this can't exist + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get(url_for("host_room", room=missing_room_id)) + self.assertEqual(response.status_code, 404) + + def test_host_room_own(self) -> None: + """Verify that own room gives the full output.""" + with open(self.log_filename, "w", encoding="utf-8-sig") as f: + text = "* should be visible *" + f.write(text) + + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get(url_for("host_room", room=self.room_id)) + response_text = response.get_data(True) + self.assertEqual(response.status_code, 200) + self.assertIn("href=\"/seed/", response_text) + self.assertIn(text, response_text) + + def test_host_room_other(self) -> None: + """Verify that non-own room gives the reduced output.""" + from pony.orm import db_session + from WebHostLib.models import Room + + with db_session: + room: Room = Room.get(id=self.room_id) + room.last_port = 12345 + + with open(self.log_filename, "w", encoding="utf-8-sig") as f: + text = "* should not be visible *" + f.write(text) + + other_client = self.app.test_client() + with self.app.app_context(), self.app.test_request_context(): + response = other_client.get(url_for("host_room", room=self.room_id)) + response_text = response.get_data(True) + self.assertEqual(response.status_code, 200) + self.assertNotIn("href=\"/seed/", response_text) + self.assertNotIn(text, response_text) + self.assertIn("/connect ", response_text) + self.assertIn(":12345", response_text) + + def test_host_room_own_post(self) -> None: + """Verify command from owner gets queued for the server and response is redirect.""" + from pony.orm import db_session, select + from WebHostLib.models import Command + + with self.app.app_context(), self.app.test_request_context(): + response = self.client.post(url_for("host_room", room=self.room_id), data={ + "cmd": "/help" + }) + self.assertEqual(response.status_code, 302, response.text)\ + + with db_session: + commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore + self.assertIn("/help", (command.commandtext for command in commands)) + + def test_host_room_other_post(self) -> None: + """Verify command from non-owner does not get queued for the server.""" + from pony.orm import db_session, select + from WebHostLib.models import Command + + other_client = self.app.test_client() + with self.app.app_context(), self.app.test_request_context(): + response = other_client.post(url_for("host_room", room=self.room_id), data={ + "cmd": "/help" + }) + self.assertLess(response.status_code, 500) + + with db_session: + commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore + self.assertNotIn("/help", (command.commandtext for command in commands)) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index bed375cf080a..af067e5cb8a6 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -223,6 +223,21 @@ class WebWorld(metaclass=WebWorldRegister): option_groups: ClassVar[List[OptionGroup]] = [] """Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options".""" + rich_text_options_doc = False + """Whether the WebHost should render Options' docstrings as rich text. + + If this is True, Options' docstrings are interpreted as reStructuredText_, + the standard Python markup format. In the WebHost, they're rendered to HTML + so that lists, emphasis, and other rich text features are displayed + properly. + + If this is False, the docstrings are instead interpreted as plain text, and + displayed as-is on the WebHost with whitespace preserved. For backwards + compatibility, this is the default. + + .. _reStructuredText: https://docutils.sourceforge.io/rst.html + """ + location_descriptions: Dict[str, str] = {} """An optional map from location names (or location group names) to brief descriptions for users.""" @@ -265,7 +280,7 @@ class World(metaclass=AutoWorldRegister): future. Protocol level compatibility check moved to MultiServer.min_client_version. """ - required_server_version: Tuple[int, int, int] = (0, 2, 4) + required_server_version: Tuple[int, int, int] = (0, 5, 0) """update this if the resulting multidata breaks forward-compatibility of the server""" hint_blacklist: ClassVar[FrozenSet[str]] = frozenset() diff --git a/worlds/__init__.py b/worlds/__init__.py index 8d784a5ba438..bb2fe866d02d 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -128,3 +128,4 @@ def load(self) -> bool: network_data_package: DataPackage = { "games": {world_name: world.get_data_package_data() for world_name, world in AutoWorldRegister.world_types.items()}, } + diff --git a/worlds/adventure/Rules.py b/worlds/adventure/Rules.py index 930295301288..53617b039d78 100644 --- a/worlds/adventure/Rules.py +++ b/worlds/adventure/Rules.py @@ -1,7 +1,5 @@ -from worlds.adventure import location_table -from worlds.adventure.Options import BatLogic, DifficultySwitchB, DifficultySwitchA +from .Options import BatLogic, DifficultySwitchB from worlds.generic.Rules import add_rule, set_rule, forbid_item -from BaseClasses import LocationProgressType def set_rules(self) -> None: diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 0ba0f5b9a5a4..c6aeaa357799 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -292,6 +292,9 @@ # See above comment "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations", "Murder on the Owl Express"], + + # was causing test failures + "Time Rift - Balcony": ["Alpine Free Roam"], } diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 71f74b17d7ed..b0513c433289 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -863,6 +863,8 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]): if world.is_dlc1(): for entrance in regions["Time Rift - Balcony"].entrances: add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale")) + reg_act_connection(world, world.multiworld.get_entrance("The Arctic Cruise - Finale", + world.player).connected_region, entrance) for entrance in regions["Time Rift - Deep Sea"].entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) @@ -939,6 +941,7 @@ def set_default_rift_rules(world: "HatInTimeWorld"): if world.is_dlc1(): for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances: add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale")) + reg_act_connection(world, "Rock the Boat", entrance.name) for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index db7555f24615..a0b28829f4bb 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -339,7 +339,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool: def new_check(location_id): new_locations.append(location_id) ctx.locations_checked.add(location_id) - location = ctx.location_names.lookup_in_slot(location_id) + location = ctx.location_names.lookup_in_game(location_id) snes_logger.info( f'New Check: {location} ' + f'({len(ctx.checked_locations) + 1 if ctx.checked_locations else len(ctx.locations_checked)}/' + @@ -552,7 +552,7 @@ async def game_watcher(self, ctx): item = ctx.items_received[recv_index] recv_index += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), + color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) @@ -682,7 +682,7 @@ def onButtonClick(answer: str = 'no'): if 'yes' in choice: import LttPAdjuster - from worlds.alttp.Rom import get_base_rom_path + from .Rom import get_base_rom_path last_settings.rom = romfile last_settings.baserom = get_base_rom_path() last_settings.world = None diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index 87e28646a262..f759b6309a0e 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -1437,7 +1437,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): invalid_cave_connections = defaultdict(set) if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: - from worlds.alttp import OverworldGlitchRules + from . import OverworldGlitchRules for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'): invalid_connections[entrance] = set() if entrance in must_be_exits: diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 11c1a0165b53..ee3ebc587ce7 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -486,7 +486,7 @@ class LTTPBosses(PlandoBosses): @classmethod def can_place_boss(cls, boss: str, location: str) -> bool: - from worlds.alttp.Bosses import can_place_boss + from .Bosses import can_place_boss level = '' words = location.split(" ") if words[-1] in ("top", "middle", "bottom"): diff --git a/worlds/alttp/OverworldGlitchRules.py b/worlds/alttp/OverworldGlitchRules.py index 146fc2f0cac9..2da76234bd40 100644 --- a/worlds/alttp/OverworldGlitchRules.py +++ b/worlds/alttp/OverworldGlitchRules.py @@ -220,26 +220,7 @@ def get_invalid_bunny_revival_dungeons(): yield 'Sanctuary' -def no_logic_rules(world, player): - """ - Add OWG transitions to no logic player's world - """ - create_no_logic_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted')) - create_no_logic_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player)) - - # Glitched speed drops. - create_no_logic_connections(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted')) - - # Mirror clip spots. - if world.mode[player] != 'inverted': - create_no_logic_connections(player, world, get_mirror_clip_spots_dw()) - create_no_logic_connections(player, world, get_mirror_offset_spots_dw()) - else: - create_no_logic_connections(player, world, get_mirror_offset_spots_lw(player)) - - def overworld_glitch_connections(world, player): - # Boots-accessible locations. create_owg_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted')) create_owg_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player)) diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py index 4c2e7d509e9a..f3dbbdc059f1 100644 --- a/worlds/alttp/Regions.py +++ b/worlds/alttp/Regions.py @@ -406,7 +406,7 @@ def create_dungeon_region(world: MultiWorld, player: int, name: str, hint: str, def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None, exits=None): - from worlds.alttp.SubClasses import ALttPLocation + from .SubClasses import ALttPLocation ret = LTTPRegion(name, type, hint, player, world) if exits: for exit in exits: @@ -760,7 +760,7 @@ def mark_light_world_regions(world, player: int): 'Turtle Rock - Prize': ( [0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')} -from worlds.alttp.Shops import shop_table_by_location_id, shop_table_by_location +from .Shops import shop_table_by_location_id, shop_table_by_location lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int} lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}} lookup_id_to_name.update(shop_table_by_location_id) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index eac810610b48..67684a6f3ced 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -10,7 +10,7 @@ from .Bosses import GanonDefeatRule from .Items import item_factory, item_name_groups, item_table, progression_items from .Options import small_key_shuffle -from .OverworldGlitchRules import no_logic_rules, overworld_glitches_rules +from .OverworldGlitchRules import overworld_glitches_rules from .Regions import LTTPRegionType, location_table from .StateHelpers import (can_extend_magic, can_kill_most_things, can_lift_heavy_rocks, can_lift_rocks, @@ -33,7 +33,6 @@ def set_rules(world): 'WARNING! Seeds generated under this logic often require major glitches and may be impossible!') if world.players == 1: - no_logic_rules(world, player) for exit in world.get_region('Menu', player).exits: exit.hide_path = True return @@ -406,16 +405,14 @@ def global_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: can_use_bombs(state, player)) set_rule(multiworld.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player)) - if multiworld.worlds[player].dungeons["Thieves Town"].boss.enemizer_name == "Blind": set_rule(multiworld.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3) and can_use_bombs(state, player)) - set_rule(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player)) - + set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player), + lambda state: state._lttp_has_key('Small Key (Thieves Town)', player)) if multiworld.accessibility[player] != 'locations' and not multiworld.key_drop_shuffle[player]: set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player) - set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3)) set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player)) @@ -491,7 +488,7 @@ def global_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_location('Turtle Rock - Roller Room - Right', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player)) set_rule(multiworld.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player))) set_rule(multiworld.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player)) - set_rule(multiworld.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10)) + set_rule(multiworld.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10) and can_bomb_or_bonk(state, player)) set_rule(multiworld.get_location('Turtle Rock - Chain Chomps', player), lambda state: can_use_bombs(state, player) or can_shoot_arrows(state, player) or has_beam_sword(state, player) or state.has_any(["Blue Boomerang", "Red Boomerang", "Hookshot", "Cane of Somaria", "Fire Rod", "Ice Rod"], player)) set_rule(multiworld.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player)) diff --git a/worlds/alttp/test/dungeons/TestThievesTown.py b/worlds/alttp/test/dungeons/TestThievesTown.py index 342823c91007..1cd3f5ca8f39 100644 --- a/worlds/alttp/test/dungeons/TestThievesTown.py +++ b/worlds/alttp/test/dungeons/TestThievesTown.py @@ -37,7 +37,8 @@ def testThievesTown(self): ["Thieves' Town - Blind's Cell", False, []], ["Thieves' Town - Blind's Cell", False, [], ['Big Key (Thieves Town)']], - ["Thieves' Town - Blind's Cell", True, ['Big Key (Thieves Town)']], + ["Thieves' Town - Blind's Cell", False, [], ['Small Key (Thieves Town)']], + ["Thieves' Town - Blind's Cell", True, ['Big Key (Thieves Town)', 'Small Key (Thieves Town)']], ["Thieves' Town - Boss", False, []], ["Thieves' Town - Boss", False, [], ['Big Key (Thieves Town)']], diff --git a/worlds/aquaria/Locations.py b/worlds/aquaria/Locations.py index 33d165db411a..2eb9d1e9a29d 100644 --- a/worlds/aquaria/Locations.py +++ b/worlds/aquaria/Locations.py @@ -30,7 +30,7 @@ class AquariaLocations: locations_verse_cave_r = { "Verse Cave, bulb in the skeleton room": 698107, - "Verse Cave, bulb in the path left of the skeleton room": 698108, + "Verse Cave, bulb in the path right of the skeleton room": 698108, "Verse Cave right area, Big Seed": 698175, } @@ -122,6 +122,7 @@ class AquariaLocations: "Open Water top right area, second urn in the Mithalas exit": 698149, "Open Water top right area, third urn in the Mithalas exit": 698150, } + locations_openwater_tr_turtle = { "Open Water top right area, bulb in the turtle room": 698009, "Open Water top right area, Transturtle": 698211, @@ -195,7 +196,7 @@ class AquariaLocations: locations_cathedral_l = { "Mithalas City Castle, bulb in the flesh hole": 698042, - "Mithalas City Castle, Blue banner": 698165, + "Mithalas City Castle, Blue Banner": 698165, "Mithalas City Castle, urn in the bedroom": 698130, "Mithalas City Castle, first urn of the single lamp path": 698131, "Mithalas City Castle, second urn of the single lamp path": 698132, @@ -226,7 +227,7 @@ class AquariaLocations: "Mithalas Cathedral, third urn in the path behind the flesh vein": 698146, "Mithalas Cathedral, fourth urn in the top right room": 698147, "Mithalas Cathedral, Mithalan Dress": 698189, - "Mithalas Cathedral right area, urn below the left entrance": 698198, + "Mithalas Cathedral, urn below the left entrance": 698198, } locations_cathedral_underground = { @@ -239,7 +240,7 @@ class AquariaLocations: } locations_cathedral_boss = { - "Cathedral boss area, beating Mithalan God": 698202, + "Mithalas boss area, beating Mithalan God": 698202, } locations_forest_tl = { @@ -269,7 +270,7 @@ class AquariaLocations: locations_forest_bl = { "Kelp Forest bottom left area, bulb close to the spirit crystals": 698054, - "Kelp Forest bottom left area, Walker baby": 698186, + "Kelp Forest bottom left area, Walker Baby": 698186, "Kelp Forest bottom left area, Transturtle": 698212, } @@ -451,7 +452,7 @@ class AquariaLocations: locations_body_c = { "The Body center area, breaking Li's cage": 698201, - "The Body main area, bulb on the main path blocking tube": 698097, + "The Body center area, bulb on the main path blocking tube": 698097, } locations_body_l = { diff --git a/worlds/aquaria/Options.py b/worlds/aquaria/Options.py index 4c795d350898..8c0142debff0 100644 --- a/worlds/aquaria/Options.py +++ b/worlds/aquaria/Options.py @@ -5,7 +5,7 @@ """ from dataclasses import dataclass -from Options import Toggle, Choice, Range, DeathLink, PerGameCommonOptions, DefaultOnToggle, StartInventoryPool +from Options import Toggle, Choice, Range, PerGameCommonOptions, DefaultOnToggle, StartInventoryPool class IngredientRandomizer(Choice): @@ -111,6 +111,14 @@ class BindSongNeededToGetUnderRockBulb(Toggle): display_name = "Bind song needed to get sing bulbs under rocks" +class BlindGoal(Toggle): + """ + Hide the goal's requirements from the help page so that you have to go to the last boss door to know + what is needed to access the boss. + """ + display_name = "Hide the goal's requirements" + + class UnconfineHomeWater(Choice): """ Open the way out of the Home Water area so that Naija can go to open water and beyond without the bind song. @@ -142,4 +150,4 @@ class AquariaOptions(PerGameCommonOptions): dish_randomizer: DishRandomizer aquarian_translation: AquarianTranslation skip_first_vision: SkipFirstVision - death_link: DeathLink + blind_goal: BlindGoal diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py index 28120259254c..93c02d4e6766 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -300,7 +300,7 @@ def __create_mithalas(self) -> None: AquariaLocations.locations_cathedral_l_sc) self.cathedral_r = self.__add_region("Mithalas Cathedral", AquariaLocations.locations_cathedral_r) - self.cathedral_underground = self.__add_region("Mithalas Cathedral Underground area", + self.cathedral_underground = self.__add_region("Mithalas Cathedral underground", AquariaLocations.locations_cathedral_underground) self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, Mithalan God room", AquariaLocations.locations_cathedral_boss) @@ -597,22 +597,22 @@ def __connect_mithalas_regions(self) -> None: lambda state: _has_beast_form(state, self.player) and _has_energy_form(state, self.player) and _has_bind_song(state, self.player)) - self.__connect_regions("Mithalas castle", "Cathedral underground", + self.__connect_regions("Mithalas castle", "Mithalas Cathedral underground", self.cathedral_l, self.cathedral_underground, lambda state: _has_beast_form(state, self.player) and _has_bind_song(state, self.player)) - self.__connect_regions("Mithalas castle", "Cathedral right area", + self.__connect_regions("Mithalas castle", "Mithalas Cathedral", self.cathedral_l, self.cathedral_r, lambda state: _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_regions("Cathedral right area", "Cathedral underground", + self.__connect_regions("Mithalas Cathedral", "Mithalas Cathedral underground", self.cathedral_r, self.cathedral_underground, lambda state: _has_energy_form(state, self.player)) - self.__connect_one_way_regions("Cathedral underground", "Cathedral boss left area", + self.__connect_one_way_regions("Mithalas Cathedral underground", "Cathedral boss left area", self.cathedral_underground, self.cathedral_boss_r, lambda state: _has_energy_form(state, self.player) and _has_bind_song(state, self.player)) - self.__connect_one_way_regions("Cathedral boss left area", "Cathedral underground", + self.__connect_one_way_regions("Cathedral boss left area", "Mithalas Cathedral underground", self.cathedral_boss_r, self.cathedral_underground, lambda state: _has_beast_form(state, self.player)) self.__connect_regions("Cathedral boss right area", "Cathedral boss left area", @@ -1099,7 +1099,7 @@ def __adjusting_manual_rules(self) -> None: lambda state: _has_beast_form(state, self.player)) add_rule(self.multiworld.get_location("Open Water bottom left area, bulb inside the lowest fish pass", self.player), lambda state: _has_fish_form(state, self.player)) - add_rule(self.multiworld.get_location("Kelp Forest bottom left area, Walker baby", self.player), + add_rule(self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", self.player), lambda state: _has_spirit_form(state, self.player)) add_rule(self.multiworld.get_location("The Veil top left area, bulb hidden behind the blocking rock", self.player), lambda state: _has_bind_song(state, self.player)) @@ -1134,7 +1134,7 @@ def __no_progression_hard_or_hidden_location(self) -> None: self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Cathedral boss area, beating Mithalan God", + self.multiworld.get_location("Mithalas boss area, beating Mithalan God", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Kelp Forest boss area, beating Drunian God", @@ -1191,7 +1191,7 @@ def __no_progression_hard_or_hidden_location(self) -> None: self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Kelp Forest bottom left area, Walker baby", + self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sun Temple, Sun Key", diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py index ce46aeea75aa..1fb04036d81b 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -204,7 +204,8 @@ def generate_basic(self) -> None: def fill_slot_data(self) -> Dict[str, Any]: return {"ingredientReplacement": self.ingredients_substitution, - "aquarianTranslate": bool(self.options.aquarian_translation.value), + "aquarian_translate": bool(self.options.aquarian_translation.value), + "blind_goal": bool(self.options.blind_goal.value), "secret_needed": self.options.objective.value > 0, "minibosses_to_kill": self.options.mini_bosses_to_beat.value, "bigbosses_to_kill": self.options.big_bosses_to_beat.value, diff --git a/worlds/aquaria/test/__init__.py b/worlds/aquaria/test/__init__.py index 5c63c9bb2968..029db691b66b 100644 --- a/worlds/aquaria/test/__init__.py +++ b/worlds/aquaria/test/__init__.py @@ -60,7 +60,7 @@ "Mithalas City, Doll", "Mithalas City, urn inside a home fish pass", "Mithalas City Castle, bulb in the flesh hole", - "Mithalas City Castle, Blue banner", + "Mithalas City Castle, Blue Banner", "Mithalas City Castle, urn in the bedroom", "Mithalas City Castle, first urn of the single lamp path", "Mithalas City Castle, second urn of the single lamp path", @@ -82,14 +82,14 @@ "Mithalas Cathedral, third urn in the path behind the flesh vein", "Mithalas Cathedral, fourth urn in the top right room", "Mithalas Cathedral, Mithalan Dress", - "Mithalas Cathedral right area, urn below the left entrance", + "Mithalas Cathedral, urn below the left entrance", "Cathedral Underground, bulb in the center part", "Cathedral Underground, first bulb in the top left part", "Cathedral Underground, second bulb in the top left part", "Cathedral Underground, third bulb in the top left part", "Cathedral Underground, bulb close to the save crystal", "Cathedral Underground, bulb in the bottom right path", - "Cathedral boss area, beating Mithalan God", + "Mithalas boss area, beating Mithalan God", "Kelp Forest top left area, bulb in the bottom left clearing", "Kelp Forest top left area, bulb in the path down from the top left clearing", "Kelp Forest top left area, bulb in the top left clearing", @@ -104,7 +104,7 @@ "Kelp Forest top right area, Black Pearl", "Kelp Forest top right area, bulb in the top fish pass", "Kelp Forest bottom left area, bulb close to the spirit crystals", - "Kelp Forest bottom left area, Walker baby", + "Kelp Forest bottom left area, Walker Baby", "Kelp Forest bottom left area, Transturtle", "Kelp Forest bottom right area, Odd Container", "Kelp Forest boss area, beating Drunian God", @@ -175,7 +175,7 @@ "Sunken City left area, Girl Costume", "Sunken City, bulb on top of the boss area", "The Body center area, breaking Li's cage", - "The Body main area, bulb on the main path blocking tube", + "The Body center area, bulb on the main path blocking tube", "The Body left area, first bulb in the top face room", "The Body left area, second bulb in the top face room", "The Body left area, bulb below the water stream", diff --git a/worlds/aquaria/test/test_beast_form_access.py b/worlds/aquaria/test/test_beast_form_access.py index 4bb4d5656c01..0efc3e7388fe 100644 --- a/worlds/aquaria/test/test_beast_form_access.py +++ b/worlds/aquaria/test/test_beast_form_access.py @@ -4,7 +4,7 @@ Description: Unit test used to test accessibility of locations with and without the beast form """ -from worlds.aquaria.test import AquariaTestBase +from . import AquariaTestBase class BeastFormAccessTest(AquariaTestBase): diff --git a/worlds/aquaria/test/test_bind_song_access.py b/worlds/aquaria/test/test_bind_song_access.py index ca663369cc63..05f96edb9192 100644 --- a/worlds/aquaria/test/test_bind_song_access.py +++ b/worlds/aquaria/test/test_bind_song_access.py @@ -5,7 +5,7 @@ under rock needing bind song option) """ -from worlds.aquaria.test import AquariaTestBase, after_home_water_locations +from . import AquariaTestBase, after_home_water_locations class BindSongAccessTest(AquariaTestBase): diff --git a/worlds/aquaria/test/test_bind_song_option_access.py b/worlds/aquaria/test/test_bind_song_option_access.py index a75ef60cdf05..e391eef101bf 100644 --- a/worlds/aquaria/test/test_bind_song_option_access.py +++ b/worlds/aquaria/test/test_bind_song_option_access.py @@ -5,8 +5,8 @@ under rock needing bind song option) """ -from worlds.aquaria.test import AquariaTestBase -from worlds.aquaria.test.test_bind_song_access import after_home_water_locations +from . import AquariaTestBase +from .test_bind_song_access import after_home_water_locations class BindSongOptionAccessTest(AquariaTestBase): diff --git a/worlds/aquaria/test/test_confined_home_water.py b/worlds/aquaria/test/test_confined_home_water.py index 72fddfb4048a..89c51ac5c775 100644 --- a/worlds/aquaria/test/test_confined_home_water.py +++ b/worlds/aquaria/test/test_confined_home_water.py @@ -4,7 +4,7 @@ Description: Unit test used to test accessibility of region with the home water confine via option """ -from worlds.aquaria.test import AquariaTestBase +from . import AquariaTestBase class ConfinedHomeWaterAccessTest(AquariaTestBase): diff --git a/worlds/aquaria/test/test_dual_song_access.py b/worlds/aquaria/test/test_dual_song_access.py index 8266ffb181d9..bb9b2e739604 100644 --- a/worlds/aquaria/test/test_dual_song_access.py +++ b/worlds/aquaria/test/test_dual_song_access.py @@ -4,7 +4,7 @@ Description: Unit test used to test accessibility of locations with and without the dual song """ -from worlds.aquaria.test import AquariaTestBase +from . import AquariaTestBase class LiAccessTest(AquariaTestBase): diff --git a/worlds/aquaria/test/test_energy_form_access.py b/worlds/aquaria/test/test_energy_form_access.py index ce4ed4099416..82d8e89a0066 100644 --- a/worlds/aquaria/test/test_energy_form_access.py +++ b/worlds/aquaria/test/test_energy_form_access.py @@ -5,7 +5,7 @@ energy form option) """ -from worlds.aquaria.test import AquariaTestBase +from . import AquariaTestBase class EnergyFormAccessTest(AquariaTestBase): @@ -39,8 +39,8 @@ def test_energy_form_location(self) -> None: "Mithalas Cathedral, third urn in the path behind the flesh vein", "Mithalas Cathedral, fourth urn in the top right room", "Mithalas Cathedral, Mithalan Dress", - "Mithalas Cathedral right area, urn below the left entrance", - "Cathedral boss area, beating Mithalan God", + "Mithalas Cathedral, urn below the left entrance", + "Mithalas boss area, beating Mithalan God", "Kelp Forest top left area, bulb close to the Verse Egg", "Kelp Forest top left area, Verse Egg", "Kelp Forest boss area, beating Drunian God", diff --git a/worlds/aquaria/test/test_fish_form_access.py b/worlds/aquaria/test/test_fish_form_access.py index d252bb1f1862..c98a53e92438 100644 --- a/worlds/aquaria/test/test_fish_form_access.py +++ b/worlds/aquaria/test/test_fish_form_access.py @@ -4,7 +4,7 @@ Description: Unit test used to test accessibility of locations with and without the fish form """ -from worlds.aquaria.test import AquariaTestBase +from . import AquariaTestBase class FishFormAccessTest(AquariaTestBase): diff --git a/worlds/aquaria/test/test_li_song_access.py b/worlds/aquaria/test/test_li_song_access.py index 42adc90e5aa1..f615fb10c640 100644 --- a/worlds/aquaria/test/test_li_song_access.py +++ b/worlds/aquaria/test/test_li_song_access.py @@ -4,7 +4,7 @@ Description: Unit test used to test accessibility of locations with and without Li """ -from worlds.aquaria.test import AquariaTestBase +from . import AquariaTestBase class LiAccessTest(AquariaTestBase): @@ -24,7 +24,7 @@ def test_li_song_location(self) -> None: "Sunken City left area, Girl Costume", "Sunken City, bulb on top of the boss area", "The Body center area, breaking Li's cage", - "The Body main area, bulb on the main path blocking tube", + "The Body center area, bulb on the main path blocking tube", "The Body left area, first bulb in the top face room", "The Body left area, second bulb in the top face room", "The Body left area, bulb below the water stream", diff --git a/worlds/aquaria/test/test_light_access.py b/worlds/aquaria/test/test_light_access.py index 41e65cb30d9b..b5d7cf99fea2 100644 --- a/worlds/aquaria/test/test_light_access.py +++ b/worlds/aquaria/test/test_light_access.py @@ -4,7 +4,7 @@ Description: Unit test used to test accessibility of locations with and without a light (Dumbo pet or sun form) """ -from worlds.aquaria.test import AquariaTestBase +from . import AquariaTestBase class LightAccessTest(AquariaTestBase): diff --git a/worlds/aquaria/test/test_nature_form_access.py b/worlds/aquaria/test/test_nature_form_access.py index b380e5048fc9..1d3b8f4150eb 100644 --- a/worlds/aquaria/test/test_nature_form_access.py +++ b/worlds/aquaria/test/test_nature_form_access.py @@ -4,7 +4,7 @@ Description: Unit test used to test accessibility of locations with and without the nature form """ -from worlds.aquaria.test import AquariaTestBase +from . import AquariaTestBase class NatureFormAccessTest(AquariaTestBase): @@ -38,7 +38,7 @@ def test_nature_form_location(self) -> None: "Beating the Golem", "Sunken City cleared", "The Body center area, breaking Li's cage", - "The Body main area, bulb on the main path blocking tube", + "The Body center area, bulb on the main path blocking tube", "The Body left area, first bulb in the top face room", "The Body left area, second bulb in the top face room", "The Body left area, bulb below the water stream", diff --git a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py index b0d2b0d880fa..f015b26de10b 100644 --- a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py @@ -4,7 +4,7 @@ Description: Unit test used to test that no progression items can be put in hard or hidden locations when option enabled """ -from worlds.aquaria.test import AquariaTestBase +from . import AquariaTestBase from BaseClasses import ItemClassification @@ -16,7 +16,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): unfillable_locations = [ "Energy Temple boss area, Fallen God Tooth", - "Cathedral boss area, beating Mithalan God", + "Mithalas boss area, beating Mithalan God", "Kelp Forest boss area, beating Drunian God", "Sun Temple boss area, beating Sun God", "Sunken City, bulb on top of the boss area", @@ -35,7 +35,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", "Bubble Cave, Verse Egg", "Kelp Forest bottom left area, bulb close to the spirit crystals", - "Kelp Forest bottom left area, Walker baby", + "Kelp Forest bottom left area, Walker Baby", "Sun Temple, Sun Key", "The Body bottom area, Mutant Costume", "Sun Temple, bulb in the hidden room of the right part", diff --git a/worlds/aquaria/test/test_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_progression_hard_hidden_locations.py index 390fc40b295d..a1493c5d0f39 100644 --- a/worlds/aquaria/test/test_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_progression_hard_hidden_locations.py @@ -4,8 +4,7 @@ Description: Unit test used to test that progression items can be put in hard or hidden locations when option disabled """ -from worlds.aquaria.test import AquariaTestBase -from BaseClasses import ItemClassification +from . import AquariaTestBase class UNoProgressionHardHiddenTest(AquariaTestBase): @@ -16,7 +15,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): unfillable_locations = [ "Energy Temple boss area, Fallen God Tooth", - "Cathedral boss area, beating Mithalan God", + "Mithalas boss area, beating Mithalan God", "Kelp Forest boss area, beating Drunian God", "Sun Temple boss area, beating Sun God", "Sunken City, bulb on top of the boss area", @@ -35,7 +34,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", "Bubble Cave, Verse Egg", "Kelp Forest bottom left area, bulb close to the spirit crystals", - "Kelp Forest bottom left area, Walker baby", + "Kelp Forest bottom left area, Walker Baby", "Sun Temple, Sun Key", "The Body bottom area, Mutant Costume", "Sun Temple, bulb in the hidden room of the right part", diff --git a/worlds/aquaria/test/test_spirit_form_access.py b/worlds/aquaria/test/test_spirit_form_access.py index a6eec0da5dd3..3bcbd7d72e02 100644 --- a/worlds/aquaria/test/test_spirit_form_access.py +++ b/worlds/aquaria/test/test_spirit_form_access.py @@ -4,7 +4,7 @@ Description: Unit test used to test accessibility of locations with and without the spirit form """ -from worlds.aquaria.test import AquariaTestBase +from . import AquariaTestBase class SpiritFormAccessTest(AquariaTestBase): @@ -16,7 +16,7 @@ def test_spirit_form_location(self) -> None: "The Veil bottom area, bulb in the spirit path", "Mithalas City Castle, Trident Head", "Open Water skeleton path, King Skull", - "Kelp Forest bottom left area, Walker baby", + "Kelp Forest bottom left area, Walker Baby", "Abyss right area, bulb behind the rock in the whale room", "The Whale, Verse Egg", "Ice Cave, bulb in the room to the right", diff --git a/worlds/aquaria/test/test_sun_form_access.py b/worlds/aquaria/test/test_sun_form_access.py index cbe8c08a52a7..394d5e4b27ae 100644 --- a/worlds/aquaria/test/test_sun_form_access.py +++ b/worlds/aquaria/test/test_sun_form_access.py @@ -4,7 +4,7 @@ Description: Unit test used to test accessibility of locations with and without the sun form """ -from worlds.aquaria.test import AquariaTestBase +from . import AquariaTestBase class SunFormAccessTest(AquariaTestBase): diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_both.py b/worlds/aquaria/test/test_unconfine_home_water_via_both.py index 24d3adad9745..5b8689bc53a2 100644 --- a/worlds/aquaria/test/test_unconfine_home_water_via_both.py +++ b/worlds/aquaria/test/test_unconfine_home_water_via_both.py @@ -5,7 +5,7 @@ turtle and energy door """ -from worlds.aquaria.test import AquariaTestBase +from . import AquariaTestBase class UnconfineHomeWaterBothAccessTest(AquariaTestBase): diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py b/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py index 92eb8d029135..37a5c98610b5 100644 --- a/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py +++ b/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py @@ -4,7 +4,7 @@ Description: Unit test used to test accessibility of region with the unconfined home water option via the energy door """ -from worlds.aquaria.test import AquariaTestBase +from . import AquariaTestBase class UnconfineHomeWaterEnergyDoorAccessTest(AquariaTestBase): diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py b/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py index 66c40d23f1d8..da4c83c2bc7f 100644 --- a/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py +++ b/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py @@ -4,7 +4,7 @@ Description: Unit test used to test accessibility of region with the unconfined home water option via transturtle """ -from worlds.aquaria.test import AquariaTestBase +from . import AquariaTestBase class UnconfineHomeWaterTransturtleAccessTest(AquariaTestBase): diff --git a/worlds/cv64/client.py b/worlds/cv64/client.py index bea8ce38825d..2430cc5ffc67 100644 --- a/worlds/cv64/client.py +++ b/worlds/cv64/client.py @@ -146,7 +146,7 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: text_color = bytearray([0xA2, 0x0B]) else: text_color = bytearray([0xA2, 0x02]) - received_text, num_lines = cv64_text_wrap(f"{ctx.item_names.lookup_in_slot(next_item.item)}\n" + received_text, num_lines = cv64_text_wrap(f"{ctx.item_names.lookup_in_game(next_item.item)}\n" f"from {ctx.player_names[next_item.player]}", 96) await bizhawk.guarded_write(ctx.bizhawk_ctx, [(0x389BE1, [next_item.item & 0xFF], "RDRAM"), diff --git a/worlds/dkc3/Client.py b/worlds/dkc3/Client.py index 8e4a1bf2a423..ee2bd1dbdfb8 100644 --- a/worlds/dkc3/Client.py +++ b/worlds/dkc3/Client.py @@ -60,7 +60,7 @@ async def game_watcher(self, ctx): return new_checks = [] - from worlds.dkc3.Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map + from .Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map location_ram_data = await snes_read(ctx, WRAM_START + 0x5FE, 0x81) for loc_id, loc_data in location_rom_data.items(): if loc_id not in ctx.locations_checked: @@ -86,7 +86,7 @@ async def game_watcher(self, ctx): for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names.lookup_in_slot(new_check_id) + location = ctx.location_names.lookup_in_game(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) @@ -99,7 +99,7 @@ async def game_watcher(self, ctx): item = ctx.items_received[recv_index] recv_index += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), + color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) diff --git a/worlds/doom_ii/__init__.py b/worlds/doom_ii/__init__.py index 38840f552a13..32c3cbd5a2c1 100644 --- a/worlds/doom_ii/__init__.py +++ b/worlds/doom_ii/__init__.py @@ -60,17 +60,18 @@ class DOOM2World(World): # Item ratio that scales depending on episode count. These are the ratio for 3 episode. In DOOM1. # The ratio have been tweaked seem, and feel good. items_ratio: Dict[str, float] = { - "Armor": 41, - "Mega Armor": 25, - "Berserk": 12, + "Armor": 39, + "Mega Armor": 23, + "Berserk": 11, "Invulnerability": 10, "Partial invisibility": 18, - "Supercharge": 28, + "Supercharge": 26, "Medikit": 15, "Box of bullets": 13, "Box of rockets": 13, "Box of shotgun shells": 13, - "Energy cell pack": 10 + "Energy cell pack": 10, + "Megasphere": 7 } def __init__(self, multiworld: MultiWorld, player: int): @@ -233,6 +234,7 @@ def create_items(self): self.create_ratioed_items("Invulnerability", itempool) self.create_ratioed_items("Partial invisibility", itempool) self.create_ratioed_items("Supercharge", itempool) + self.create_ratioed_items("Megasphere", itempool) while len(itempool) < self.location_count: itempool.append(self.create_item(self.get_filler_item_name())) diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py index 258a5445328c..23dfa0633eb4 100644 --- a/worlds/factorio/Client.py +++ b/worlds/factorio/Client.py @@ -247,7 +247,7 @@ async def game_watcher(ctx: FactorioContext): if ctx.locations_checked != research_data: bridge_logger.debug( f"New researches done: " - f"{[ctx.location_names.lookup_in_slot(rid) for rid in research_data - ctx.locations_checked]}") + f"{[ctx.location_names.lookup_in_game(rid) for rid in research_data - ctx.locations_checked]}") ctx.locations_checked = research_data await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) death_link_tick = data.get("death_link_tick", 0) @@ -360,7 +360,7 @@ async def factorio_server_watcher(ctx: FactorioContext): transfer_item: NetworkItem = ctx.items_received[ctx.send_index] item_id = transfer_item.item player_name = ctx.player_names[transfer_item.player] - item_name = ctx.item_names.lookup_in_slot(item_id) + item_name = ctx.item_names.lookup_in_game(item_id) factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.") commands[ctx.send_index] = f"/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}" ctx.send_index += 1 diff --git a/worlds/generic/docs/setup_en.md b/worlds/generic/docs/setup_en.md index ef2413378960..22622cd0e94d 100644 --- a/worlds/generic/docs/setup_en.md +++ b/worlds/generic/docs/setup_en.md @@ -11,8 +11,8 @@ Some steps also assume use of Windows, so may vary with your OS. ## Installing the Archipelago software -The most recent public release of Archipelago can be found on the GitHub Releases page: -[Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases). +The most recent public release of Archipelago can be found on GitHub: +[Archipelago Latest Release](https://github.com/ArchipelagoMW/Archipelago/releases/latest). Run the exe file, and after accepting the license agreement you will be asked which components you would like to install. diff --git a/worlds/heretic/__init__.py b/worlds/heretic/__init__.py index fc5ffdd2de2b..bc0a54698a59 100644 --- a/worlds/heretic/__init__.py +++ b/worlds/heretic/__init__.py @@ -71,6 +71,7 @@ class HereticWorld(World): "Tome of Power": 16, "Silver Shield": 10, "Enchanted Shield": 5, + "Torch": 5, "Morph Ovum": 3, "Mystic Urn": 2, "Chaos Device": 1, @@ -242,6 +243,7 @@ def create_items(self): self.create_ratioed_items("Mystic Urn", itempool) self.create_ratioed_items("Ring of Invincibility", itempool) self.create_ratioed_items("Shadowsphere", itempool) + self.create_ratioed_items("Torch", itempool) self.create_ratioed_items("Timebomb of the Ancients", itempool) self.create_ratioed_items("Tome of Power", itempool) self.create_ratioed_items("Silver Shield", itempool) diff --git a/worlds/kdl3/Client.py b/worlds/kdl3/Client.py index 6faa8206c2df..1ca21d550e67 100644 --- a/worlds/kdl3/Client.py +++ b/worlds/kdl3/Client.py @@ -330,7 +330,7 @@ async def game_watcher(self, ctx) -> None: item = ctx.items_received[recv_amount] recv_amount += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), + color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received))) @@ -415,7 +415,7 @@ async def game_watcher(self, ctx) -> None: for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names.lookup_in_slot(new_check_id) + location = ctx.location_names.lookup_in_game(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/' f'{len(ctx.missing_locations) + len(ctx.checked_locations)})') diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index e87459fb1115..e6f608a92180 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -4,6 +4,7 @@ import os import pkgutil from collections import defaultdict +from typing import TYPE_CHECKING from .romTables import ROMWithTables from . import assembler @@ -67,10 +68,14 @@ from ..Locations import LinksAwakeningLocation from ..Options import TrendyGame, Palette, MusicChangeCondition, BootsControls +if TYPE_CHECKING: + from .. import LinksAwakeningWorld + # Function to generate a final rom, this patches the rom with all required patches -def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, multiworld=None, player_name=None, player_names=[], player_id = 0): +def generateRom(args, world: "LinksAwakeningWorld"): rom_patches = [] + player_names = list(world.multiworld.player_name.values()) rom = ROMWithTables(args.input_filename, rom_patches) rom.player_names = player_names @@ -84,10 +89,10 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m for pymod in pymods: pymod.prePatch(rom) - if settings.gfxmod: - patches.aesthetics.gfxMod(rom, os.path.join("data", "sprites", "ladx", settings.gfxmod)) + if world.ladxr_settings.gfxmod: + patches.aesthetics.gfxMod(rom, os.path.join("data", "sprites", "ladx", world.ladxr_settings.gfxmod)) - item_list = [item for item in logic.iteminfo_list if not isinstance(item, KeyLocation)] + item_list = [item for item in world.ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)] assembler.resetConsts() assembler.const("INV_SIZE", 16) @@ -116,7 +121,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m assembler.const("wLinkSpawnDelay", 0xDE13) #assembler.const("HARDWARE_LINK", 1) - assembler.const("HARD_MODE", 1 if settings.hardmode != "none" else 0) + assembler.const("HARD_MODE", 1 if world.ladxr_settings.hardmode != "none" else 0) patches.core.cleanup(rom) patches.save.singleSaveSlot(rom) @@ -130,7 +135,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m patches.core.easyColorDungeonAccess(rom) patches.owl.removeOwlEvents(rom) patches.enemies.fixArmosKnightAsMiniboss(rom) - patches.bank3e.addBank3E(rom, auth, player_id, player_names) + patches.bank3e.addBank3E(rom, world.multi_key, world.player, player_names) patches.bank3f.addBank3F(rom) patches.bank34.addBank34(rom, item_list) patches.core.removeGhost(rom) @@ -141,10 +146,11 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m from ..Options import ShuffleSmallKeys, ShuffleNightmareKeys - if ap_settings["shuffle_small_keys"] != ShuffleSmallKeys.option_original_dungeon or ap_settings["shuffle_nightmare_keys"] != ShuffleNightmareKeys.option_original_dungeon: + if world.options.shuffle_small_keys != ShuffleSmallKeys.option_original_dungeon or\ + world.options.shuffle_nightmare_keys != ShuffleNightmareKeys.option_original_dungeon: patches.inventory.advancedInventorySubscreen(rom) patches.inventory.moreSlots(rom) - if settings.witch: + if world.ladxr_settings.witch: patches.witch.updateWitch(rom) patches.softlock.fixAll(rom) patches.maptweaks.tweakMap(rom) @@ -158,9 +164,9 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m patches.tarin.updateTarin(rom) patches.fishingMinigame.updateFinishingMinigame(rom) patches.health.upgradeHealthContainers(rom) - if settings.owlstatues in ("dungeon", "both"): + if world.ladxr_settings.owlstatues in ("dungeon", "both"): patches.owl.upgradeDungeonOwlStatues(rom) - if settings.owlstatues in ("overworld", "both"): + if world.ladxr_settings.owlstatues in ("overworld", "both"): patches.owl.upgradeOverworldOwlStatues(rom) patches.goldenLeaf.fixGoldenLeaf(rom) patches.heartPiece.fixHeartPiece(rom) @@ -170,106 +176,110 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m patches.songs.upgradeMarin(rom) patches.songs.upgradeManbo(rom) patches.songs.upgradeMamu(rom) - if settings.tradequest: - patches.tradeSequence.patchTradeSequence(rom, settings.boomerang) + if world.ladxr_settings.tradequest: + patches.tradeSequence.patchTradeSequence(rom, world.ladxr_settings.boomerang) else: # Monkey bridge patch, always have the bridge there. rom.patch(0x00, 0x333D, assembler.ASM("bit 4, e\njr Z, $05"), b"", fill_nop=True) - patches.bowwow.fixBowwow(rom, everywhere=settings.bowwow != 'normal') - if settings.bowwow != 'normal': + patches.bowwow.fixBowwow(rom, everywhere=world.ladxr_settings.bowwow != 'normal') + if world.ladxr_settings.bowwow != 'normal': patches.bowwow.bowwowMapPatches(rom) patches.desert.desertAccess(rom) - if settings.overworld == 'dungeondive': + if world.ladxr_settings.overworld == 'dungeondive': patches.overworld.patchOverworldTilesets(rom) patches.overworld.createDungeonOnlyOverworld(rom) - elif settings.overworld == 'nodungeons': + elif world.ladxr_settings.overworld == 'nodungeons': patches.dungeon.patchNoDungeons(rom) - elif settings.overworld == 'random': + elif world.ladxr_settings.overworld == 'random': patches.overworld.patchOverworldTilesets(rom) - mapgen.store_map(rom, logic.world.map) + mapgen.store_map(rom, world.ladxr_logic.world.map) #if settings.dungeon_items == 'keysy': # patches.dungeon.removeKeyDoors(rom) # patches.reduceRNG.slowdownThreeOfAKind(rom) patches.reduceRNG.fixHorseHeads(rom) patches.bomb.onlyDropBombsWhenHaveBombs(rom) - if ap_settings['music_change_condition'] == MusicChangeCondition.option_always: + if world.options.music_change_condition == MusicChangeCondition.option_always: patches.aesthetics.noSwordMusic(rom) - patches.aesthetics.reduceMessageLengths(rom, rnd) + patches.aesthetics.reduceMessageLengths(rom, world.random) patches.aesthetics.allowColorDungeonSpritesEverywhere(rom) - if settings.music == 'random': - patches.music.randomizeMusic(rom, rnd) - elif settings.music == 'off': + if world.ladxr_settings.music == 'random': + patches.music.randomizeMusic(rom, world.random) + elif world.ladxr_settings.music == 'off': patches.music.noMusic(rom) - if settings.noflash: + if world.ladxr_settings.noflash: patches.aesthetics.removeFlashingLights(rom) - if settings.hardmode == "oracle": + if world.ladxr_settings.hardmode == "oracle": patches.hardMode.oracleMode(rom) - elif settings.hardmode == "hero": + elif world.ladxr_settings.hardmode == "hero": patches.hardMode.heroMode(rom) - elif settings.hardmode == "ohko": + elif world.ladxr_settings.hardmode == "ohko": patches.hardMode.oneHitKO(rom) - if settings.superweapons: + if world.ladxr_settings.superweapons: patches.weapons.patchSuperWeapons(rom) - if settings.textmode == 'fast': + if world.ladxr_settings.textmode == 'fast': patches.aesthetics.fastText(rom) - if settings.textmode == 'none': + if world.ladxr_settings.textmode == 'none': patches.aesthetics.fastText(rom) patches.aesthetics.noText(rom) - if not settings.nagmessages: + if not world.ladxr_settings.nagmessages: patches.aesthetics.removeNagMessages(rom) - if settings.lowhpbeep == 'slow': + if world.ladxr_settings.lowhpbeep == 'slow': patches.aesthetics.slowLowHPBeep(rom) - if settings.lowhpbeep == 'none': + if world.ladxr_settings.lowhpbeep == 'none': patches.aesthetics.removeLowHPBeep(rom) - if 0 <= int(settings.linkspalette): - patches.aesthetics.forceLinksPalette(rom, int(settings.linkspalette)) + if 0 <= int(world.ladxr_settings.linkspalette): + patches.aesthetics.forceLinksPalette(rom, int(world.ladxr_settings.linkspalette)) if args.romdebugmode: # The default rom has this build in, just need to set a flag and we get this save. rom.patch(0, 0x0003, "00", "01") # Patch the sword check on the shopkeeper turning around. - if settings.steal == 'never': + if world.ladxr_settings.steal == 'never': rom.patch(4, 0x36F9, "FA4EDB", "3E0000") - elif settings.steal == 'always': + elif world.ladxr_settings.steal == 'always': rom.patch(4, 0x36F9, "FA4EDB", "3E0100") - if settings.hpmode == 'inverted': + if world.ladxr_settings.hpmode == 'inverted': patches.health.setStartHealth(rom, 9) - elif settings.hpmode == '1': + elif world.ladxr_settings.hpmode == '1': patches.health.setStartHealth(rom, 1) patches.inventory.songSelectAfterOcarinaSelect(rom) - if settings.quickswap == 'a': + if world.ladxr_settings.quickswap == 'a': patches.core.quickswap(rom, 1) - elif settings.quickswap == 'b': + elif world.ladxr_settings.quickswap == 'b': patches.core.quickswap(rom, 0) - patches.core.addBootsControls(rom, ap_settings['boots_controls']) + patches.core.addBootsControls(rom, world.options.boots_controls) - world_setup = logic.world_setup + world_setup = world.ladxr_logic.world_setup JUNK_HINT = 0.33 RANDOM_HINT= 0.66 # USEFUL_HINT = 1.0 # TODO: filter events, filter unshuffled keys - all_items = multiworld.get_items() - our_items = [item for item in all_items if item.player == player_id and item.location and item.code is not None and item.location.show_in_spoiler] + all_items = world.multiworld.get_items() + our_items = [item for item in all_items + if item.player == world.player + and item.location + and item.code is not None + and item.location.show_in_spoiler] our_useful_items = [item for item in our_items if ItemClassification.progression in item.classification] def gen_hint(): - chance = rnd.uniform(0, 1) + chance = world.random.uniform(0, 1) if chance < JUNK_HINT: return None elif chance < RANDOM_HINT: - location = rnd.choice(our_items).location + location = world.random.choice(our_items).location else: # USEFUL_HINT - location = rnd.choice(our_useful_items).location + location = world.random.choice(our_useful_items).location - if location.item.player == player_id: + if location.item.player == world.player: name = "Your" else: - name = f"{multiworld.player_name[location.item.player]}'s" + name = f"{world.multiworld.player_name[location.item.player]}'s" if isinstance(location, LinksAwakeningLocation): location_name = location.ladxr_item.metadata.name @@ -277,8 +287,8 @@ def gen_hint(): location_name = location.name hint = f"{name} {location.item} is at {location_name}" - if location.player != player_id: - hint += f" in {multiworld.player_name[location.player]}'s world" + if location.player != world.player: + hint += f" in {world.multiworld.player_name[location.player]}'s world" # Cap hint size at 85 # Realistically we could go bigger but let's be safe instead @@ -286,7 +296,7 @@ def gen_hint(): return hint - hints.addHints(rom, rnd, gen_hint) + hints.addHints(rom, world.random, gen_hint) if world_setup.goal == "raft": patches.goal.setRaftGoal(rom) @@ -299,7 +309,7 @@ def gen_hint(): # Patch the generated logic into the rom patches.chest.setMultiChest(rom, world_setup.multichest) - if settings.overworld not in {"dungeondive", "random"}: + if world.ladxr_settings.overworld not in {"dungeondive", "random"}: patches.entrances.changeEntrances(rom, world_setup.entrance_mapping) for spot in item_list: if spot.item and spot.item.startswith("*"): @@ -318,15 +328,16 @@ def gen_hint(): patches.core.addFrameCounter(rom, len(item_list)) patches.core.warpHome(rom) # Needs to be done after setting the start location. - patches.titleScreen.setRomInfo(rom, auth, seed_name, settings, player_name, player_id) - if ap_settings["ap_title_screen"]: + patches.titleScreen.setRomInfo(rom, world.multi_key, world.multiworld.seed_name, world.ladxr_settings, + world.player_name, world.player) + if world.options.ap_title_screen: patches.titleScreen.setTitleGraphics(rom) patches.endscreen.updateEndScreen(rom) patches.aesthetics.updateSpriteData(rom) if args.doubletrouble: patches.enemies.doubleTrouble(rom) - if ap_settings["text_shuffle"]: + if world.options.text_shuffle: buckets = defaultdict(list) # For each ROM bank, shuffle text within the bank for n, data in enumerate(rom.texts._PointerTable__data): @@ -336,20 +347,20 @@ def gen_hint(): for bucket in buckets.values(): # For each bucket, make a copy and shuffle shuffled = bucket.copy() - rnd.shuffle(shuffled) + world.random.shuffle(shuffled) # Then put new text in for bucket_idx, (orig_idx, data) in enumerate(bucket): rom.texts[shuffled[bucket_idx][0]] = data - if ap_settings["trendy_game"] != TrendyGame.option_normal: + if world.options.trendy_game != TrendyGame.option_normal: # TODO: if 0 or 4, 5, remove inaccurate conveyor tiles room_editor = RoomEditor(rom, 0x2A0) - if ap_settings["trendy_game"] == TrendyGame.option_easy: + if world.options.trendy_game == TrendyGame.option_easy: # Set physics flag on all objects for i in range(0, 6): rom.banks[0x4][0x6F1E + i -0x4000] = 0x4 @@ -360,7 +371,7 @@ def gen_hint(): # Add new conveyor to "push" yoshi (it's only a visual) room_editor.objects.append(Object(5, 3, 0xD0)) - if int(ap_settings["trendy_game"]) >= TrendyGame.option_harder: + if world.options.trendy_game >= TrendyGame.option_harder: """ Data_004_76A0:: db $FC, $00, $04, $00, $00 @@ -374,12 +385,12 @@ def gen_hint(): TrendyGame.option_impossible: (3, 16), } def speed(): - return rnd.randint(*speeds[ap_settings["trendy_game"]]) + return world.random.randint(*speeds[world.options.trendy_game]) rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed() rom.banks[0x4][0x76A2-0x4000] = speed() rom.banks[0x4][0x76A6-0x4000] = speed() rom.banks[0x4][0x76A8-0x4000] = 0xFF - speed() - if int(ap_settings["trendy_game"]) >= TrendyGame.option_hardest: + if world.options.trendy_game >= TrendyGame.option_hardest: rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed() rom.banks[0x4][0x76A3-0x4000] = speed() rom.banks[0x4][0x76A5-0x4000] = speed() @@ -403,10 +414,10 @@ def speed(): for channel in range(3): color[channel] = color[channel] * 31 // 0xbc - if ap_settings["warp_improvements"]: - patches.core.addWarpImprovements(rom, ap_settings["additional_warp_points"]) + if world.options.warp_improvements: + patches.core.addWarpImprovements(rom, world.options.additional_warp_points) - palette = ap_settings["palette"] + palette = world.options.palette if palette != Palette.option_normal: ranges = { # Object palettes @@ -472,8 +483,8 @@ def clamp(x, min, max): SEED_LOCATION = 0x0134 # Patch over the title - assert(len(auth) == 12) - rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(auth)) + assert(len(world.multi_key) == 12) + rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(world.multi_key)) for pymod in pymods: pymod.postPatch(rom) diff --git a/worlds/ladx/LADXR/locations/droppedKey.py b/worlds/ladx/LADXR/locations/droppedKey.py index baa093bb3892..d7998633ce65 100644 --- a/worlds/ladx/LADXR/locations/droppedKey.py +++ b/worlds/ladx/LADXR/locations/droppedKey.py @@ -19,7 +19,7 @@ def __init__(self, room=None): extra = 0x01F8 super().__init__(room, extra) def patch(self, rom, option, *, multiworld=None): - if (option.startswith(MAP) and option != MAP) or (option.startswith(COMPASS) and option != COMPASS) or option.startswith(STONE_BEAK) or (option.startswith(NIGHTMARE_KEY) and option != NIGHTMARE_KEY )or (option.startswith(KEY) and option != KEY): + if (option.startswith(MAP) and option != MAP) or (option.startswith(COMPASS) and option != COMPASS) or (option.startswith(STONE_BEAK) and option != STONE_BEAK) or (option.startswith(NIGHTMARE_KEY) and option != NIGHTMARE_KEY) or (option.startswith(KEY) and option != KEY): if option[-1] == 'P': print(option) if self._location.dungeon == int(option[-1]) and multiworld is None and self.room not in {0x166, 0x223}: diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index f7bf632545f7..c5dcc080537c 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -1,7 +1,9 @@ +from dataclasses import dataclass + import os.path import typing import logging -from Options import Choice, Option, Toggle, DefaultOnToggle, Range, FreeText +from Options import Choice, Toggle, DefaultOnToggle, Range, FreeText, PerGameCommonOptions, OptionGroup from collections import defaultdict import Utils @@ -14,7 +16,7 @@ class LADXROption: def to_ladxr_option(self, all_options): if not self.ladxr_name: return None, None - + return (self.ladxr_name, self.name_lookup[self.value].replace("_", "")) @@ -32,9 +34,10 @@ class Logic(Choice, LADXROption): option_hard = 2 option_glitched = 3 option_hell = 4 - + default = option_normal + class TradeQuest(DefaultOffToggle, LADXROption): """ [On] adds the trade items to the pool (the trade locations will always be local items) @@ -43,11 +46,14 @@ class TradeQuest(DefaultOffToggle, LADXROption): display_name = "Trade Quest" ladxr_name = "tradequest" + class TextShuffle(DefaultOffToggle): """ [On] Shuffles all the text in the game [Off] (default) doesn't shuffle them. """ + display_name = "Text Shuffle" + class Rooster(DefaultOnToggle, LADXROption): """ @@ -57,16 +63,19 @@ class Rooster(DefaultOnToggle, LADXROption): display_name = "Rooster" ladxr_name = "rooster" + class Boomerang(Choice): """ [Normal] requires Magnifying Lens to get the boomerang. [Gift] The boomerang salesman will give you a random item, and the boomerang is shuffled. """ - + display_name = "Boomerang" + normal = 0 gift = 1 default = gift + class EntranceShuffle(Choice, LADXROption): """ [WARNING] Experimental, may fail to fill @@ -75,19 +84,20 @@ class EntranceShuffle(Choice, LADXROption): If random start location and/or dungeon shuffle is enabled, then these will be shuffled with all the non-connector entrance pool. Note, some entrances can lead into water, use the warp-to-home from the save&quit menu to escape this.""" - #[Advanced] Simple, but two-way connector caves are shuffled in their own pool as well. - #[Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool. - #[Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool. + # [Advanced] Simple, but two-way connector caves are shuffled in their own pool as well. + # [Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool. + # [Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool. option_none = 0 option_simple = 1 - #option_advanced = 2 - #option_expert = 3 - #option_insanity = 4 + # option_advanced = 2 + # option_expert = 3 + # option_insanity = 4 default = option_none display_name = "Experimental Entrance Shuffle" ladxr_name = "entranceshuffle" + class DungeonShuffle(DefaultOffToggle, LADXROption): """ [WARNING] Experimental, may fail to fill @@ -96,13 +106,16 @@ class DungeonShuffle(DefaultOffToggle, LADXROption): display_name = "Experimental Dungeon Shuffle" ladxr_name = "dungeonshuffle" + class APTitleScreen(DefaultOnToggle): """ Enables AP specific title screen and disables the intro cutscene """ display_name = "AP Title Screen" + class BossShuffle(Choice): + display_name = "Boss Shuffle" none = 0 shuffle = 1 random = 2 @@ -110,15 +123,18 @@ class BossShuffle(Choice): class DungeonItemShuffle(Choice): + display_name = "Dungeon Item Shuffle" option_original_dungeon = 0 option_own_dungeons = 1 option_own_world = 2 option_any_world = 3 option_different_world = 4 - #option_delete = 5 - #option_start_with = 6 + # option_delete = 5 + # option_start_with = 6 alias_true = 3 alias_false = 0 + ladxr_item: str + class ShuffleNightmareKeys(DungeonItemShuffle): """ @@ -132,6 +148,7 @@ class ShuffleNightmareKeys(DungeonItemShuffle): display_name = "Shuffle Nightmare Keys" ladxr_item = "NIGHTMARE_KEY" + class ShuffleSmallKeys(DungeonItemShuffle): """ Shuffle Small Keys @@ -143,6 +160,8 @@ class ShuffleSmallKeys(DungeonItemShuffle): """ display_name = "Shuffle Small Keys" ladxr_item = "KEY" + + class ShuffleMaps(DungeonItemShuffle): """ Shuffle Dungeon Maps @@ -155,6 +174,7 @@ class ShuffleMaps(DungeonItemShuffle): display_name = "Shuffle Maps" ladxr_item = "MAP" + class ShuffleCompasses(DungeonItemShuffle): """ Shuffle Dungeon Compasses @@ -167,6 +187,7 @@ class ShuffleCompasses(DungeonItemShuffle): display_name = "Shuffle Compasses" ladxr_item = "COMPASS" + class ShuffleStoneBeaks(DungeonItemShuffle): """ Shuffle Owl Beaks @@ -179,6 +200,7 @@ class ShuffleStoneBeaks(DungeonItemShuffle): display_name = "Shuffle Stone Beaks" ladxr_item = "STONE_BEAK" + class ShuffleInstruments(DungeonItemShuffle): """ Shuffle Instruments @@ -195,6 +217,7 @@ class ShuffleInstruments(DungeonItemShuffle): option_vanilla = 100 alias_false = 100 + class Goal(Choice, LADXROption): """ The Goal of the game @@ -207,7 +230,7 @@ class Goal(Choice, LADXROption): option_instruments = 1 option_seashells = 2 option_open = 3 - + default = option_instruments def to_ladxr_option(self, all_options): @@ -216,6 +239,7 @@ def to_ladxr_option(self, all_options): else: return LADXROption.to_ladxr_option(self, all_options) + class InstrumentCount(Range, LADXROption): """ Sets the number of instruments required to open the Egg @@ -226,6 +250,7 @@ class InstrumentCount(Range, LADXROption): range_end = 8 default = 8 + class NagMessages(DefaultOffToggle, LADXROption): """ Controls if nag messages are shown when rocks and crystals are touched. Useful for glitches, annoying for everyone else. @@ -233,6 +258,7 @@ class NagMessages(DefaultOffToggle, LADXROption): display_name = "Nag Messages" ladxr_name = "nagmessages" + class MusicChangeCondition(Choice): """ Controls how the music changes. @@ -243,6 +269,8 @@ class MusicChangeCondition(Choice): option_sword = 0 option_always = 1 default = option_always + + # Setting('hpmode', 'Gameplay', 'm', 'Health mode', options=[('default', '', 'Normal'), ('inverted', 'i', 'Inverted'), ('1', '1', 'Start with 1 heart'), ('low', 'l', 'Low max')], default='default', # description=""" # [Normal} health works as you would expect. @@ -267,10 +295,12 @@ class Bowwow(Choice): [Normal] BowWow is in the item pool, but can be logically expected as a damage source. [Swordless] The progressive swords are removed from the item pool. """ + display_name = "BowWow" normal = 0 swordless = 1 default = normal + class Overworld(Choice, LADXROption): """ [Dungeon Dive] Create a different overworld where all the dungeons are directly accessible and almost no chests are located in the overworld. @@ -284,9 +314,10 @@ class Overworld(Choice, LADXROption): # option_shuffled = 3 default = option_normal -#Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False, + +# Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False, # description='All items will be more powerful, faster, harder, bigger stronger. You name it.'), -#Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none', +# Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none', # description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.', # aesthetic=True), # Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('default', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast', @@ -329,7 +360,7 @@ class BootsControls(Choice): option_bracelet = 1 option_press_a = 2 option_press_b = 3 - + class LinkPalette(Choice, LADXROption): """ @@ -352,6 +383,7 @@ class LinkPalette(Choice, LADXROption): def to_ladxr_option(self, all_options): return self.ladxr_name, str(self.value) + class TrendyGame(Choice): """ [Easy] All of the items hold still for you @@ -370,6 +402,7 @@ class TrendyGame(Choice): option_impossible = 5 default = option_normal + class GfxMod(FreeText, LADXROption): """ Sets the sprite for link, among other things @@ -380,7 +413,7 @@ class GfxMod(FreeText, LADXROption): normal = '' default = 'Link' - __spriteDir: str = Utils.local_path(os.path.join('data', 'sprites','ladx')) + __spriteDir: str = Utils.local_path(os.path.join('data', 'sprites', 'ladx')) __spriteFiles: typing.DefaultDict[str, typing.List[str]] = defaultdict(list) extensions = [".bin", ".bdiff", ".png", ".bmp"] @@ -389,16 +422,15 @@ class GfxMod(FreeText, LADXROption): name, extension = os.path.splitext(file) if extension in extensions: __spriteFiles[name].append(file) - + def __init__(self, value: str): super().__init__(value) - def verify(self, world, player_name: str, plando_options) -> None: if self.value == "Link" or self.value in GfxMod.__spriteFiles: return - raise Exception(f"LADX Sprite '{self.value}' not found. Possible sprites are: {['Link'] + list(GfxMod.__spriteFiles.keys())}") - + raise Exception( + f"LADX Sprite '{self.value}' not found. Possible sprites are: {['Link'] + list(GfxMod.__spriteFiles.keys())}") def to_ladxr_option(self, all_options): if self.value == -1 or self.value == "Link": @@ -407,10 +439,12 @@ def to_ladxr_option(self, all_options): assert self.value in GfxMod.__spriteFiles if len(GfxMod.__spriteFiles[self.value]) > 1: - logger.warning(f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}") + logger.warning( + f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}") return self.ladxr_name, self.__spriteDir + "/" + GfxMod.__spriteFiles[self.value][0] + class Palette(Choice): """ Sets the palette for the game. @@ -430,18 +464,19 @@ class Palette(Choice): option_pink = 4 option_inverted = 5 + class Music(Choice, LADXROption): """ [Vanilla] Regular Music [Shuffled] Shuffled Music [Off] No music """ + display_name = "Music" ladxr_name = "music" option_vanilla = 0 option_shuffled = 1 option_off = 2 - def to_ladxr_option(self, all_options): s = "" if self.value == self.option_shuffled: @@ -450,55 +485,97 @@ def to_ladxr_option(self, all_options): s = "off" return self.ladxr_name, s + class WarpImprovements(DefaultOffToggle): """ [On] Adds remake style warp screen to the game. Choose your warp destination on the map after jumping in a portal and press B to select. [Off] No change """ + display_name = "Warp Improvements" + class AdditionalWarpPoints(DefaultOffToggle): """ [On] (requires warp improvements) Adds a warp point at Crazy Tracy's house (the Mambo teleport spot) and Eagle's Tower [Off] No change """ - - -links_awakening_options: typing.Dict[str, typing.Type[Option]] = { - 'logic': Logic, + display_name = "Additional Warp Points" + +ladx_option_groups = [ + OptionGroup("Goal Options", [ + Goal, + InstrumentCount, + ]), + OptionGroup("Shuffles", [ + ShuffleInstruments, + ShuffleNightmareKeys, + ShuffleSmallKeys, + ShuffleMaps, + ShuffleCompasses, + ShuffleStoneBeaks + ]), + OptionGroup("Warp Points", [ + WarpImprovements, + AdditionalWarpPoints, + ]), + OptionGroup("Miscellaneous", [ + TradeQuest, + Rooster, + TrendyGame, + NagMessages, + BootsControls + ]), + OptionGroup("Experimental", [ + DungeonShuffle, + EntranceShuffle + ]), + OptionGroup("Visuals & Sound", [ + LinkPalette, + Palette, + TextShuffle, + APTitleScreen, + GfxMod, + Music, + MusicChangeCondition + ]) +] + +@dataclass +class LinksAwakeningOptions(PerGameCommonOptions): + logic: Logic # 'heartpiece': DefaultOnToggle, # description='Includes heart pieces in the item pool'), # 'seashells': DefaultOnToggle, # description='Randomizes the secret sea shells hiding in the ground/trees. (chest are always randomized)'), # 'heartcontainers': DefaultOnToggle, # description='Includes boss heart container drops in the item pool'), # 'instruments': DefaultOffToggle, # description='Instruments are placed on random locations, dungeon goal will just contain a random item.'), - 'tradequest': TradeQuest, # description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'), + tradequest: TradeQuest # description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'), # 'witch': DefaultOnToggle, # description='Adds both the toadstool and the reward for giving the toadstool to the witch to the item pool'), - 'rooster': Rooster, # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'), + rooster: Rooster # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'), # 'boomerang': Boomerang, # 'randomstartlocation': DefaultOffToggle, # 'Randomize where your starting house is located'), - 'experimental_dungeon_shuffle': DungeonShuffle, # 'Randomizes the dungeon that each dungeon entrance leads to'), - 'experimental_entrance_shuffle': EntranceShuffle, + experimental_dungeon_shuffle: DungeonShuffle # 'Randomizes the dungeon that each dungeon entrance leads to'), + experimental_entrance_shuffle: EntranceShuffle # 'bossshuffle': BossShuffle, # 'minibossshuffle': BossShuffle, - 'goal': Goal, - 'instrument_count': InstrumentCount, + goal: Goal + instrument_count: InstrumentCount # 'itempool': ItemPool, # 'bowwow': Bowwow, # 'overworld': Overworld, - 'link_palette': LinkPalette, - 'warp_improvements': WarpImprovements, - 'additional_warp_points': AdditionalWarpPoints, - 'trendy_game': TrendyGame, - 'gfxmod': GfxMod, - 'palette': Palette, - 'text_shuffle': TextShuffle, - 'shuffle_nightmare_keys': ShuffleNightmareKeys, - 'shuffle_small_keys': ShuffleSmallKeys, - 'shuffle_maps': ShuffleMaps, - 'shuffle_compasses': ShuffleCompasses, - 'shuffle_stone_beaks': ShuffleStoneBeaks, - 'music': Music, - 'shuffle_instruments': ShuffleInstruments, - 'music_change_condition': MusicChangeCondition, - 'nag_messages': NagMessages, - 'ap_title_screen': APTitleScreen, - 'boots_controls': BootsControls, -} + link_palette: LinkPalette + warp_improvements: WarpImprovements + additional_warp_points: AdditionalWarpPoints + trendy_game: TrendyGame + gfxmod: GfxMod + palette: Palette + text_shuffle: TextShuffle + shuffle_nightmare_keys: ShuffleNightmareKeys + shuffle_small_keys: ShuffleSmallKeys + shuffle_maps: ShuffleMaps + shuffle_compasses: ShuffleCompasses + shuffle_stone_beaks: ShuffleStoneBeaks + music: Music + shuffle_instruments: ShuffleInstruments + music_change_condition: MusicChangeCondition + nag_messages: NagMessages + ap_title_screen: APTitleScreen + boots_controls: BootsControls diff --git a/worlds/ladx/Rom.py b/worlds/ladx/Rom.py index eb573fe5b2cb..8ae1fac0fa31 100644 --- a/worlds/ladx/Rom.py +++ b/worlds/ladx/Rom.py @@ -1,4 +1,4 @@ - +import settings import worlds.Files import hashlib import Utils @@ -32,7 +32,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: def get_base_rom_path(file_name: str = "") -> str: - options = Utils.get_options() + options = settings.get_settings() if not file_name: file_name = options["ladx_options"]["rom_file"] if not os.path.exists(file_name): diff --git a/worlds/ladx/Tracker.py b/worlds/ladx/Tracker.py index 29dce401b895..851fca164453 100644 --- a/worlds/ladx/Tracker.py +++ b/worlds/ladx/Tracker.py @@ -1,4 +1,4 @@ -from worlds.ladx.LADXR.checkMetadata import checkMetadataTable +from .LADXR.checkMetadata import checkMetadataTable import json import logging import websockets diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index f7de0f41f9c0..21876ed671e2 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -1,4 +1,5 @@ import binascii +import dataclasses import os import pkgutil import tempfile @@ -7,7 +8,7 @@ import bsdiff4 import settings -from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial +from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial, MultiWorld from Fill import fill_restrictive from worlds.AutoWorld import WebWorld, World from .Common import * @@ -17,14 +18,14 @@ from .LADXR.itempool import ItemPool as LADXRItemPool from .LADXR.locations.constants import CHEST_ITEMS from .LADXR.locations.instrument import Instrument -from .LADXR.logic import Logic as LAXDRLogic +from .LADXR.logic import Logic as LADXRLogic from .LADXR.main import get_parser from .LADXR.settings import Settings as LADXRSettings from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, create_regions_from_ladxr, get_locations_to_id) -from .Options import DungeonItemShuffle, links_awakening_options, ShuffleInstruments -from .Rom import LADXDeltaPatch +from .Options import DungeonItemShuffle, ShuffleInstruments, LinksAwakeningOptions, ladx_option_groups +from .Rom import LADXDeltaPatch, get_base_rom_path DEVELOPER_MODE = False @@ -64,7 +65,7 @@ class LinksAwakeningWebWorld(WebWorld): ["zig"] )] theme = "dirt" - + option_groups = ladx_option_groups class LinksAwakeningWorld(World): """ @@ -73,8 +74,9 @@ class LinksAwakeningWorld(World): """ game = LINKS_AWAKENING # name of the game/world web = LinksAwakeningWebWorld() - - option_definitions = links_awakening_options # options the player can set + + options_dataclass = LinksAwakeningOptions + options: LinksAwakeningOptions settings: typing.ClassVar[LinksAwakeningSettings] topology_present = True # show path to required location checks in spoiler @@ -102,7 +104,11 @@ class LinksAwakeningWorld(World): prefill_dungeon_items = None - player_options = None + ladxr_settings: LADXRSettings + ladxr_logic: LADXRLogic + ladxr_itempool: LADXRItemPool + + multi_key: bytearray rupees = { ItemName.RUPEES_20: 20, @@ -113,17 +119,13 @@ class LinksAwakeningWorld(World): } def convert_ap_options_to_ladxr_logic(self): - self.player_options = { - option: getattr(self.multiworld, option)[self.player] for option in self.option_definitions - } + self.ladxr_settings = LADXRSettings(dataclasses.asdict(self.options)) - self.laxdr_options = LADXRSettings(self.player_options) - - self.laxdr_options.validate() + self.ladxr_settings.validate() world_setup = LADXRWorldSetup() - world_setup.randomize(self.laxdr_options, self.multiworld.random) - self.ladxr_logic = LAXDRLogic(configuration_options=self.laxdr_options, world_setup=world_setup) - self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict() + world_setup.randomize(self.ladxr_settings, self.random) + self.ladxr_logic = LADXRLogic(configuration_options=self.ladxr_settings, world_setup=world_setup) + self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.ladxr_settings, self.random).toDict() def create_regions(self) -> None: # Initialize @@ -180,8 +182,8 @@ def create_items(self) -> None: # For any and different world, set item rule instead for dungeon_item_type in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]: - option = "shuffle_" + dungeon_item_type - option = self.player_options[option] + option_name = "shuffle_" + dungeon_item_type + option: DungeonItemShuffle = getattr(self.options, option_name) dungeon_item_types[option.ladxr_item] = option.value @@ -189,11 +191,11 @@ def create_items(self) -> None: num_items = 8 if dungeon_item_type == "instruments" else 9 if option.value == DungeonItemShuffle.option_own_world: - self.multiworld.local_items[self.player].value |= { + self.options.local_items.value |= { ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1) } elif option.value == DungeonItemShuffle.option_different_world: - self.multiworld.non_local_items[self.player].value |= { + self.options.non_local_items.value |= { ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1) } # option_original_dungeon = 0 @@ -215,7 +217,7 @@ def create_items(self) -> None: else: item = self.create_item(item_name) - if not self.multiworld.tradequest[self.player] and isinstance(item.item_data, TradeItemData): + if not self.options.tradequest and isinstance(item.item_data, TradeItemData): location = self.multiworld.get_location(item.item_data.vanilla_location, self.player) location.place_locked_item(item) location.show_in_spoiler = False @@ -287,7 +289,7 @@ def force_start_item(self): if item.player == self.player and item.item_data.ladxr_id in start_loc.ladxr_item.OPTIONS and not item.location] if possible_start_items: - index = self.multiworld.random.choice(possible_start_items) + index = self.random.choice(possible_start_items) start_item = self.multiworld.itempool.pop(index) start_loc.place_locked_item(start_item) @@ -336,7 +338,7 @@ def pre_fill(self) -> None: # Get the list of locations and shuffle all_dungeon_locs_to_fill = sorted(all_dungeon_locs) - self.multiworld.random.shuffle(all_dungeon_locs_to_fill) + self.random.shuffle(all_dungeon_locs_to_fill) # Get the list of items and sort by priority def priority(item): @@ -433,6 +435,12 @@ def guess_icon_for_other_world(self, other): return "TRADING_ITEM_LETTER" + @classmethod + def stage_assert_generate(cls, multiworld: MultiWorld): + rom_file = get_base_rom_path() + if not os.path.exists(rom_file): + raise FileNotFoundError(rom_file) + def generate_output(self, output_directory: str): # copy items back to locations for r in self.multiworld.get_regions(self.player): @@ -459,34 +467,19 @@ def generate_output(self, output_directory: str): loc.ladxr_item.location_owner = self.player rom_name = Rom.get_base_rom_path() - out_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.player_name[self.player]}.gbc" + out_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.player_name}.gbc" out_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.gbc") parser = get_parser() args = parser.parse_args([rom_name, "-o", out_name, "--dump"]) - name_for_rom = self.multiworld.player_name[self.player] - - all_names = [self.multiworld.player_name[i + 1] for i in range(len(self.multiworld.player_name))] - - rom = generator.generateRom( - args, - self.laxdr_options, - self.player_options, - self.multi_key, - self.multiworld.seed_name, - self.ladxr_logic, - rnd=self.multiworld.per_slot_randoms[self.player], - player_name=name_for_rom, - player_names=all_names, - player_id = self.player, - multiworld=self.multiworld) + rom = generator.generateRom(args, self) with open(out_path, "wb") as handle: rom.save(handle, name="LADXR") # Write title screen after everything else is done - full gfxmods may stomp over the egg tiles - if self.player_options["ap_title_screen"]: + if self.options.ap_title_screen: with tempfile.NamedTemporaryFile(delete=False) as title_patch: title_patch.write(pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4")) @@ -494,16 +487,16 @@ def generate_output(self, output_directory: str): os.unlink(title_patch.name) patch = LADXDeltaPatch(os.path.splitext(out_path)[0]+LADXDeltaPatch.patch_file_ending, player=self.player, - player_name=self.multiworld.player_name[self.player], patched_path=out_path) + player_name=self.player_name, patched_path=out_path) patch.write() if not DEVELOPER_MODE: os.unlink(out_path) def generate_multi_key(self): - return bytearray(self.multiworld.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big') + return bytearray(self.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big') def modify_multidata(self, multidata: dict): - multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.multiworld.player_name[self.player]] + multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.player_name] def collect(self, state, item: Item) -> bool: change = super().collect(state, item) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 302e7e1d85b2..8d6a7fc4ebee 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -16,6 +16,7 @@ class LingoWebWorld(WebWorld): option_groups = lingo_option_groups + rich_text_options_doc = True theme = "grass" tutorials = [Tutorial( "Multiworld Setup Guide", diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index 1c1f645b8613..333b3e1ef08e 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -9,8 +9,12 @@ class ShuffleDoors(Choice): """If on, opening doors will require their respective "keys". - In "simple", doors are sorted into logical groups, which are all opened by receiving an item. - In "complex", the items are much more granular, and will usually only open a single door each.""" + + - **Simple:** Doors are sorted into logical groups, which are all opened by + receiving an item. + - **Complex:** The items are much more granular, and will usually only open + a single door each. + """ display_name = "Shuffle Doors" option_none = 0 option_simple = 1 @@ -19,24 +23,37 @@ class ShuffleDoors(Choice): class ProgressiveOrangeTower(DefaultOnToggle): """When "Shuffle Doors" is on, this setting governs the manner in which the Orange Tower floors open up. - If off, there is an item for each floor of the tower, and each floor's item is the only one needed to access that floor. - If on, there are six progressive items, which open up the tower from the bottom floor upward. + + - **Off:** There is an item for each floor of the tower, and each floor's + item is the only one needed to access that floor. + - **On:** There are six progressive items, which open up the tower from the + bottom floor upward. """ display_name = "Progressive Orange Tower" class ProgressiveColorful(DefaultOnToggle): """When "Shuffle Doors" is on "complex", this setting governs the manner in which The Colorful opens up. - If off, there is an item for each room of The Colorful, meaning that random rooms in the middle of the sequence can open up without giving you access to them. - If on, there are ten progressive items, which open up the sequence from White forward.""" + + - **Off:** There is an item for each room of The Colorful, meaning that + random rooms in the middle of the sequence can open up without giving you + access to them. + - **On:** There are ten progressive items, which open up the sequence from + White forward. + """ display_name = "Progressive Colorful" class LocationChecks(Choice): """Determines what locations are available. - On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for achievement panels and a small handful of other panels. - On "reduced", many of the locations that are associated with opening doors are removed. - On "insanity", every individual panel in the game is a location check.""" + + - **Normal:** There will be a location check for each panel set that would + ordinarily open a door, as well as for achievement panels and a small + handful of other panels. + - **Reduced:** Many of the locations that are associated with opening doors + are removed. + - **Insanity:** Every individual panel in the game is a location check. + """ display_name = "Location Checks" option_normal = 0 option_reduced = 1 @@ -44,16 +61,20 @@ class LocationChecks(Choice): class ShuffleColors(DefaultOnToggle): - """ - If on, an item is added to the pool for every puzzle color (besides White). - You will need to unlock the requisite colors in order to be able to solve puzzles of that color. + """If on, an item is added to the pool for every puzzle color (besides White). + + You will need to unlock the requisite colors in order to be able to solve + puzzles of that color. """ display_name = "Shuffle Colors" class ShufflePanels(Choice): """If on, the puzzles on each panel are randomized. - On "rearrange", the puzzles are the same as the ones in the base game, but are placed in different areas.""" + + On "rearrange", the puzzles are the same as the ones in the base game, but + are placed in different areas. + """ display_name = "Shuffle Panels" option_none = 0 option_rearrange = 1 @@ -66,22 +87,26 @@ class ShufflePaintings(Toggle): class EnablePilgrimage(Toggle): """Determines how the pilgrimage works. - If on, you are required to complete a pilgrimage in order to access the Pilgrim Antechamber. - If off, the pilgrimage will be deactivated, and the sun painting will be added to the pool, even if door shuffle is off.""" + + - **On:** You are required to complete a pilgrimage in order to access the + Pilgrim Antechamber. + - **Off:** The pilgrimage will be deactivated, and the sun painting will be + added to the pool, even if door shuffle is off. + """ display_name = "Enable Pilgrimage" class PilgrimageAllowsRoofAccess(DefaultOnToggle): - """ - If on, you may use the Crossroads roof access during a pilgrimage (and you may be expected to do so). + """If on, you may use the Crossroads roof access during a pilgrimage (and you may be expected to do so). + Otherwise, pilgrimage will be deactivated when going up the stairs. """ display_name = "Allow Roof Access for Pilgrimage" class PilgrimageAllowsPaintings(DefaultOnToggle): - """ - If on, you may use paintings during a pilgrimage (and you may be expected to do so). + """If on, you may use paintings during a pilgrimage (and you may be expected to do so). + Otherwise, pilgrimage will be deactivated when going through a painting. """ display_name = "Allow Paintings for Pilgrimage" @@ -89,11 +114,17 @@ class PilgrimageAllowsPaintings(DefaultOnToggle): class SunwarpAccess(Choice): """Determines how access to sunwarps works. - On "normal", all sunwarps are enabled from the start. - On "disabled", all sunwarps are disabled. Pilgrimage must be disabled when this is used. - On "unlock", sunwarps start off disabled, and all six activate once you receive an item. - On "individual", sunwarps start off disabled, and each has a corresponding item that unlocks it. - On "progressive", sunwarps start off disabled, and they unlock in order using a progressive item.""" + + - **Normal:** All sunwarps are enabled from the start. + - **Disabled:** All sunwarps are disabled. Pilgrimage must be disabled when + this is used. + - **Unlock:** Sunwarps start off disabled, and all six activate once you + receive an item. + - **Individual:** Sunwarps start off disabled, and each has a corresponding + item that unlocks it. + - **Progressive:** Sunwarps start off disabled, and they unlock in order + using a progressive item. + """ display_name = "Sunwarp Access" option_normal = 0 option_disabled = 1 @@ -109,10 +140,16 @@ class ShuffleSunwarps(Toggle): class VictoryCondition(Choice): """Change the victory condition. - On "the_end", the goal is to solve THE END at the top of the tower. - On "the_master", the goal is to solve THE MASTER at the top of the tower, after getting the number of achievements specified in the Mastery Achievements option. - On "level_2", the goal is to solve LEVEL 2 in the second room, after solving the number of panels specified in the Level 2 Requirement option. - On "pilgrimage", the goal is to solve PILGRIM in the Pilgrim Antechamber, typically after performing a Pilgrimage.""" + + - **The End:** the goal is to solve THE END at the top of the tower. + - **The Master:** The goal is to solve THE MASTER at the top of the tower, + after getting the number of achievements specified in the Mastery + Achievements option. + - **Level 2:** The goal is to solve LEVEL 2 in the second room, after + solving the number of panels specified in the Level 2 Requirement option. + - **Pilgrimage:** The goal is to solve PILGRIM in the Pilgrim Antechamber, + typically after performing a Pilgrimage. + """ display_name = "Victory Condition" option_the_end = 0 option_the_master = 1 @@ -122,9 +159,12 @@ class VictoryCondition(Choice): class MasteryAchievements(Range): """The number of achievements required to unlock THE MASTER. - In the base game, 21 achievements are needed. - If you include The Scientific and The Unchallenged, which are in the base game but are not counted for mastery, 23 would be required. - If you include the custom achievement (The Wanderer), 24 would be required. + + - In the base game, 21 achievements are needed. + - If you include The Scientific and The Unchallenged, which are in the base + game but are not counted for mastery, 23 would be required. + - If you include the custom achievement (The Wanderer), 24 would be + required. """ display_name = "Mastery Achievements" range_start = 1 @@ -134,9 +174,10 @@ class MasteryAchievements(Range): class Level2Requirement(Range): """The number of panel solves required to unlock LEVEL 2. - In the base game, 223 are needed. - Note that this count includes ANOTHER TRY. - When set to 1, the panel hunt is disabled, and you can access LEVEL 2 for free. + + In the base game, 223 are needed. Note that this count includes ANOTHER TRY. + When set to 1, the panel hunt is disabled, and you can access LEVEL 2 for + free. """ display_name = "Level 2 Requirement" range_start = 1 @@ -145,9 +186,10 @@ class Level2Requirement(Range): class EarlyColorHallways(Toggle): - """ - When on, a painting warp to the color hallways area will appear in the starting room. - This lets you avoid being trapped in the starting room for long periods of time when door shuffle is on. + """When on, a painting warp to the color hallways area will appear in the starting room. + + This lets you avoid being trapped in the starting room for long periods of + time when door shuffle is on. """ display_name = "Early Color Hallways" @@ -161,8 +203,8 @@ class TrapPercentage(Range): class TrapWeights(OptionDict): - """ - Specify the distribution of traps that should be placed into the pool. + """Specify the distribution of traps that should be placed into the pool. + If you don't want a specific type of trap, set the weight to zero. """ display_name = "Trap Weights" diff --git a/worlds/lufia2ac/Client.py b/worlds/lufia2ac/Client.py index 1e8437d20e84..633bd93ef0c9 100644 --- a/worlds/lufia2ac/Client.py +++ b/worlds/lufia2ac/Client.py @@ -147,7 +147,7 @@ async def game_watcher(self, ctx: SNIContext) -> None: snes_items_received += 1 snes_logger.info("Received %s from %s (%s) (%d/%d in list)" % ( - ctx.item_names.lookup_in_slot(item.item), + ctx.item_names.lookup_in_game(item.item), ctx.player_names[item.player], ctx.location_names.lookup_in_slot(item.location, item.player), snes_items_received, len(ctx.items_received))) diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index ff1b75d70f27..85b73dec4147 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -231,6 +231,8 @@ def __init__(self, world: "MessengerWorld") -> None: self.is_aerobatic, "Autumn Hills Seal - Trip Saws": self.has_wingsuit, + "Autumn Hills Seal - Double Swing Saws": + self.has_vertical, # forlorn temple "Forlorn Temple Seal - Rocket Maze": self.has_vertical, @@ -430,6 +432,8 @@ def __init__(self, world: "MessengerWorld") -> None: { "Autumn Hills Seal - Spike Ball Darts": lambda state: self.has_vertical(state) and self.has_windmill(state) or self.is_aerobatic(state), + "Autumn Hills Seal - Double Swing Saws": + lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state), "Bamboo Creek - Claustro": self.has_wingsuit, "Bamboo Creek Seal - Spike Ball Pits": diff --git a/worlds/mlss/data/basepatch.bsdiff b/worlds/mlss/data/basepatch.bsdiff index 156d28f346e8..8f9324995ec4 100644 Binary files a/worlds/mlss/data/basepatch.bsdiff and b/worlds/mlss/data/basepatch.bsdiff differ diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index d822a3dc3839..6f48d6af9fdd 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -553,7 +553,17 @@ NOVA|73-4|Happy Otaku Pack Vol.19|True|6|8|10| Heaven's Gradius|73-5|Happy Otaku Pack Vol.19|True|6|8|10| Ray Tuning|74-0|CHUNITHM COURSE MUSE|True|6|8|10| World Vanquisher|74-1|CHUNITHM COURSE MUSE|True|6|8|10|11 -Territory Battles|74-2|CHUNITHM COURSE MUSE|True|5|7|9| +Tsukuyomi Ni Naru|74-2|CHUNITHM COURSE MUSE|False|5|7|9| The wheel to the right|74-3|CHUNITHM COURSE MUSE|True|5|7|9|11 Climax|74-4|CHUNITHM COURSE MUSE|True|4|8|11|11 Spider's Thread|74-5|CHUNITHM COURSE MUSE|True|5|8|10|12 +HIT ME UP|43-54|MD Plus Project|True|4|6|8| +Test Me feat. Uyeon|43-55|MD Plus Project|True|3|5|9| +Assault TAXI|43-56|MD Plus Project|True|4|7|10| +No|43-57|MD Plus Project|False|4|6|9| +Pop it|43-58|MD Plus Project|True|1|3|6| +HEARTBEAT! KyunKyun!|43-59|MD Plus Project|True|4|6|9| +SUPERHERO|75-0|Novice Rider Pack|False|2|4|7| +Highway_Summer|75-1|Novice Rider Pack|True|2|4|6| +Mx. Black Box|75-2|Novice Rider Pack|True|5|7|9| +Sweet Encounter|75-3|Novice Rider Pack|True|2|4|7| diff --git a/worlds/musedash/docs/setup_en.md b/worlds/musedash/docs/setup_en.md index 312cdbd1958f..a3c2f43a91f0 100644 --- a/worlds/musedash/docs/setup_en.md +++ b/worlds/musedash/docs/setup_en.md @@ -17,11 +17,12 @@ ## Installing the Archipelago mod to Muse Dash 1. Download [MelonLoader.Installer.exe](https://github.com/LavaGang/MelonLoader/releases/latest) and run it. -2. Choose the automated tab, click the select button and browse to `MuseDash.exe`. Then click install. +2. Choose the automated tab, click the select button and browse to `MuseDash.exe`. - You can find the folder in steam by finding the game in your library, right clicking it and choosing *Manage→Browse Local Files*. - If you click the bar at the top telling you your current folder, this will give you a path you can copy. If you paste that into the window popped up by **MelonLoader**, it will automatically go to the same folder. -3. Run the game once, and wait until you get to the Muse Dash start screen before exiting. -4. Download the latest [Muse Dash Archipelago Mod](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) and then extract that into the newly created `/Mods/` folder in MuseDash's install location. +3. Uncheck "Latest" and select v0.6.1. Then click install. +4. Run the game once, and wait until you get to the Muse Dash start screen before exiting. +5. Download the latest [Muse Dash Archipelago Mod](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) and then extract that into the newly created `/Mods/` folder in MuseDash's install location. - All files must be under the `/Mods/` folder and not within a sub folder inside of `/Mods/` If you've successfully installed everything, a button will appear in the bottom right which will allow you to log into an Archipelago server. diff --git a/worlds/musedash/docs/setup_es.md b/worlds/musedash/docs/setup_es.md index 1b16c7af3f54..fe5358921def 100644 --- a/worlds/musedash/docs/setup_es.md +++ b/worlds/musedash/docs/setup_es.md @@ -17,11 +17,12 @@ ## Instalar el mod de Archipelago en Muse Dash 1. Descarga [MelonLoader.Installer.exe](https://github.com/LavaGang/MelonLoader/releases/latest) y ejecutalo. -2. Elije la pestaña "automated", haz clic en el botón "select" y busca tu `MuseDash.exe`. Luego haz clic en "install". +2. Elije la pestaña "automated", haz clic en el botón "select" y busca tu `MuseDash.exe`. - Puedes encontrar la carpeta en Steam buscando el juego en tu biblioteca, haciendo clic derecho sobre el y elegir *Administrar→Ver archivos locales*. - Si haces clic en la barra superior que te indica la carpeta en la que estas, te dará la dirección de ésta para que puedas copiarla. Al pegar esa dirección en la ventana que **MelonLoader** abre, irá automaticamente a esa carpeta. -3. Ejecuta el juego una vez, y espera hasta que aparezca la pantalla de inicio de Muse Dash antes de cerrarlo. -4. Descarga la última version de [Muse Dash Archipelago Mod](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) y extraelo en la nueva carpeta creada llamada `/Mods/`, localizada en la carpeta de instalación de Muse Dash. +3. Desmarca "Latest" y selecciona v0.6.1. Luego haz clic en "install". +4. Ejecuta el juego una vez, y espera hasta que aparezca la pantalla de inicio de Muse Dash antes de cerrarlo. +5. Descarga la última version de [Muse Dash Archipelago Mod](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) y extraelo en la nueva carpeta creada llamada `/Mods/`, localizada en la carpeta de instalación de Muse Dash. - Todos los archivos deben ir directamente en la carpeta `/Mods/`, y NO en una subcarpeta dentro de la carpeta `/Mods/` Si todo fue instalado correctamente, un botón aparecerá en la parte inferior derecha del juego una vez abierto, que te permitirá conectarte al servidor de Archipelago. diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 34b3935fec4e..89f10a5a2da0 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -173,6 +173,15 @@ class OOTWorld(World): "Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Cojiro", "Odd Mushroom", "Odd Potion", "Poachers Saw", "Broken Sword", "Prescription", "Eyeball Frog", "Eyedrops", "Claim Check"}, + "Keys": {"Small Key (Bottom of the Well)", "Small Key (Fire Temple)", "Small Key (Forest Temple)", + "Small Key (Ganons Castle)", "Small Key (Gerudo Training Ground)", "Small Key (Shadow Temple)", + "Small Key (Spirit Temple)", "Small Key (Thieves Hideout)", "Small Key (Water Temple)", + "Small Key Ring (Bottom of the Well)", "Small Key Ring (Fire Temple)", + "Small Key Ring (Forest Temple)", "Small Key Ring (Ganons Castle)", + "Small Key Ring (Gerudo Training Ground)", "Small Key Ring (Shadow Temple)", + "Small Key Ring (Spirit Temple)", "Small Key Ring (Thieves Hideout)", "Small Key Ring (Water Temple)", + "Boss Key (Fire Temple)", "Boss Key (Forest Temple)", "Boss Key (Ganons Castle)", + "Boss Key (Shadow Temple)", "Boss Key (Spirit Temple)", "Boss Key (Water Temple)"}, } location_name_groups = build_location_name_groups() @@ -1173,10 +1182,35 @@ def hint_type_players(hint_type: str) -> set: def fill_slot_data(self): self.collectible_flags_available.wait() - return { + + slot_data = { 'collectible_override_flags': self.collectible_override_flags, 'collectible_flag_offsets': self.collectible_flag_offsets } + slot_data.update(self.options.as_dict( + "open_forest", "open_kakariko", "open_door_of_time", "zora_fountain", "gerudo_fortress", + "bridge", "bridge_stones", "bridge_medallions", "bridge_rewards", "bridge_tokens", "bridge_hearts", + "shuffle_ganon_bosskey", "ganon_bosskey_medallions", "ganon_bosskey_stones", "ganon_bosskey_rewards", + "ganon_bosskey_tokens", "ganon_bosskey_hearts", "trials", + "triforce_hunt", "triforce_goal", "extra_triforce_percentage", + "shopsanity", "shop_slots", "shopsanity_prices", "tokensanity", + "dungeon_shortcuts", "dungeon_shortcuts_list", + "mq_dungeons_mode", "mq_dungeons_list", "mq_dungeons_count", + "shuffle_interior_entrances", "shuffle_grotto_entrances", "shuffle_dungeon_entrances", + "shuffle_overworld_entrances", "shuffle_bosses", "key_rings", "key_rings_list", "enhance_map_compass", + "shuffle_mapcompass", "shuffle_smallkeys", "shuffle_hideoutkeys", "shuffle_bosskeys", + "logic_rules", "logic_no_night_tokens_without_suns_song", "logic_tricks", + "warp_songs", "shuffle_song_items","shuffle_medigoron_carpet_salesman", "shuffle_frog_song_rupees", + "shuffle_scrubs", "shuffle_child_trade", "shuffle_freestanding_items", "shuffle_pots", "shuffle_crates", + "shuffle_cows", "shuffle_beehives", "shuffle_kokiri_sword", "shuffle_ocarinas", "shuffle_gerudo_card", + "shuffle_beans", "starting_age", "bombchus_in_logic", "spawn_positions", "owl_drops", + "no_epona_race", "skip_some_minigame_phases", "complete_mask_quest", "free_scarecrow", "plant_beans", + "chicken_count", "big_poe_count", "fae_torch_count", "blue_fire_arrows", + "damage_multiplier", "deadly_bonks", "starting_tod", "junk_ice_traps", + "start_with_consumables", "adult_trade_start", "plando_connections" + ) + ) + return slot_data def modify_multidata(self, multidata: dict): diff --git a/worlds/overcooked2/test/TestOvercooked2.py b/worlds/overcooked2/test/TestOvercooked2.py index ee0b44a86e9f..a3c1c3dc0d3e 100644 --- a/worlds/overcooked2/test/TestOvercooked2.py +++ b/worlds/overcooked2/test/TestOvercooked2.py @@ -4,10 +4,11 @@ from worlds.AutoWorld import AutoWorldRegister from test.general import setup_solo_multiworld -from worlds.overcooked2.Items import * -from worlds.overcooked2.Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, OverworldRegion, overworld_region_by_level, level_id_to_shortname -from worlds.overcooked2.Logic import level_logic, overworld_region_logic, level_shuffle_factory -from worlds.overcooked2.Locations import oc2_location_name_to_id +from ..Items import * +from ..Overcooked2Levels import (Overcooked2Dlc, Overcooked2Level, OverworldRegion, overworld_region_by_level, + level_id_to_shortname) +from ..Logic import level_logic, overworld_region_logic, level_shuffle_factory +from ..Locations import oc2_location_name_to_id class Overcooked2Test(unittest.TestCase): diff --git a/worlds/pokemon_emerald/README.md b/worlds/pokemon_emerald/README.md deleted file mode 100644 index 8441afc56ae6..000000000000 --- a/worlds/pokemon_emerald/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Pokemon Emerald - -Version 2.0.0 diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py index f93441baeac1..5b2aaa1ffcd0 100644 --- a/worlds/pokemon_emerald/rules.py +++ b/worlds/pokemon_emerald/rules.py @@ -558,6 +558,10 @@ def get_location(location: str): get_location("NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON"), lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player) ) + set_rule( + get_location("NPC_GIFT_RECEIVED_COIN_CASE"), + lambda state: state.has("EVENT_BUY_HARBOR_MAIL", world.player) + ) # Route 117 set_rule( @@ -1638,10 +1642,6 @@ def get_location(location: str): get_location("NPC_GIFT_GOT_TM_THUNDERBOLT_FROM_WATTSON"), lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player) and state.has("EVENT_TURN_OFF_GENERATOR", world.player) ) - set_rule( - get_location("NPC_GIFT_RECEIVED_COIN_CASE"), - lambda state: state.has("EVENT_BUY_HARBOR_MAIL", world.player) - ) # Fallarbor Town set_rule( diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 003e0a32e97d..5f527033289a 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -661,6 +661,9 @@ def fill_slot_data(self) -> dict: "dark_rock_tunnel_logic": self.multiworld.dark_rock_tunnel_logic[self.player].value, "split_card_key": self.multiworld.split_card_key[self.player].value, "all_elevators_locked": self.multiworld.all_elevators_locked[self.player].value, + "require_pokedex": self.multiworld.require_pokedex[self.player].value, + "area_1_to_1_mapping": self.multiworld.area_1_to_1_mapping[self.player].value, + "blind_trainers": self.multiworld.blind_trainers[self.player].value, } diff --git a/worlds/pokemon_rb/docs/setup_en.md b/worlds/pokemon_rb/docs/setup_en.md index 45b0175eac9d..773fb14da9e7 100644 --- a/worlds/pokemon_rb/docs/setup_en.md +++ b/worlds/pokemon_rb/docs/setup_en.md @@ -15,7 +15,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst ## Optional Software -- [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/j-imbo/pkmnrb_jim/releases/latest), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases) +- [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/coveleski/rb_tracker/releases/latest), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases) ## Configuring BizHawk @@ -109,7 +109,7 @@ server uses password, type in the bottom textfield `/connect
: [p Pokémon Red and Blue has a fully functional map tracker that supports auto-tracking. -1. Download [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/j-imbo/pkmnrb_jim/releases/latest) and [PopTracker](https://github.com/black-sliver/PopTracker/releases). +1. Download [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/coveleski/rb_tracker/releases/latest) and [PopTracker](https://github.com/black-sliver/PopTracker/releases). 2. Open PopTracker, and load the Pokémon Red and Blue pack. 3. Click on the "AP" symbol at the top. 4. Enter the AP address, slot name and password. diff --git a/worlds/pokemon_rb/docs/setup_es.md b/worlds/pokemon_rb/docs/setup_es.md index 9d87db224bb7..6499c9501263 100644 --- a/worlds/pokemon_rb/docs/setup_es.md +++ b/worlds/pokemon_rb/docs/setup_es.md @@ -17,7 +17,7 @@ Al usar BizHawk, esta guía solo es aplicable en los sistemas de Windows y Linux ## Software Opcional -- [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/j-imbo/pkmnrb_jim/releases/latest), para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases) +- [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/coveleski/rb_tracker/releases/latest), para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases) ## Configurando BizHawk @@ -101,7 +101,7 @@ Ahora ya estás listo para tu aventura en Kanto. Pokémon Red and Blue tiene un mapa completamente funcional que soporta seguimiento automático. -1. Descarga el [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/j-imbo/pkmnrb_jim/releases/latest) y [PopTracker](https://github.com/black-sliver/PopTracker/releases). +1. Descarga el [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/coveleski/rb_tracker/releases/latest) y [PopTracker](https://github.com/black-sliver/PopTracker/releases). 2. Abre PopTracker, y carga el pack de Pokémon Red and Blue. 3. Haz clic en el símbolo "AP" en la parte superior. 4. Ingresa la dirección de AP, nombre del slot y contraseña (si es que hay). diff --git a/worlds/raft/Options.py b/worlds/raft/Options.py index b9d0a2298a07..efe460b50353 100644 --- a/worlds/raft/Options.py +++ b/worlds/raft/Options.py @@ -1,4 +1,5 @@ -from Options import Range, Toggle, DefaultOnToggle, Choice, DeathLink +from dataclasses import dataclass +from Options import Range, Toggle, DefaultOnToggle, Choice, DeathLink, PerGameCommonOptions class MinimumResourcePackAmount(Range): """The minimum amount of resources available in a resource pack""" @@ -32,7 +33,13 @@ class FillerItemTypes(Choice): option_both = 2 class IslandFrequencyLocations(Choice): - """Sets where frequencies for story islands are located.""" + """Sets where frequencies for story islands are located. + Vanilla will keep frequencies in their vanilla, non-randomized locations. + Random On Island will randomize each frequency within its vanilla island, but will preserve island order. + Random Island Order will change the order you visit islands, but will preserve the vanilla location of each frequency unlock. + Random On Island Random Order will randomize the location containing the frequency on each island and randomize the order. + Progressive will randomize the frequencies to anywhere, but will always unlock the frequencies in vanilla order as the frequency items are received. + Anywhere will randomize the frequencies to anywhere, and frequencies will be received in any order.""" display_name = "Frequency locations" option_vanilla = 0 option_random_on_island = 1 @@ -41,6 +48,8 @@ class IslandFrequencyLocations(Choice): option_progressive = 4 option_anywhere = 5 default = 2 + def is_filling_frequencies_in_world(self): + return self.value <= self.option_random_on_island_random_order class IslandGenerationDistance(Choice): """Sets how far away islands spawn from you when you input their coordinates into the Receiver.""" @@ -53,7 +62,8 @@ class IslandGenerationDistance(Choice): default = 8 class ExpensiveResearch(Toggle): - """Makes unlocking items in the Crafting Table consume the researched items.""" + """If No is selected, researching items and unlocking items in the Crafting Table works the same as vanilla Raft. + If Yes is selected, each unlock in the Crafting Table will require its own set of researched items in order to unlock it.""" display_name = "Expensive research" class ProgressiveItems(DefaultOnToggle): @@ -66,20 +76,19 @@ class BigIslandEarlyCrafting(Toggle): display_name = "Early recipes behind big islands" class PaddleboardMode(Toggle): - """Sets later story islands to in logic without an Engine or Steering Wheel. May require lots of paddling. Not - recommended.""" + """Sets later story islands to be in logic without an Engine or Steering Wheel. May require lots of paddling.""" display_name = "Paddleboard Mode" -raft_options = { - "minimum_resource_pack_amount": MinimumResourcePackAmount, - "maximum_resource_pack_amount": MaximumResourcePackAmount, - "duplicate_items": DuplicateItems, - "filler_item_types": FillerItemTypes, - "island_frequency_locations": IslandFrequencyLocations, - "island_generation_distance": IslandGenerationDistance, - "expensive_research": ExpensiveResearch, - "progressive_items": ProgressiveItems, - "big_island_early_crafting": BigIslandEarlyCrafting, - "paddleboard_mode": PaddleboardMode, - "death_link": DeathLink -} +@dataclass +class RaftOptions(PerGameCommonOptions): + minimum_resource_pack_amount: MinimumResourcePackAmount + maximum_resource_pack_amount: MaximumResourcePackAmount + duplicate_items: DuplicateItems + filler_item_types: FillerItemTypes + island_frequency_locations: IslandFrequencyLocations + island_generation_distance: IslandGenerationDistance + expensive_research: ExpensiveResearch + progressive_items: ProgressiveItems + big_island_early_crafting: BigIslandEarlyCrafting + paddleboard_mode: PaddleboardMode + death_link: DeathLink diff --git a/worlds/raft/Rules.py b/worlds/raft/Rules.py index e84068a6f584..b6bd49c187cd 100644 --- a/worlds/raft/Rules.py +++ b/worlds/raft/Rules.py @@ -5,10 +5,10 @@ class RaftLogic(LogicMixin): def raft_paddleboard_mode_enabled(self, player): - return self.multiworld.paddleboard_mode[player].value + return bool(self.multiworld.worlds[player].options.paddleboard_mode) def raft_big_islands_available(self, player): - return self.multiworld.big_island_early_crafting[player].value or self.raft_can_access_radio_tower(player) + return bool(self.multiworld.worlds[player].options.big_island_early_crafting) or self.raft_can_access_radio_tower(player) def raft_can_smelt_items(self, player): return self.has("Smelter", player) diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index e96cd4471268..71d5d1c7e44b 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -6,7 +6,7 @@ from .Regions import create_regions, getConnectionName from .Rules import set_rules -from .Options import raft_options +from .Options import RaftOptions from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, Tutorial from ..AutoWorld import World, WebWorld @@ -37,16 +37,17 @@ class RaftWorld(World): lastItemId = max(filter(lambda val: val is not None, item_name_to_id.values())) location_name_to_id = locations_lookup_name_to_id - option_definitions = raft_options + options_dataclass = RaftOptions + options: RaftOptions required_client_version = (0, 3, 4) def create_items(self): - minRPSpecified = self.multiworld.minimum_resource_pack_amount[self.player].value - maxRPSpecified = self.multiworld.maximum_resource_pack_amount[self.player].value + minRPSpecified = self.options.minimum_resource_pack_amount.value + maxRPSpecified = self.options.maximum_resource_pack_amount.value minimumResourcePackAmount = min(minRPSpecified, maxRPSpecified) maximumResourcePackAmount = max(minRPSpecified, maxRPSpecified) - isFillingFrequencies = self.multiworld.island_frequency_locations[self.player].value <= 3 + isFillingFrequencies = self.options.island_frequency_locations.is_filling_frequencies_in_world() # Generate item pool pool = [] frequencyItems = [] @@ -64,20 +65,20 @@ def create_items(self): extraItemNamePool = [] extras = len(location_table) - len(item_table) - 1 # Victory takes up 1 unaccounted-for slot if extras > 0: - if (self.multiworld.filler_item_types[self.player].value != 1): # Use resource packs + if (self.options.filler_item_types != self.options.filler_item_types.option_duplicates): # Use resource packs for packItem in resourcePackItems: for i in range(minimumResourcePackAmount, maximumResourcePackAmount + 1): extraItemNamePool.append(createResourcePackName(i, packItem)) - if self.multiworld.filler_item_types[self.player].value != 0: # Use duplicate items + if self.options.filler_item_types != self.options.filler_item_types.option_resource_packs: # Use duplicate items dupeItemPool = item_table.copy() # Remove frequencies if necessary - if self.multiworld.island_frequency_locations[self.player].value != 5: # Not completely random locations + if self.options.island_frequency_locations != self.options.island_frequency_locations.option_anywhere: # Not completely random locations # If we let frequencies stay in with progressive-frequencies, the progressive-frequency item # will be included 7 times. This is a massive flood of progressive-frequency items, so we # instead add progressive-frequency as its own item a smaller amount of times to prevent # flooding the duplicate item pool with them. - if self.multiworld.island_frequency_locations[self.player].value == 4: + if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive: for _ in range(2): # Progressives are not in item_pool, need to create faux item for duplicate item pool # This can still be filtered out later by duplicate_items setting @@ -86,9 +87,9 @@ def create_items(self): dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"]) # Remove progression or non-progression items if necessary - if (self.multiworld.duplicate_items[self.player].value == 0): # Progression only + if (self.options.duplicate_items == self.options.duplicate_items.option_progression): # Progression only dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True) - elif (self.multiworld.duplicate_items[self.player].value == 1): # Non-progression only + elif (self.options.duplicate_items == self.options.duplicate_items.option_non_progression): # Non-progression only dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False) dupeItemPool = list(dupeItemPool) @@ -115,14 +116,14 @@ def create_regions(self): create_regions(self.multiworld, self.player) def get_pre_fill_items(self): - if self.multiworld.island_frequency_locations[self.player] in [0, 1, 2, 3]: + if self.options.island_frequency_locations.is_filling_frequencies_in_world(): return [loc.item for loc in self.multiworld.get_filled_locations()] return [] def create_item_replaceAsNecessary(self, name: str) -> Item: isFrequency = "Frequency" in name - shouldUseProgressive = ((isFrequency and self.multiworld.island_frequency_locations[self.player].value == 4) - or (not isFrequency and self.multiworld.progressive_items[self.player].value)) + shouldUseProgressive = bool((isFrequency and self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive) + or (not isFrequency and self.options.progressive_items)) if shouldUseProgressive and name in progressive_table: name = progressive_table[name] return self.create_item(name) @@ -152,7 +153,7 @@ def collect_item(self, state, item, remove=False): return super(RaftWorld, self).collect_item(state, item, remove) def pre_fill(self): - if self.multiworld.island_frequency_locations[self.player] == 0: # Vanilla + if self.options.island_frequency_locations == self.options.island_frequency_locations.option_vanilla: self.setLocationItem("Radio Tower Frequency to Vasagatan", "Vasagatan Frequency") self.setLocationItem("Vasagatan Frequency to Balboa", "Balboa Island Frequency") self.setLocationItem("Relay Station quest", "Caravan Island Frequency") @@ -160,7 +161,7 @@ def pre_fill(self): self.setLocationItem("Tangaroa Frequency to Varuna Point", "Varuna Point Frequency") self.setLocationItem("Varuna Point Frequency to Temperance", "Temperance Frequency") self.setLocationItem("Temperance Frequency to Utopia", "Utopia Frequency") - elif self.multiworld.island_frequency_locations[self.player] == 1: # Random on island + elif self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_on_island: self.setLocationItemFromRegion("RadioTower", "Vasagatan Frequency") self.setLocationItemFromRegion("Vasagatan", "Balboa Island Frequency") self.setLocationItemFromRegion("BalboaIsland", "Caravan Island Frequency") @@ -168,7 +169,10 @@ def pre_fill(self): self.setLocationItemFromRegion("Tangaroa", "Varuna Point Frequency") self.setLocationItemFromRegion("Varuna Point", "Temperance Frequency") self.setLocationItemFromRegion("Temperance", "Utopia Frequency") - elif self.multiworld.island_frequency_locations[self.player] in [2, 3]: + elif self.options.island_frequency_locations in [ + self.options.island_frequency_locations.option_random_island_order, + self.options.island_frequency_locations.option_random_on_island_random_order + ]: locationToFrequencyItemMap = { "Vasagatan": "Vasagatan Frequency", "BalboaIsland": "Balboa Island Frequency", @@ -196,9 +200,9 @@ def pre_fill(self): else: currentLocation = availableLocationList[0] # Utopia (only one left in list) availableLocationList.remove(currentLocation) - if self.multiworld.island_frequency_locations[self.player] == 2: # Random island order + if self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_island_order: self.setLocationItem(locationToVanillaFrequencyLocationMap[previousLocation], locationToFrequencyItemMap[currentLocation]) - elif self.multiworld.island_frequency_locations[self.player] == 3: # Random on island random order + elif self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_on_island_random_order: self.setLocationItemFromRegion(previousLocation, locationToFrequencyItemMap[currentLocation]) previousLocation = currentLocation @@ -215,9 +219,9 @@ def setLocationItemFromRegion(self, region: str, itemName: str): def fill_slot_data(self): return { - "IslandGenerationDistance": self.multiworld.island_generation_distance[self.player].value, - "ExpensiveResearch": bool(self.multiworld.expensive_research[self.player].value), - "DeathLink": bool(self.multiworld.death_link[self.player].value) + "IslandGenerationDistance": self.options.island_generation_distance.value, + "ExpensiveResearch": bool(self.options.expensive_research), + "DeathLink": bool(self.options.death_link) } def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): diff --git a/worlds/raft/docs/setup_en.md b/worlds/raft/docs/setup_en.md index 236bb8d8acf2..16e7883776c3 100644 --- a/worlds/raft/docs/setup_en.md +++ b/worlds/raft/docs/setup_en.md @@ -17,7 +17,7 @@ 4. Open RML and click Play. If you've already installed it, the executable that was used to install RML ("RMLLauncher.exe" unless renamed) should be used to run RML. Raft should start after clicking Play. -5. Open the RML menu. This should open automatically when Raft first loads. If it does not, and you see RML information in the top center of the Raft main menu, press F9 to open it. +5. Open the RML menu. This should open automatically when Raft first loads. If it does not, and you see RML information in the top center of the Raft main menu, press F9 to open it. If you do not see RML information at the top, close Raft+RML, go back to Step 4 and run RML as administrator. 6. Navigate to the "Mod manager" tab in the left-hand menu. diff --git a/worlds/rogue_legacy/test/TestUnique.py b/worlds/rogue_legacy/test/TestUnique.py index dbe35dd94fc0..1ae9968d5519 100644 --- a/worlds/rogue_legacy/test/TestUnique.py +++ b/worlds/rogue_legacy/test/TestUnique.py @@ -1,8 +1,8 @@ from typing import Dict from . import RLTestBase -from worlds.rogue_legacy.Items import RLItemData, item_table -from worlds.rogue_legacy.Locations import RLLocationData, location_table +from ..Items import item_table +from ..Locations import location_table class UniqueTest(RLTestBase): diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index e6696b782da2..bb325ba1da45 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -22,10 +22,9 @@ # CommonClient import first to trigger ModuleUpdater from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser from Utils import init_logging, is_windows, async_start -from worlds.sc2 import ItemNames -from worlds.sc2.ItemGroups import item_name_groups, unlisted_item_name_groups -from worlds.sc2 import Options -from worlds.sc2.Options import ( +from . import ItemNames, Options +from .ItemGroups import item_name_groups +from .Options import ( MissionOrder, KerriganPrimalStatus, kerrigan_unit_available, KerriganPresence, GameSpeed, GenericUpgradeItems, GenericUpgradeResearch, ColorChoice, GenericUpgradeMissions, LocationInclusion, ExtraLocations, MasteryLocations, ChallengeLocations, VanillaLocations, @@ -46,11 +45,12 @@ from worlds._sc2common.bot.data import Race from worlds._sc2common.bot.main import run_game from worlds._sc2common.bot.player import Bot -from worlds.sc2.Items import lookup_id_to_name, get_full_item_list, ItemData, type_flaggroups, upgrade_numbers, upgrade_numbers_all -from worlds.sc2.Locations import SC2WOL_LOC_ID_OFFSET, LocationType, SC2HOTS_LOC_ID_OFFSET -from worlds.sc2.MissionTables import lookup_id_to_mission, SC2Campaign, lookup_name_to_mission, \ - lookup_id_to_campaign, MissionConnection, SC2Mission, campaign_mission_table, SC2Race, get_no_build_missions -from worlds.sc2.Regions import MissionInfo +from .Items import (lookup_id_to_name, get_full_item_list, ItemData, type_flaggroups, upgrade_numbers, + upgrade_numbers_all) +from .Locations import SC2WOL_LOC_ID_OFFSET, LocationType, SC2HOTS_LOC_ID_OFFSET +from .MissionTables import (lookup_id_to_mission, SC2Campaign, lookup_name_to_mission, + lookup_id_to_campaign, MissionConnection, SC2Mission, campaign_mission_table, SC2Race) +from .Regions import MissionInfo import colorama from Options import Option @@ -243,10 +243,10 @@ def print_faction_title(): self.formatted_print(f" [u]{faction.name}[/u] ") for item_id in categorized_items[faction]: - item_name = self.ctx.item_names.lookup_in_slot(item_id) + item_name = self.ctx.item_names.lookup_in_game(item_id) received_child_items = items_received_set.intersection(parent_to_child.get(item_id, [])) matching_children = [child for child in received_child_items - if item_matches_filter(self.ctx.item_names.lookup_in_slot(child))] + if item_matches_filter(self.ctx.item_names.lookup_in_game(child))] received_items_of_this_type = items_received.get(item_id, []) item_is_match = item_matches_filter(item_name) if item_is_match or len(matching_children) > 0: @@ -1164,7 +1164,7 @@ def request_unfinished_missions(ctx: SC2Context) -> None: objectives = set(ctx.locations_for_mission(mission)) if objectives: remaining_objectives = objectives.difference(ctx.checked_locations) - unfinished_locations[mission] = [ctx.location_names.lookup_in_slot(location_id) for location_id in remaining_objectives] + unfinished_locations[mission] = [ctx.location_names.lookup_in_game(location_id) for location_id in remaining_objectives] else: unfinished_locations[mission] = [] diff --git a/worlds/sc2/ClientGui.py b/worlds/sc2/ClientGui.py index f9dcfc18eb4a..22e444efe7c9 100644 --- a/worlds/sc2/ClientGui.py +++ b/worlds/sc2/ClientGui.py @@ -13,12 +13,12 @@ from kivy.uix.scrollview import ScrollView from kivy.properties import StringProperty -from worlds.sc2.Client import SC2Context, calc_unfinished_missions, parse_unlock -from worlds.sc2.MissionTables import lookup_id_to_mission, lookup_name_to_mission, campaign_race_exceptions, \ - SC2Mission, SC2Race, SC2Campaign -from worlds.sc2.Locations import LocationType, lookup_location_id_to_type -from worlds.sc2.Options import LocationInclusion -from worlds.sc2 import SC2World, get_first_mission +from .Client import SC2Context, calc_unfinished_missions, parse_unlock +from .MissionTables import (lookup_id_to_mission, lookup_name_to_mission, campaign_race_exceptions, SC2Mission, SC2Race, + SC2Campaign) +from .Locations import LocationType, lookup_location_id_to_type +from .Options import LocationInclusion +from . import SC2World, get_first_mission class HoverableButton(HoverBehavior, Button): @@ -269,7 +269,7 @@ def sort_unfinished_locations(self, mission_name: str) -> Tuple[Dict[LocationTyp for loc in self.ctx.locations_for_mission(mission_name): if loc in self.ctx.missing_locations: count += 1 - locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names.lookup_in_slot(loc)) + locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names.lookup_in_game(loc)) plando_locations = [] for plando_loc in self.ctx.plando_locations: diff --git a/worlds/sc2/ItemGroups.py b/worlds/sc2/ItemGroups.py index a77fb920f64d..3a3733044579 100644 --- a/worlds/sc2/ItemGroups.py +++ b/worlds/sc2/ItemGroups.py @@ -51,7 +51,7 @@ item_name_groups.setdefault(data.type, []).append(item) # Numbered flaggroups get sorted into an unnumbered group # Currently supports numbers of one or two digits - if data.type[-2:].strip().isnumeric: + if data.type[-2:].strip().isnumeric(): type_group = data.type[:-2].strip() item_name_groups.setdefault(type_group, []).append(item) # Flaggroups with numbers are unlisted diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py index b1abb718c275..3dc4f51c47a2 100644 --- a/worlds/shivers/Rules.py +++ b/worlds/shivers/Rules.py @@ -1,4 +1,4 @@ -from typing import Dict, List, TYPE_CHECKING +from typing import Dict, TYPE_CHECKING from collections.abc import Callable from BaseClasses import CollectionState from worlds.generic.Rules import forbid_item @@ -78,7 +78,7 @@ def all_skull_dials_available(state: CollectionState, player: int) -> bool: def get_rules_lookup(player: int): - rules_lookup: Dict[str, List[Callable[[CollectionState], bool]]] = { + rules_lookup: Dict[str, Dict[str, Callable[[CollectionState], bool]]] = { "entrances": { "To Office Elevator From Underground Blue Tunnels": lambda state: state.has("Key for Office Elevator", player), "To Office Elevator From Office": lambda state: state.has("Key for Office Elevator", player), @@ -195,6 +195,15 @@ def set_rules(world: "ShiversWorld") -> None: for location_name, rule in rules_lookup["lightning"].items(): multiworld.get_location(location_name, player).access_rule = rule + # Register indirect conditions + multiworld.register_indirect_condition(world.get_region("Burial"), world.get_entrance("To Slide Room")) + multiworld.register_indirect_condition(world.get_region("Egypt"), world.get_entrance("To Slide Room")) + multiworld.register_indirect_condition(world.get_region("Gods Room"), world.get_entrance("To Slide Room")) + multiworld.register_indirect_condition(world.get_region("Prehistoric"), world.get_entrance("To Slide Room")) + multiworld.register_indirect_condition(world.get_region("Tar River"), world.get_entrance("To Slide Room")) + multiworld.register_indirect_condition(world.get_region("Werewolf"), world.get_entrance("To Slide Room")) + multiworld.register_indirect_condition(world.get_region("Prehistoric"), world.get_entrance("To Tar River From Lobby")) + # forbid cloth in janitor closet and oil in tar river forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Bottom DUPE", player) forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Top DUPE", player) @@ -226,7 +235,3 @@ def set_rules(world: "ShiversWorld") -> None: # Set completion condition multiworld.completion_condition[player] = lambda state: (first_nine_ixupi_capturable(state, player) and lightning_capturable(state, player)) - - - - diff --git a/worlds/sm/Client.py b/worlds/sm/Client.py index 6d6dd08ba5df..b9c13433aeb7 100644 --- a/worlds/sm/Client.py +++ b/worlds/sm/Client.py @@ -123,7 +123,7 @@ async def game_watcher(self, ctx): location_id = locations_start_id + item_index ctx.locations_checked.add(location_id) - location = ctx.location_names.lookup_in_slot(location_id) + location = ctx.location_names.lookup_in_game(location_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) @@ -151,7 +151,7 @@ async def game_watcher(self, ctx): snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), + color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), ctx.location_names.lookup_in_slot(item.location, item.player), item_out_ptr, len(ctx.items_received))) diff --git a/worlds/smw/Client.py b/worlds/smw/Client.py index 85bb3fe1ee16..600e1bff8304 100644 --- a/worlds/smw/Client.py +++ b/worlds/smw/Client.py @@ -223,7 +223,7 @@ async def handle_trap_queue(self, ctx): next_trap, message = self.trap_queue.pop(0) - from worlds.smw.Rom import trap_rom_data + from .Rom import trap_rom_data if next_trap.item in trap_rom_data: trap_active = await snes_read(ctx, WRAM_START + trap_rom_data[next_trap.item][0], 0x3) @@ -349,8 +349,8 @@ async def game_watcher(self, ctx): blocksanity_flags = bytearray(await snes_read(ctx, SMW_BLOCKSANITY_FLAGS, 0xC)) blocksanity_active = await snes_read(ctx, SMW_BLOCKSANITY_ACTIVE_ADDR, 0x1) level_clear_flags = bytearray(await snes_read(ctx, SMW_LEVEL_CLEAR_FLAGS, 0x60)) - from worlds.smw.Rom import item_rom_data, ability_rom_data, trap_rom_data, icon_rom_data - from worlds.smw.Levels import location_id_to_level_id, level_info_dict, level_blocks_data + from .Rom import item_rom_data, ability_rom_data, trap_rom_data, icon_rom_data + from .Levels import location_id_to_level_id, level_info_dict, level_blocks_data from worlds import AutoWorldRegister for loc_name, level_data in location_id_to_level_id.items(): loc_id = AutoWorldRegister.world_types[ctx.game].location_name_to_id[loc_name] @@ -448,7 +448,7 @@ async def game_watcher(self, ctx): for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names.lookup_in_slot(new_check_id) + location = ctx.location_names.lookup_in_game(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) @@ -501,14 +501,14 @@ async def game_watcher(self, ctx): recv_index += 1 sending_game = ctx.slot_info[item.player].game logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), + color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) if self.should_show_message(ctx, item): if item.item != 0xBC0012 and item.item not in trap_rom_data: # Don't send messages for Boss Tokens - item_name = ctx.item_names.lookup_in_slot(item.item) + item_name = ctx.item_names.lookup_in_game(item.item) player_name = ctx.player_names[item.player] receive_message = generate_received_text(item_name, player_name) @@ -516,7 +516,7 @@ async def game_watcher(self, ctx): snes_buffered_write(ctx, SMW_RECV_PROGRESS_ADDR, bytes([recv_index&0xFF, (recv_index>>8)&0xFF])) if item.item in trap_rom_data: - item_name = ctx.item_names.lookup_in_slot(item.item) + item_name = ctx.item_names.lookup_in_game(item.item) player_name = ctx.player_names[item.player] receive_message = generate_received_text(item_name, player_name) @@ -597,7 +597,7 @@ async def game_watcher(self, ctx): for loc_id in ctx.checked_locations: if loc_id not in ctx.locations_checked: ctx.locations_checked.add(loc_id) - loc_name = ctx.location_names.lookup_in_slot(loc_id) + loc_name = ctx.location_names.lookup_in_game(loc_id) if loc_name not in location_id_to_level_id: continue diff --git a/worlds/smz3/Client.py b/worlds/smz3/Client.py index 3c90ead0064c..028c44869a68 100644 --- a/worlds/smz3/Client.py +++ b/worlds/smz3/Client.py @@ -109,7 +109,7 @@ async def game_watcher(self, ctx): location_id = locations_start_id + convertLocSMZ3IDToAPID(item_index) ctx.locations_checked.add(location_id) - location = ctx.location_names.lookup_in_slot(location_id) + location = ctx.location_names.lookup_in_game(location_id) snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) @@ -132,7 +132,7 @@ async def game_watcher(self, ctx): item_out_ptr += 1 snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + recv_progress_addr_table_offset, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), + color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), ctx.location_names.lookup_in_slot(item.location, item.player), item_out_ptr, len(ctx.items_received))) await snes_flush_writes(ctx) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 61c866631690..07235ad2983a 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -1,12 +1,13 @@ import logging from typing import Dict, Any, Iterable, Optional, Union, List, TextIO -from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld +from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState from Options import PerGameCommonOptions from worlds.AutoWorld import World, WebWorld from . import rules from .bundles.bundle_room import BundleRoom from .bundles.bundles import get_all_bundles +from .content import content_packs, StardewContent, unpack_content, create_content from .early_items import setup_early_items from .items import item_table, create_items, ItemData, Group, items_by_group, get_all_filler_items, remove_limited_amount_packs from .locations import location_table, create_locations, LocationData, locations_by_tag @@ -14,16 +15,17 @@ from .logic.logic import StardewLogic from .logic.time_logic import MAX_MONTHS from .option_groups import sv_option_groups -from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, NumberOfLuckBuffs, NumberOfMovementBuffs, \ - BackpackProgression, BuildingProgression, ExcludeGingerIsland, TrapItems, EntranceRandomization +from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, EnabledFillerBuffs, NumberOfMovementBuffs, \ + BackpackProgression, BuildingProgression, ExcludeGingerIsland, TrapItems, EntranceRandomization, FarmType, Walnutsanity from .presets import sv_options_presets from .regions import create_regions from .rules import set_rules -from .stardew_rule import True_, StardewRule, HasProgressionPercent +from .stardew_rule import True_, StardewRule, HasProgressionPercent, true_ from .strings.ap_names.event_names import Event from .strings.entrance_names import Entrance as EntranceName from .strings.goal_names import Goal as GoalName -from .strings.region_names import Region as RegionName +from .strings.metal_names import Ore +from .strings.region_names import Region as RegionName, LogicRegion client_version = 0 @@ -77,6 +79,7 @@ class StardewValleyWorld(World): options_dataclass = StardewValleyOptions options: StardewValleyOptions + content: StardewContent logic: StardewLogic web = StardewWebWorld() @@ -94,6 +97,7 @@ def __init__(self, multiworld: MultiWorld, player: int): def generate_early(self): self.force_change_options_if_incompatible() + self.content = create_content(self.options) def force_change_options_if_incompatible(self): goal_is_walnut_hunter = self.options.goal == Goal.option_greatest_walnut_hunter @@ -106,6 +110,11 @@ def force_change_options_if_incompatible(self): player_name = self.multiworld.player_name[self.player] logging.warning( f"Goal '{goal_name}' requires Ginger Island. Exclude Ginger Island setting forced to 'False' for player {self.player} ({player_name})") + if exclude_ginger_island and self.options.walnutsanity != Walnutsanity.preset_none: + self.options.walnutsanity.value = Walnutsanity.preset_none + player_name = self.multiworld.player_name[self.player] + logging.warning( + f"Walnutsanity requires Ginger Island. Ginger Island was excluded from {self.player} ({player_name})'s world, so walnutsanity was force disabled") def create_regions(self): def create_region(name: str, exits: Iterable[str]) -> Region: @@ -115,9 +124,10 @@ def create_region(name: str, exits: Iterable[str]) -> Region: world_regions, world_entrances, self.randomized_entrances = create_regions(create_region, self.random, self.options) - self.logic = StardewLogic(self.player, self.options, world_regions.keys()) + self.logic = StardewLogic(self.player, self.options, self.content, world_regions.keys()) self.modified_bundles = get_all_bundles(self.random, self.logic, + self.content, self.options) def add_location(name: str, code: Optional[int], region: str): @@ -125,11 +135,12 @@ def add_location(name: str, code: Optional[int], region: str): location = StardewLocation(self.player, name, code, region) region.locations.append(location) - create_locations(add_location, self.modified_bundles, self.options, self.random) + create_locations(add_location, self.modified_bundles, self.options, self.content, self.random) self.multiworld.regions.extend(world_regions.values()) def create_items(self): self.precollect_starting_season() + self.precollect_farm_type_items() items_to_exclude = [excluded_items for excluded_items in self.multiworld.precollected_items[self.player] if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK, @@ -143,7 +154,7 @@ def create_items(self): for location in self.multiworld.get_locations(self.player) if location.address is not None]) - created_items = create_items(self.create_item, self.delete_item, locations_count, items_to_exclude, self.options, + created_items = create_items(self.create_item, self.delete_item, locations_count, items_to_exclude, self.options, self.content, self.random) self.multiworld.itempool += created_items @@ -173,10 +184,15 @@ def precollect_starting_season(self): starting_season = self.create_starting_item(self.random.choice(season_pool)) self.multiworld.push_precollected(starting_season) + def precollect_farm_type_items(self): + if self.options.farm_type == FarmType.option_meadowlands and self.options.building_progression & BuildingProgression.option_progressive: + self.multiworld.push_precollected(self.create_starting_item("Progressive Coop")) + def setup_player_events(self): self.setup_construction_events() self.setup_quest_events() self.setup_action_events() + self.setup_logic_events() def setup_construction_events(self): can_construct_buildings = LocationData(None, RegionName.carpenter, Event.can_construct_buildings) @@ -187,10 +203,26 @@ def setup_quest_events(self): self.create_event_location(start_dark_talisman_quest, self.logic.wallet.has_rusty_key(), Event.start_dark_talisman_quest) def setup_action_events(self): - can_ship_event = LocationData(None, RegionName.shipping, Event.can_ship_items) - self.create_event_location(can_ship_event, True_(), Event.can_ship_items) + can_ship_event = LocationData(None, LogicRegion.shipping, Event.can_ship_items) + self.create_event_location(can_ship_event, true_, Event.can_ship_items) can_shop_pierre_event = LocationData(None, RegionName.pierre_store, Event.can_shop_at_pierre) - self.create_event_location(can_shop_pierre_event, True_(), Event.can_shop_at_pierre) + self.create_event_location(can_shop_pierre_event, true_, Event.can_shop_at_pierre) + + spring_farming = LocationData(None, LogicRegion.spring_farming, Event.spring_farming) + self.create_event_location(spring_farming, true_, Event.spring_farming) + summer_farming = LocationData(None, LogicRegion.summer_farming, Event.summer_farming) + self.create_event_location(summer_farming, true_, Event.summer_farming) + fall_farming = LocationData(None, LogicRegion.fall_farming, Event.fall_farming) + self.create_event_location(fall_farming, true_, Event.fall_farming) + winter_farming = LocationData(None, LogicRegion.winter_farming, Event.winter_farming) + self.create_event_location(winter_farming, true_, Event.winter_farming) + + def setup_logic_events(self): + def register_event(name: str, region: str, rule: StardewRule): + event_location = LocationData(None, region, name) + self.create_event_location(event_location, rule, name) + + self.logic.setup_events(register_event) def setup_victory(self): if self.options.goal == Goal.option_community_center: @@ -211,7 +243,7 @@ def setup_victory(self): Event.victory) elif self.options.goal == Goal.option_master_angler: self.create_event_location(location_table[GoalName.master_angler], - self.logic.fishing.can_catch_every_fish_in_slot(self.get_all_location_names()), + self.logic.fishing.can_catch_every_fish_for_fishsanity(), Event.victory) elif self.options.goal == Goal.option_complete_collection: self.create_event_location(location_table[GoalName.complete_museum], @@ -270,18 +302,13 @@ def create_item(self, item: Union[str, ItemData], override_classification: ItemC if override_classification is None: override_classification = item.classification - if override_classification == ItemClassification.progression and item.name != Event.victory: + if override_classification == ItemClassification.progression: self.total_progression_items += 1 - # if item.name not in self.all_progression_items: - # self.all_progression_items[item.name] = 0 - # self.all_progression_items[item.name] += 1 return StardewItem(item.name, override_classification, item.code, self.player) def delete_item(self, item: Item): if item.classification & ItemClassification.progression: self.total_progression_items -= 1 - # if item.name in self.all_progression_items: - # self.all_progression_items[item.name] -= 1 def create_starting_item(self, item: Union[str, ItemData]) -> StardewItem: if isinstance(item, str): @@ -299,7 +326,11 @@ def create_event_location(self, location_data: LocationData, rule: StardewRule = location = StardewLocation(self.player, location_data.name, None, region) location.access_rule = rule region.locations.append(location) - location.place_locked_item(self.create_item(item)) + location.place_locked_item(StardewItem(item, ItemClassification.progression, None, self.player)) + + # This is not ideal, but the rule count them so... + if item != Event.victory: + self.total_progression_items += 1 def set_rules(self): set_rules(self) @@ -358,7 +389,7 @@ def add_bundles_to_spoiler_log(self, spoiler_handle: TextIO): quality = "" else: quality = f" ({item.quality.split(' ')[0]})" - spoiler_handle.write(f"\t\t{item.amount}x {item.item_name}{quality}\n") + spoiler_handle.write(f"\t\t{item.amount}x {item.get_item()}{quality}\n") def add_entrances_to_spoiler_log(self): if self.options.entrance_randomization == EntranceRandomization.option_disabled: @@ -373,9 +404,9 @@ def fill_slot_data(self) -> Dict[str, Any]: for bundle in room.bundles: bundles[room.name][bundle.name] = {"number_required": bundle.number_required} for i, item in enumerate(bundle.items): - bundles[room.name][bundle.name][i] = f"{item.item_name}|{item.amount}|{item.quality}" + bundles[room.name][bundle.name][i] = f"{item.get_item()}|{item.amount}|{item.quality}" - excluded_options = [BundleRandomization, NumberOfMovementBuffs, NumberOfLuckBuffs] + excluded_options = [BundleRandomization, NumberOfMovementBuffs, EnabledFillerBuffs] excluded_option_names = [option.internal_name for option in excluded_options] generic_option_names = [option_name for option_name in PerGameCommonOptions.type_hints] excluded_option_names.extend(generic_option_names) @@ -385,7 +416,29 @@ def fill_slot_data(self) -> Dict[str, Any]: "seed": self.random.randrange(1000000000), # Seed should be max 9 digits "randomized_entrances": self.randomized_entrances, "modified_bundles": bundles, - "client_version": "5.0.0", + "client_version": "6.0.0", }) return slot_data + + def collect(self, state: CollectionState, item: StardewItem) -> bool: + change = super().collect(state, item) + if change: + state.prog_items[self.player][Event.received_walnuts] += self.get_walnut_amount(item.name) + return change + + def remove(self, state: CollectionState, item: StardewItem) -> bool: + change = super().remove(state, item) + if change: + state.prog_items[self.player][Event.received_walnuts] -= self.get_walnut_amount(item.name) + return change + + @staticmethod + def get_walnut_amount(item_name: str) -> int: + if item_name == "Golden Walnut": + return 1 + if item_name == "3 Golden Walnuts": + return 3 + if item_name == "5 Golden Walnuts": + return 5 + return 0 diff --git a/worlds/stardew_valley/bundles/bundle.py b/worlds/stardew_valley/bundles/bundle.py index 199826b96bc8..43afc750b87a 100644 --- a/worlds/stardew_valley/bundles/bundle.py +++ b/worlds/stardew_valley/bundles/bundle.py @@ -1,8 +1,10 @@ +import math from dataclasses import dataclass from random import Random -from typing import List +from typing import List, Tuple from .bundle_item import BundleItem +from ..content import StardewContent from ..options import BundlePrice, StardewValleyOptions, ExcludeGingerIsland, FestivalLocations from ..strings.currency_names import Currency @@ -26,7 +28,8 @@ class BundleTemplate: number_possible_items: int number_required_items: int - def __init__(self, room: str, name: str, items: List[BundleItem], number_possible_items: int, number_required_items: int): + def __init__(self, room: str, name: str, items: List[BundleItem], number_possible_items: int, + number_required_items: int): self.room = room self.name = name self.items = items @@ -35,17 +38,12 @@ def __init__(self, room: str, name: str, items: List[BundleItem], number_possibl @staticmethod def extend_from(template, items: List[BundleItem]): - return BundleTemplate(template.room, template.name, items, template.number_possible_items, template.number_required_items) + return BundleTemplate(template.room, template.name, items, template.number_possible_items, + template.number_required_items) - def create_bundle(self, bundle_price_option: BundlePrice, random: Random, options: StardewValleyOptions) -> Bundle: - if bundle_price_option == BundlePrice.option_minimum: - number_required = 1 - elif bundle_price_option == BundlePrice.option_maximum: - number_required = 8 - else: - number_required = self.number_required_items + bundle_price_option.value - number_required = max(1, number_required) - filtered_items = [item for item in self.items if item.can_appear(options)] + def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle: + number_required, price_multiplier = get_bundle_final_prices(options.bundle_price, self.number_required_items, False) + filtered_items = [item for item in self.items if item.can_appear(content, options)] number_items = len(filtered_items) number_chosen_items = self.number_possible_items if number_chosen_items < number_required: @@ -55,6 +53,7 @@ def create_bundle(self, bundle_price_option: BundlePrice, random: Random, option chosen_items = filtered_items + random.choices(filtered_items, k=number_chosen_items - number_items) else: chosen_items = random.sample(filtered_items, number_chosen_items) + chosen_items = [item.as_amount(max(1, math.floor(item.amount * price_multiplier))) for item in chosen_items] return Bundle(self.room, self.name, chosen_items, number_required) def can_appear(self, options: StardewValleyOptions) -> bool: @@ -68,19 +67,13 @@ def __init__(self, room: str, name: str, item: BundleItem): super().__init__(room, name, [item], 1, 1) self.item = item - def create_bundle(self, bundle_price_option: BundlePrice, random: Random, options: StardewValleyOptions) -> Bundle: - currency_amount = self.get_currency_amount(bundle_price_option) + def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle: + currency_amount = self.get_currency_amount(options.bundle_price) return Bundle(self.room, self.name, [BundleItem(self.item.item_name, currency_amount)], 1) def get_currency_amount(self, bundle_price_option: BundlePrice): - if bundle_price_option == BundlePrice.option_minimum: - price_multiplier = 0.1 - elif bundle_price_option == BundlePrice.option_maximum: - price_multiplier = 4 - else: - price_multiplier = round(1 + (bundle_price_option.value * 0.4), 2) - - currency_amount = int(self.item.amount * price_multiplier) + _, price_multiplier = get_bundle_final_prices(bundle_price_option, self.number_required_items, True) + currency_amount = max(1, int(self.item.amount * price_multiplier)) return currency_amount def can_appear(self, options: StardewValleyOptions) -> bool: @@ -95,11 +88,11 @@ def can_appear(self, options: StardewValleyOptions) -> bool: class MoneyBundleTemplate(CurrencyBundleTemplate): - def __init__(self, room: str, item: BundleItem): - super().__init__(room, "", item) + def __init__(self, room: str, default_name: str, item: BundleItem): + super().__init__(room, default_name, item) - def create_bundle(self, bundle_price_option: BundlePrice, random: Random, options: StardewValleyOptions) -> Bundle: - currency_amount = self.get_currency_amount(bundle_price_option) + def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle: + currency_amount = self.get_currency_amount(options.bundle_price) currency_name = "g" if currency_amount >= 1000: unit_amount = currency_amount % 1000 @@ -111,13 +104,8 @@ def create_bundle(self, bundle_price_option: BundlePrice, random: Random, option return Bundle(self.room, name, [BundleItem(self.item.item_name, currency_amount)], 1) def get_currency_amount(self, bundle_price_option: BundlePrice): - if bundle_price_option == BundlePrice.option_minimum: - price_multiplier = 0.1 - elif bundle_price_option == BundlePrice.option_maximum: - price_multiplier = 4 - else: - price_multiplier = round(1 + (bundle_price_option.value * 0.4), 2) - currency_amount = int(self.item.amount * price_multiplier) + _, price_multiplier = get_bundle_final_prices(bundle_price_option, self.number_required_items, True) + currency_amount = max(1, int(self.item.amount * price_multiplier)) return currency_amount @@ -134,30 +122,54 @@ def can_appear(self, options: StardewValleyOptions) -> bool: class DeepBundleTemplate(BundleTemplate): categories: List[List[BundleItem]] - def __init__(self, room: str, name: str, categories: List[List[BundleItem]], number_possible_items: int, number_required_items: int): + def __init__(self, room: str, name: str, categories: List[List[BundleItem]], number_possible_items: int, + number_required_items: int): super().__init__(room, name, [], number_possible_items, number_required_items) self.categories = categories - def create_bundle(self, bundle_price_option: BundlePrice, random: Random, options: StardewValleyOptions) -> Bundle: - if bundle_price_option == BundlePrice.option_minimum: - number_required = 1 - elif bundle_price_option == BundlePrice.option_maximum: - number_required = 8 - else: - number_required = self.number_required_items + bundle_price_option.value + def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle: + number_required, price_multiplier = get_bundle_final_prices(options.bundle_price, self.number_required_items, False) number_categories = len(self.categories) number_chosen_categories = self.number_possible_items if number_chosen_categories < number_required: number_chosen_categories = number_required if number_chosen_categories > number_categories: - chosen_categories = self.categories + random.choices(self.categories, k=number_chosen_categories - number_categories) + chosen_categories = self.categories + random.choices(self.categories, + k=number_chosen_categories - number_categories) else: chosen_categories = random.sample(self.categories, number_chosen_categories) chosen_items = [] for category in chosen_categories: - filtered_items = [item for item in category if item.can_appear(options)] + filtered_items = [item for item in category if item.can_appear(content, options)] chosen_items.append(random.choice(filtered_items)) + chosen_items = [item.as_amount(max(1, math.floor(item.amount * price_multiplier))) for item in chosen_items] return Bundle(self.room, self.name, chosen_items, number_required) + + +def get_bundle_final_prices(bundle_price_option: BundlePrice, default_required_items: int, is_currency: bool) -> Tuple[int, float]: + number_required_items = get_number_required_items(bundle_price_option, default_required_items) + price_multiplier = get_price_multiplier(bundle_price_option, is_currency) + return number_required_items, price_multiplier + + +def get_number_required_items(bundle_price_option: BundlePrice, default_required_items: int) -> int: + if bundle_price_option == BundlePrice.option_minimum: + return 1 + if bundle_price_option == BundlePrice.option_maximum: + return 8 + number_required = default_required_items + bundle_price_option.value + return min(8, max(1, number_required)) + + +def get_price_multiplier(bundle_price_option: BundlePrice, is_currency: bool) -> float: + if bundle_price_option == BundlePrice.option_minimum: + return 0.1 if is_currency else 0.2 + if bundle_price_option == BundlePrice.option_maximum: + return 4 if is_currency else 1.4 + price_factor = 0.4 if is_currency else (0.2 if bundle_price_option.value <= 0 else 0.1) + price_multiplier_difference = bundle_price_option.value * price_factor + price_multiplier = 1 + price_multiplier_difference + return round(price_multiplier, 2) diff --git a/worlds/stardew_valley/bundles/bundle_item.py b/worlds/stardew_valley/bundles/bundle_item.py index 8aaa67c5f242..7dc9c0e1a3b5 100644 --- a/worlds/stardew_valley/bundles/bundle_item.py +++ b/worlds/stardew_valley/bundles/bundle_item.py @@ -3,7 +3,8 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from ..options import StardewValleyOptions, ExcludeGingerIsland, FestivalLocations +from ..content import StardewContent +from ..options import StardewValleyOptions, ExcludeGingerIsland, FestivalLocations, SkillProgression from ..strings.crop_names import Fruit from ..strings.currency_names import Currency from ..strings.quality_names import CropQuality, FishQuality, ForageQuality @@ -30,27 +31,50 @@ def can_appear(self, options: StardewValleyOptions) -> bool: return options.festival_locations != FestivalLocations.option_disabled +class MasteryItemSource(BundleItemSource): + def can_appear(self, options: StardewValleyOptions) -> bool: + return options.skill_progression == SkillProgression.option_progressive_with_masteries + + +class ContentItemSource(BundleItemSource): + """This is meant to be used for items that are managed by the content packs.""" + + def can_appear(self, options: StardewValleyOptions) -> bool: + raise ValueError("This should not be called, check if the item is in the content instead.") + + @dataclass(frozen=True, order=True) class BundleItem: class Sources: vanilla = VanillaItemSource() island = IslandItemSource() festival = FestivalItemSource() + masteries = MasteryItemSource() + content = ContentItemSource() item_name: str amount: int = 1 quality: str = CropQuality.basic source: BundleItemSource = Sources.vanilla + flavor: str = None + can_have_quality: bool = True @staticmethod def money_bundle(amount: int) -> BundleItem: return BundleItem(Currency.money, amount) + def get_item(self) -> str: + if self.flavor is None: + return self.item_name + return f"{self.item_name} [{self.flavor}]" + def as_amount(self, amount: int) -> BundleItem: - return BundleItem(self.item_name, amount, self.quality, self.source) + return BundleItem(self.item_name, amount, self.quality, self.source, self.flavor) def as_quality(self, quality: str) -> BundleItem: - return BundleItem(self.item_name, self.amount, quality, self.source) + if self.can_have_quality: + return BundleItem(self.item_name, self.amount, quality, self.source, self.flavor) + return BundleItem(self.item_name, self.amount, self.quality, self.source, self.flavor) def as_quality_crop(self) -> BundleItem: amount = 5 @@ -67,7 +91,11 @@ def as_quality_forage(self) -> BundleItem: def __repr__(self): quality = "" if self.quality == CropQuality.basic else self.quality - return f"{self.amount} {quality} {self.item_name}" + return f"{self.amount} {quality} {self.get_item()}" + + def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool: + if isinstance(self.source, ContentItemSource): + return self.get_item() in content.game_items - def can_appear(self, options: StardewValleyOptions) -> bool: return self.source.can_appear(options) + diff --git a/worlds/stardew_valley/bundles/bundle_room.py b/worlds/stardew_valley/bundles/bundle_room.py index a5cdb89144f5..8068ff17ac83 100644 --- a/worlds/stardew_valley/bundles/bundle_room.py +++ b/worlds/stardew_valley/bundles/bundle_room.py @@ -3,6 +3,7 @@ from typing import List from .bundle import Bundle, BundleTemplate +from ..content import StardewContent from ..options import BundlePrice, StardewValleyOptions @@ -18,7 +19,25 @@ class BundleRoomTemplate: bundles: List[BundleTemplate] number_bundles: int - def create_bundle_room(self, bundle_price_option: BundlePrice, random: Random, options: StardewValleyOptions): + def create_bundle_room(self, random: Random, content: StardewContent, options: StardewValleyOptions): filtered_bundles = [bundle for bundle in self.bundles if bundle.can_appear(options)] - chosen_bundles = random.sample(filtered_bundles, self.number_bundles) - return BundleRoom(self.name, [bundle.create_bundle(bundle_price_option, random, options) for bundle in chosen_bundles]) + + priority_bundles = [] + unpriority_bundles = [] + for bundle in filtered_bundles: + if bundle.name in options.bundle_plando: + priority_bundles.append(bundle) + else: + unpriority_bundles.append(bundle) + + if self.number_bundles <= len(priority_bundles): + chosen_bundles = random.sample(priority_bundles, self.number_bundles) + else: + chosen_bundles = priority_bundles + num_remaining_bundles = self.number_bundles - len(priority_bundles) + if num_remaining_bundles > len(unpriority_bundles): + chosen_bundles.extend(random.choices(unpriority_bundles, k=num_remaining_bundles)) + else: + chosen_bundles.extend(random.sample(unpriority_bundles, num_remaining_bundles)) + + return BundleRoom(self.name, [bundle.create_bundle(random, content, options) for bundle in chosen_bundles]) diff --git a/worlds/stardew_valley/bundles/bundles.py b/worlds/stardew_valley/bundles/bundles.py index 260ee17cbe82..99619e09aadf 100644 --- a/worlds/stardew_valley/bundles/bundles.py +++ b/worlds/stardew_valley/bundles/bundles.py @@ -1,65 +1,102 @@ from random import Random -from typing import List +from typing import List, Tuple -from .bundle_room import BundleRoom +from .bundle import Bundle +from .bundle_room import BundleRoom, BundleRoomTemplate +from ..content import StardewContent from ..data.bundle_data import pantry_vanilla, crafts_room_vanilla, fish_tank_vanilla, boiler_room_vanilla, bulletin_board_vanilla, vault_vanilla, \ pantry_thematic, crafts_room_thematic, fish_tank_thematic, boiler_room_thematic, bulletin_board_thematic, vault_thematic, pantry_remixed, \ crafts_room_remixed, fish_tank_remixed, boiler_room_remixed, bulletin_board_remixed, vault_remixed, all_bundle_items_except_money, \ - abandoned_joja_mart_thematic, abandoned_joja_mart_vanilla, abandoned_joja_mart_remixed + abandoned_joja_mart_thematic, abandoned_joja_mart_vanilla, abandoned_joja_mart_remixed, raccoon_vanilla, raccoon_thematic, raccoon_remixed, \ + community_center_remixed_anywhere from ..logic.logic import StardewLogic -from ..options import BundleRandomization, StardewValleyOptions, ExcludeGingerIsland +from ..options import BundleRandomization, StardewValleyOptions -def get_all_bundles(random: Random, logic: StardewLogic, options: StardewValleyOptions) -> List[BundleRoom]: +def get_all_bundles(random: Random, logic: StardewLogic, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: if options.bundle_randomization == BundleRandomization.option_vanilla: - return get_vanilla_bundles(random, options) + return get_vanilla_bundles(random, content, options) elif options.bundle_randomization == BundleRandomization.option_thematic: - return get_thematic_bundles(random, options) + return get_thematic_bundles(random, content, options) elif options.bundle_randomization == BundleRandomization.option_remixed: - return get_remixed_bundles(random, options) + return get_remixed_bundles(random, content, options) + elif options.bundle_randomization == BundleRandomization.option_remixed_anywhere: + return get_remixed_bundles_anywhere(random, content, options) elif options.bundle_randomization == BundleRandomization.option_shuffled: - return get_shuffled_bundles(random, logic, options) + return get_shuffled_bundles(random, logic, content, options) raise NotImplementedError -def get_vanilla_bundles(random: Random, options: StardewValleyOptions) -> List[BundleRoom]: - pantry = pantry_vanilla.create_bundle_room(options.bundle_price, random, options) - crafts_room = crafts_room_vanilla.create_bundle_room(options.bundle_price, random, options) - fish_tank = fish_tank_vanilla.create_bundle_room(options.bundle_price, random, options) - boiler_room = boiler_room_vanilla.create_bundle_room(options.bundle_price, random, options) - bulletin_board = bulletin_board_vanilla.create_bundle_room(options.bundle_price, random, options) - vault = vault_vanilla.create_bundle_room(options.bundle_price, random, options) - abandoned_joja_mart = abandoned_joja_mart_vanilla.create_bundle_room(options.bundle_price, random, options) - return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart] - - -def get_thematic_bundles(random: Random, options: StardewValleyOptions) -> List[BundleRoom]: - pantry = pantry_thematic.create_bundle_room(options.bundle_price, random, options) - crafts_room = crafts_room_thematic.create_bundle_room(options.bundle_price, random, options) - fish_tank = fish_tank_thematic.create_bundle_room(options.bundle_price, random, options) - boiler_room = boiler_room_thematic.create_bundle_room(options.bundle_price, random, options) - bulletin_board = bulletin_board_thematic.create_bundle_room(options.bundle_price, random, options) - vault = vault_thematic.create_bundle_room(options.bundle_price, random, options) - abandoned_joja_mart = abandoned_joja_mart_thematic.create_bundle_room(options.bundle_price, random, options) - return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart] - - -def get_remixed_bundles(random: Random, options: StardewValleyOptions) -> List[BundleRoom]: - pantry = pantry_remixed.create_bundle_room(options.bundle_price, random, options) - crafts_room = crafts_room_remixed.create_bundle_room(options.bundle_price, random, options) - fish_tank = fish_tank_remixed.create_bundle_room(options.bundle_price, random, options) - boiler_room = boiler_room_remixed.create_bundle_room(options.bundle_price, random, options) - bulletin_board = bulletin_board_remixed.create_bundle_room(options.bundle_price, random, options) - vault = vault_remixed.create_bundle_room(options.bundle_price, random, options) - abandoned_joja_mart = abandoned_joja_mart_remixed.create_bundle_room(options.bundle_price, random, options) - return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart] - - -def get_shuffled_bundles(random: Random, logic: StardewLogic, options: StardewValleyOptions) -> List[BundleRoom]: - valid_bundle_items = [bundle_item for bundle_item in all_bundle_items_except_money if bundle_item.can_appear(options)] - - rooms = [room for room in get_remixed_bundles(random, options) if room.name != "Vault"] +def get_vanilla_bundles(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: + pantry = pantry_vanilla.create_bundle_room(random, content, options) + crafts_room = crafts_room_vanilla.create_bundle_room(random, content, options) + fish_tank = fish_tank_vanilla.create_bundle_room(random, content, options) + boiler_room = boiler_room_vanilla.create_bundle_room(random, content, options) + bulletin_board = bulletin_board_vanilla.create_bundle_room(random, content, options) + vault = vault_vanilla.create_bundle_room(random, content, options) + abandoned_joja_mart = abandoned_joja_mart_vanilla.create_bundle_room(random, content, options) + raccoon = raccoon_vanilla.create_bundle_room(random, content, options) + fix_raccoon_bundle_names(raccoon) + return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon] + + +def get_thematic_bundles(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: + pantry = pantry_thematic.create_bundle_room(random, content, options) + crafts_room = crafts_room_thematic.create_bundle_room(random, content, options) + fish_tank = fish_tank_thematic.create_bundle_room(random, content, options) + boiler_room = boiler_room_thematic.create_bundle_room(random, content, options) + bulletin_board = bulletin_board_thematic.create_bundle_room(random, content, options) + vault = vault_thematic.create_bundle_room(random, content, options) + abandoned_joja_mart = abandoned_joja_mart_thematic.create_bundle_room(random, content, options) + raccoon = raccoon_thematic.create_bundle_room(random, content, options) + fix_raccoon_bundle_names(raccoon) + return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon] + + +def get_remixed_bundles(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: + pantry = pantry_remixed.create_bundle_room(random, content, options) + crafts_room = crafts_room_remixed.create_bundle_room(random, content, options) + fish_tank = fish_tank_remixed.create_bundle_room(random, content, options) + boiler_room = boiler_room_remixed.create_bundle_room(random, content, options) + bulletin_board = bulletin_board_remixed.create_bundle_room(random, content, options) + vault = vault_remixed.create_bundle_room(random, content, options) + abandoned_joja_mart = abandoned_joja_mart_remixed.create_bundle_room(random, content, options) + raccoon = raccoon_remixed.create_bundle_room(random, content, options) + fix_raccoon_bundle_names(raccoon) + return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon] + + +def get_remixed_bundles_anywhere(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: + big_room = community_center_remixed_anywhere.create_bundle_room(random, content, options) + all_chosen_bundles = big_room.bundles + random.shuffle(all_chosen_bundles) + + end_index = 0 + + pantry, end_index = create_room_from_bundles(pantry_remixed, all_chosen_bundles, end_index) + crafts_room, end_index = create_room_from_bundles(crafts_room_remixed, all_chosen_bundles, end_index) + fish_tank, end_index = create_room_from_bundles(fish_tank_remixed, all_chosen_bundles, end_index) + boiler_room, end_index = create_room_from_bundles(boiler_room_remixed, all_chosen_bundles, end_index) + bulletin_board, end_index = create_room_from_bundles(bulletin_board_remixed, all_chosen_bundles, end_index) + + vault = vault_remixed.create_bundle_room(random, content, options) + abandoned_joja_mart = abandoned_joja_mart_remixed.create_bundle_room(random, content, options) + raccoon = raccoon_remixed.create_bundle_room(random, content, options) + fix_raccoon_bundle_names(raccoon) + return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon] + + +def create_room_from_bundles(template: BundleRoomTemplate, all_bundles: List[Bundle], end_index: int) -> Tuple[BundleRoom, int]: + start_index = end_index + end_index += template.number_bundles + return BundleRoom(template.name, all_bundles[start_index:end_index]), end_index + + +def get_shuffled_bundles(random: Random, logic: StardewLogic, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: + valid_bundle_items = [bundle_item for bundle_item in all_bundle_items_except_money if bundle_item.can_appear(content, options)] + + rooms = [room for room in get_remixed_bundles(random, content, options) if room.name != "Vault"] required_items = 0 for room in rooms: for bundle in room.bundles: @@ -67,14 +104,21 @@ def get_shuffled_bundles(random: Random, logic: StardewLogic, options: StardewVa random.shuffle(room.bundles) random.shuffle(rooms) + # Remove duplicates of the same item + valid_bundle_items = [item1 for i, item1 in enumerate(valid_bundle_items) + if not any(item1.item_name == item2.item_name and item1.quality == item2.quality for item2 in valid_bundle_items[:i])] chosen_bundle_items = random.sample(valid_bundle_items, required_items) - sorted_bundle_items = sorted(chosen_bundle_items, key=lambda x: logic.has(x.item_name).get_difficulty()) for room in rooms: for bundle in room.bundles: num_items = len(bundle.items) - bundle.items = sorted_bundle_items[:num_items] - sorted_bundle_items = sorted_bundle_items[num_items:] + bundle.items = chosen_bundle_items[:num_items] + chosen_bundle_items = chosen_bundle_items[num_items:] - vault = vault_remixed.create_bundle_room(options.bundle_price, random, options) + vault = vault_remixed.create_bundle_room(random, content, options) return [*rooms, vault] + +def fix_raccoon_bundle_names(raccoon): + for i in range(len(raccoon.bundles)): + raccoon_bundle = raccoon.bundles[i] + raccoon_bundle.name = f"Raccoon Request {i + 1}" diff --git a/worlds/stardew_valley/content/__init__.py b/worlds/stardew_valley/content/__init__.py new file mode 100644 index 000000000000..9130873fa405 --- /dev/null +++ b/worlds/stardew_valley/content/__init__.py @@ -0,0 +1,107 @@ +from . import content_packs +from .feature import cropsanity, friendsanity, fishsanity, booksanity +from .game_content import ContentPack, StardewContent, StardewFeatures +from .unpacking import unpack_content +from .. import options + + +def create_content(player_options: options.StardewValleyOptions) -> StardewContent: + active_packs = choose_content_packs(player_options) + features = choose_features(player_options) + return unpack_content(features, active_packs) + + +def choose_content_packs(player_options: options.StardewValleyOptions): + active_packs = [content_packs.pelican_town, content_packs.the_desert, content_packs.the_farm, content_packs.the_mines] + + if player_options.exclude_ginger_island == options.ExcludeGingerIsland.option_false: + active_packs.append(content_packs.ginger_island_content_pack) + + if player_options.special_order_locations & options.SpecialOrderLocations.value_qi: + active_packs.append(content_packs.qi_board_content_pack) + + for mod in player_options.mods.value: + active_packs.append(content_packs.by_mod[mod]) + + return active_packs + + +def choose_features(player_options: options.StardewValleyOptions) -> StardewFeatures: + return StardewFeatures( + choose_booksanity(player_options.booksanity), + choose_cropsanity(player_options.cropsanity), + choose_fishsanity(player_options.fishsanity), + choose_friendsanity(player_options.friendsanity, player_options.friendsanity_heart_size) + ) + + +booksanity_by_option = { + options.Booksanity.option_none: booksanity.BooksanityDisabled(), + options.Booksanity.option_power: booksanity.BooksanityPower(), + options.Booksanity.option_power_skill: booksanity.BooksanityPowerSkill(), + options.Booksanity.option_all: booksanity.BooksanityAll(), +} + + +def choose_booksanity(booksanity_option: options.Booksanity) -> booksanity.BooksanityFeature: + booksanity_feature = booksanity_by_option.get(booksanity_option) + + if booksanity_feature is None: + raise ValueError(f"No booksanity feature mapped to {str(booksanity_option.value)}") + + return booksanity_feature + + +cropsanity_by_option = { + options.Cropsanity.option_disabled: cropsanity.CropsanityDisabled(), + options.Cropsanity.option_enabled: cropsanity.CropsanityEnabled(), +} + + +def choose_cropsanity(cropsanity_option: options.Cropsanity) -> cropsanity.CropsanityFeature: + cropsanity_feature = cropsanity_by_option.get(cropsanity_option) + + if cropsanity_feature is None: + raise ValueError(f"No cropsanity feature mapped to {str(cropsanity_option.value)}") + + return cropsanity_feature + + +fishsanity_by_option = { + options.Fishsanity.option_none: fishsanity.FishsanityNone(), + options.Fishsanity.option_legendaries: fishsanity.FishsanityLegendaries(), + options.Fishsanity.option_special: fishsanity.FishsanitySpecial(), + options.Fishsanity.option_randomized: fishsanity.FishsanityAll(randomization_ratio=0.4), + options.Fishsanity.option_all: fishsanity.FishsanityAll(), + options.Fishsanity.option_exclude_legendaries: fishsanity.FishsanityExcludeLegendaries(), + options.Fishsanity.option_exclude_hard_fish: fishsanity.FishsanityExcludeHardFish(), + options.Fishsanity.option_only_easy_fish: fishsanity.FishsanityOnlyEasyFish(), +} + + +def choose_fishsanity(fishsanity_option: options.Fishsanity) -> fishsanity.FishsanityFeature: + fishsanity_feature = fishsanity_by_option.get(fishsanity_option) + + if fishsanity_feature is None: + raise ValueError(f"No fishsanity feature mapped to {str(fishsanity_option.value)}") + + return fishsanity_feature + + +def choose_friendsanity(friendsanity_option: options.Friendsanity, heart_size: options.FriendsanityHeartSize) -> friendsanity.FriendsanityFeature: + if friendsanity_option == options.Friendsanity.option_none: + return friendsanity.FriendsanityNone() + + if friendsanity_option == options.Friendsanity.option_bachelors: + return friendsanity.FriendsanityBachelors(heart_size.value) + + if friendsanity_option == options.Friendsanity.option_starting_npcs: + return friendsanity.FriendsanityStartingNpc(heart_size.value) + + if friendsanity_option == options.Friendsanity.option_all: + return friendsanity.FriendsanityAll(heart_size.value) + + if friendsanity_option == options.Friendsanity.option_all_with_marriage: + return friendsanity.FriendsanityAllWithMarriage(heart_size.value) + + raise ValueError(f"No friendsanity feature mapped to {str(friendsanity_option.value)}") diff --git a/worlds/stardew_valley/content/content_packs.py b/worlds/stardew_valley/content/content_packs.py new file mode 100644 index 000000000000..fb8df8c70cba --- /dev/null +++ b/worlds/stardew_valley/content/content_packs.py @@ -0,0 +1,31 @@ +import importlib +import pkgutil + +from . import mods +from .mod_registry import by_mod +from .vanilla.base import base_game +from .vanilla.ginger_island import ginger_island_content_pack +from .vanilla.pelican_town import pelican_town +from .vanilla.qi_board import qi_board_content_pack +from .vanilla.the_desert import the_desert +from .vanilla.the_farm import the_farm +from .vanilla.the_mines import the_mines + +assert base_game +assert ginger_island_content_pack +assert pelican_town +assert qi_board_content_pack +assert the_desert +assert the_farm +assert the_mines + +# Dynamically register everything currently in the mods folder. This would ideally be done through a metaclass, but I have not looked into that yet. +mod_modules = pkgutil.iter_modules(mods.__path__) + +loaded_modules = {} +for mod_module in mod_modules: + module_name = mod_module.name + module = importlib.import_module("." + module_name, mods.__name__) + loaded_modules[module_name] = module + +assert by_mod diff --git a/worlds/stardew_valley/content/feature/__init__.py b/worlds/stardew_valley/content/feature/__init__.py new file mode 100644 index 000000000000..74249c808257 --- /dev/null +++ b/worlds/stardew_valley/content/feature/__init__.py @@ -0,0 +1,4 @@ +from . import booksanity +from . import cropsanity +from . import fishsanity +from . import friendsanity diff --git a/worlds/stardew_valley/content/feature/booksanity.py b/worlds/stardew_valley/content/feature/booksanity.py new file mode 100644 index 000000000000..5eade5932535 --- /dev/null +++ b/worlds/stardew_valley/content/feature/booksanity.py @@ -0,0 +1,72 @@ +from abc import ABC, abstractmethod +from typing import ClassVar, Optional, Iterable + +from ...data.game_item import GameItem, ItemTag +from ...strings.book_names import ordered_lost_books + +item_prefix = "Power: " +location_prefix = "Read " + + +def to_item_name(book: str) -> str: + return item_prefix + book + + +def to_location_name(book: str) -> str: + return location_prefix + book + + +def extract_book_from_location_name(location_name: str) -> Optional[str]: + if not location_name.startswith(location_prefix): + return None + + return location_name[len(location_prefix):] + + +class BooksanityFeature(ABC): + is_enabled: ClassVar[bool] + + to_item_name = staticmethod(to_item_name) + progressive_lost_book = "Progressive Lost Book" + to_location_name = staticmethod(to_location_name) + extract_book_from_location_name = staticmethod(extract_book_from_location_name) + + @abstractmethod + def is_included(self, book: GameItem) -> bool: + ... + + @staticmethod + def get_randomized_lost_books() -> Iterable[str]: + return [] + + +class BooksanityDisabled(BooksanityFeature): + is_enabled = False + + def is_included(self, book: GameItem) -> bool: + return False + + +class BooksanityPower(BooksanityFeature): + is_enabled = True + + def is_included(self, book: GameItem) -> bool: + return ItemTag.BOOK_POWER in book.tags + + +class BooksanityPowerSkill(BooksanityFeature): + is_enabled = True + + def is_included(self, book: GameItem) -> bool: + return ItemTag.BOOK_POWER in book.tags or ItemTag.BOOK_SKILL in book.tags + + +class BooksanityAll(BooksanityFeature): + is_enabled = True + + def is_included(self, book: GameItem) -> bool: + return ItemTag.BOOK_POWER in book.tags or ItemTag.BOOK_SKILL in book.tags + + @staticmethod + def get_randomized_lost_books() -> Iterable[str]: + return ordered_lost_books diff --git a/worlds/stardew_valley/content/feature/cropsanity.py b/worlds/stardew_valley/content/feature/cropsanity.py new file mode 100644 index 000000000000..18ef370815ee --- /dev/null +++ b/worlds/stardew_valley/content/feature/cropsanity.py @@ -0,0 +1,42 @@ +from abc import ABC, abstractmethod +from typing import ClassVar, Optional + +from ...data.game_item import GameItem, ItemTag + +location_prefix = "Harvest " + + +def to_location_name(crop: str) -> str: + return location_prefix + crop + + +def extract_crop_from_location_name(location_name: str) -> Optional[str]: + if not location_name.startswith(location_prefix): + return None + + return location_name[len(location_prefix):] + + +class CropsanityFeature(ABC): + is_enabled: ClassVar[bool] + + to_location_name = staticmethod(to_location_name) + extract_crop_from_location_name = staticmethod(extract_crop_from_location_name) + + @abstractmethod + def is_included(self, crop: GameItem) -> bool: + ... + + +class CropsanityDisabled(CropsanityFeature): + is_enabled = False + + def is_included(self, crop: GameItem) -> bool: + return False + + +class CropsanityEnabled(CropsanityFeature): + is_enabled = True + + def is_included(self, crop: GameItem) -> bool: + return ItemTag.CROPSANITY_SEED in crop.tags diff --git a/worlds/stardew_valley/content/feature/fishsanity.py b/worlds/stardew_valley/content/feature/fishsanity.py new file mode 100644 index 000000000000..02f9a632a873 --- /dev/null +++ b/worlds/stardew_valley/content/feature/fishsanity.py @@ -0,0 +1,101 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import ClassVar, Optional + +from ...data.fish_data import FishItem +from ...strings.fish_names import Fish + +location_prefix = "Fishsanity: " + + +def to_location_name(fish: str) -> str: + return location_prefix + fish + + +def extract_fish_from_location_name(location_name: str) -> Optional[str]: + if not location_name.startswith(location_prefix): + return None + + return location_name[len(location_prefix):] + + +@dataclass(frozen=True) +class FishsanityFeature(ABC): + is_enabled: ClassVar[bool] + + randomization_ratio: float = 1 + + to_location_name = staticmethod(to_location_name) + extract_fish_from_location_name = staticmethod(extract_fish_from_location_name) + + @property + def is_randomized(self) -> bool: + return self.randomization_ratio != 1 + + @abstractmethod + def is_included(self, fish: FishItem) -> bool: + ... + + +class FishsanityNone(FishsanityFeature): + is_enabled = False + + def is_included(self, fish: FishItem) -> bool: + return False + + +class FishsanityLegendaries(FishsanityFeature): + is_enabled = True + + def is_included(self, fish: FishItem) -> bool: + return fish.legendary + + +class FishsanitySpecial(FishsanityFeature): + is_enabled = True + + included_fishes = { + Fish.angler, + Fish.crimsonfish, + Fish.glacierfish, + Fish.legend, + Fish.mutant_carp, + Fish.blobfish, + Fish.lava_eel, + Fish.octopus, + Fish.scorpion_carp, + Fish.ice_pip, + Fish.super_cucumber, + Fish.dorado + } + + def is_included(self, fish: FishItem) -> bool: + return fish.name in self.included_fishes + + +class FishsanityAll(FishsanityFeature): + is_enabled = True + + def is_included(self, fish: FishItem) -> bool: + return True + + +class FishsanityExcludeLegendaries(FishsanityFeature): + is_enabled = True + + def is_included(self, fish: FishItem) -> bool: + return not fish.legendary + + +class FishsanityExcludeHardFish(FishsanityFeature): + is_enabled = True + + def is_included(self, fish: FishItem) -> bool: + return fish.difficulty < 80 + + +class FishsanityOnlyEasyFish(FishsanityFeature): + is_enabled = True + + def is_included(self, fish: FishItem) -> bool: + return fish.difficulty < 50 diff --git a/worlds/stardew_valley/content/feature/friendsanity.py b/worlds/stardew_valley/content/feature/friendsanity.py new file mode 100644 index 000000000000..3e1581b4e2f1 --- /dev/null +++ b/worlds/stardew_valley/content/feature/friendsanity.py @@ -0,0 +1,139 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from functools import lru_cache +from typing import Optional, Tuple, ClassVar + +from ...data.villagers_data import Villager +from ...strings.villager_names import NPC + +suffix = " <3" +location_prefix = "Friendsanity: " + + +def to_item_name(npc_name: str) -> str: + return npc_name + suffix + + +def to_location_name(npc_name: str, heart: int) -> str: + return location_prefix + npc_name + " " + str(heart) + suffix + + +pet_heart_item_name = to_item_name(NPC.pet) + + +def extract_npc_from_item_name(item_name: str) -> Optional[str]: + if not item_name.endswith(suffix): + return None + + return item_name[:-len(suffix)] + + +def extract_npc_from_location_name(location_name: str) -> Tuple[Optional[str], int]: + if not location_name.endswith(suffix): + return None, 0 + + trimmed = location_name[len(location_prefix):-len(suffix)] + last_space = trimmed.rindex(" ") + return trimmed[:last_space], int(trimmed[last_space + 1:]) + + +@lru_cache(maxsize=32) # Should not go pass 32 values if every friendsanity options are in the multi world +def get_heart_steps(max_heart: int, heart_size: int) -> Tuple[int, ...]: + return tuple(range(heart_size, max_heart + 1, heart_size)) + ((max_heart,) if max_heart % heart_size else ()) + + +@dataclass(frozen=True) +class FriendsanityFeature(ABC): + is_enabled: ClassVar[bool] + + heart_size: int + + to_item_name = staticmethod(to_item_name) + to_location_name = staticmethod(to_location_name) + pet_heart_item_name = pet_heart_item_name + extract_npc_from_item_name = staticmethod(extract_npc_from_item_name) + extract_npc_from_location_name = staticmethod(extract_npc_from_location_name) + + @abstractmethod + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + ... + + @property + def is_pet_randomized(self): + return bool(self.get_pet_randomized_hearts()) + + @abstractmethod + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + ... + + +class FriendsanityNone(FriendsanityFeature): + is_enabled = False + + def __init__(self): + super().__init__(1) + + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + return () + + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + return () + + +@dataclass(frozen=True) +class FriendsanityBachelors(FriendsanityFeature): + is_enabled = True + + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + if not villager.bachelor: + return () + + return get_heart_steps(8, self.heart_size) + + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + return () + + +@dataclass(frozen=True) +class FriendsanityStartingNpc(FriendsanityFeature): + is_enabled = True + + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + if not villager.available: + return () + + if villager.bachelor: + return get_heart_steps(8, self.heart_size) + + return get_heart_steps(10, self.heart_size) + + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + return get_heart_steps(5, self.heart_size) + + +@dataclass(frozen=True) +class FriendsanityAll(FriendsanityFeature): + is_enabled = True + + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + if villager.bachelor: + return get_heart_steps(8, self.heart_size) + + return get_heart_steps(10, self.heart_size) + + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + return get_heart_steps(5, self.heart_size) + + +@dataclass(frozen=True) +class FriendsanityAllWithMarriage(FriendsanityFeature): + is_enabled = True + + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + if villager.bachelor: + return get_heart_steps(14, self.heart_size) + + return get_heart_steps(10, self.heart_size) + + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + return get_heart_steps(5, self.heart_size) diff --git a/worlds/stardew_valley/content/game_content.py b/worlds/stardew_valley/content/game_content.py new file mode 100644 index 000000000000..8dcf933145e3 --- /dev/null +++ b/worlds/stardew_valley/content/game_content.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Iterable, Set, Any, Mapping, Type, Tuple, Union + +from .feature import booksanity, cropsanity, fishsanity, friendsanity +from ..data.fish_data import FishItem +from ..data.game_item import GameItem, ItemSource, ItemTag +from ..data.skill import Skill +from ..data.villagers_data import Villager + + +@dataclass(frozen=True) +class StardewContent: + features: StardewFeatures + registered_packs: Set[str] = field(default_factory=set) + + # regions -> To be used with can reach rule + + game_items: Dict[str, GameItem] = field(default_factory=dict) + fishes: Dict[str, FishItem] = field(default_factory=dict) + villagers: Dict[str, Villager] = field(default_factory=dict) + skills: Dict[str, Skill] = field(default_factory=dict) + quests: Dict[str, Any] = field(default_factory=dict) + + def find_sources_of_type(self, types: Union[Type[ItemSource], Tuple[Type[ItemSource]]]) -> Iterable[ItemSource]: + for item in self.game_items.values(): + for source in item.sources: + if isinstance(source, types): + yield source + + def source_item(self, item_name: str, *sources: ItemSource): + item = self.game_items.setdefault(item_name, GameItem(item_name)) + item.add_sources(sources) + + def tag_item(self, item_name: str, *tags: ItemTag): + item = self.game_items.setdefault(item_name, GameItem(item_name)) + item.add_tags(tags) + + def untag_item(self, item_name: str, tag: ItemTag): + self.game_items[item_name].tags.remove(tag) + + def find_tagged_items(self, tag: ItemTag) -> Iterable[GameItem]: + # TODO might be worth caching this, but it need to only be cached once the content is finalized... + for item in self.game_items.values(): + if tag in item.tags: + yield item + + +@dataclass(frozen=True) +class StardewFeatures: + booksanity: booksanity.BooksanityFeature + cropsanity: cropsanity.CropsanityFeature + fishsanity: fishsanity.FishsanityFeature + friendsanity: friendsanity.FriendsanityFeature + + +@dataclass(frozen=True) +class ContentPack: + name: str + + dependencies: Iterable[str] = () + """ Hard requirement, generation will fail if it's missing. """ + weak_dependencies: Iterable[str] = () + """ Not a strict dependency, only used only for ordering the packs to make sure hooks are applied correctly. """ + + # items + # def item_hook + # ... + + harvest_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict) + """Harvest sources contains both crops and forageables, but also fruits from trees, the cave farm and stuff harvested from tapping like maple syrup.""" + + def harvest_source_hook(self, content: StardewContent): + ... + + shop_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict) + + def shop_source_hook(self, content: StardewContent): + ... + + fishes: Iterable[FishItem] = () + + def fish_hook(self, content: StardewContent): + ... + + crafting_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict) + + def crafting_hook(self, content: StardewContent): + ... + + artisan_good_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict) + + def artisan_good_hook(self, content: StardewContent): + ... + + villagers: Iterable[Villager] = () + + def villager_hook(self, content: StardewContent): + ... + + skills: Iterable[Skill] = () + + def skill_hook(self, content: StardewContent): + ... + + quests: Iterable[Any] = () + + def quest_hook(self, content: StardewContent): + ... + + def finalize_hook(self, content: StardewContent): + """Last hook called on the pack, once all other content packs have been registered. + + This is the place to do any final adjustments to the content, like adding rules based on tags applied by other packs. + """ + ... diff --git a/worlds/stardew_valley/content/mod_registry.py b/worlds/stardew_valley/content/mod_registry.py new file mode 100644 index 000000000000..c598fcbad295 --- /dev/null +++ b/worlds/stardew_valley/content/mod_registry.py @@ -0,0 +1,7 @@ +from .game_content import ContentPack + +by_mod = {} + + +def register_mod_content_pack(content_pack: ContentPack): + by_mod[content_pack.name] = content_pack diff --git a/worlds/stardew_valley/content/mods/__init__.py b/worlds/stardew_valley/content/mods/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/content/mods/archeology.py b/worlds/stardew_valley/content/mods/archeology.py new file mode 100644 index 000000000000..97d38085d3b2 --- /dev/null +++ b/worlds/stardew_valley/content/mods/archeology.py @@ -0,0 +1,20 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data.game_item import ItemTag, Tag +from ...data.shop import ShopSource +from ...data.skill import Skill +from ...mods.mod_data import ModNames +from ...strings.book_names import ModBook +from ...strings.region_names import LogicRegion +from ...strings.skill_names import ModSkill + +register_mod_content_pack(ContentPack( + ModNames.archaeology, + shop_sources={ + ModBook.digging_like_worms: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=500, shop_region=LogicRegion.bookseller_1),), + }, + skills=(Skill(name=ModSkill.archaeology, has_mastery=False),), + +)) diff --git a/worlds/stardew_valley/content/mods/big_backpack.py b/worlds/stardew_valley/content/mods/big_backpack.py new file mode 100644 index 000000000000..27b4ea1f816c --- /dev/null +++ b/worlds/stardew_valley/content/mods/big_backpack.py @@ -0,0 +1,7 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.big_backpack, +)) diff --git a/worlds/stardew_valley/content/mods/boarding_house.py b/worlds/stardew_valley/content/mods/boarding_house.py new file mode 100644 index 000000000000..f3ad138fa7c2 --- /dev/null +++ b/worlds/stardew_valley/content/mods/boarding_house.py @@ -0,0 +1,13 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data import villagers_data +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.boarding_house, + villagers=( + villagers_data.gregory, + villagers_data.sheila, + villagers_data.joel, + ) +)) diff --git a/worlds/stardew_valley/content/mods/deepwoods.py b/worlds/stardew_valley/content/mods/deepwoods.py new file mode 100644 index 000000000000..a78629da57c0 --- /dev/null +++ b/worlds/stardew_valley/content/mods/deepwoods.py @@ -0,0 +1,28 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data.harvest import ForagingSource +from ...mods.mod_data import ModNames +from ...strings.crop_names import Fruit +from ...strings.flower_names import Flower +from ...strings.region_names import DeepWoodsRegion +from ...strings.season_names import Season + +register_mod_content_pack(ContentPack( + ModNames.deepwoods, + harvest_sources={ + # Deep enough to have seen such a tree at least once + Fruit.apple: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.apricot: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.cherry: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.orange: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.peach: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.pomegranate: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.mango: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + + Flower.tulip: (ForagingSource(seasons=Season.not_winter, regions=(DeepWoodsRegion.floor_10,)),), + Flower.blue_jazz: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Flower.summer_spangle: (ForagingSource(seasons=Season.not_winter, regions=(DeepWoodsRegion.floor_10,)),), + Flower.poppy: (ForagingSource(seasons=Season.not_winter, regions=(DeepWoodsRegion.floor_10,)),), + Flower.fairy_rose: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + } +)) diff --git a/worlds/stardew_valley/content/mods/distant_lands.py b/worlds/stardew_valley/content/mods/distant_lands.py new file mode 100644 index 000000000000..19380d4ff565 --- /dev/null +++ b/worlds/stardew_valley/content/mods/distant_lands.py @@ -0,0 +1,17 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data import villagers_data, fish_data +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.distant_lands, + fishes=( + fish_data.void_minnow, + fish_data.purple_algae, + fish_data.swamp_leech, + fish_data.giant_horsehoe_crab, + ), + villagers=( + villagers_data.zic, + ) +)) diff --git a/worlds/stardew_valley/content/mods/jasper.py b/worlds/stardew_valley/content/mods/jasper.py new file mode 100644 index 000000000000..146b291d800a --- /dev/null +++ b/worlds/stardew_valley/content/mods/jasper.py @@ -0,0 +1,14 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ..override import override +from ...data import villagers_data +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.jasper, + villagers=( + villagers_data.jasper, + override(villagers_data.gunther, mod_name=ModNames.jasper), + override(villagers_data.marlon, mod_name=ModNames.jasper), + ) +)) diff --git a/worlds/stardew_valley/content/mods/magic.py b/worlds/stardew_valley/content/mods/magic.py new file mode 100644 index 000000000000..aae3617cb00c --- /dev/null +++ b/worlds/stardew_valley/content/mods/magic.py @@ -0,0 +1,10 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data.skill import Skill +from ...mods.mod_data import ModNames +from ...strings.skill_names import ModSkill + +register_mod_content_pack(ContentPack( + ModNames.magic, + skills=(Skill(name=ModSkill.magic, has_mastery=False),) +)) diff --git a/worlds/stardew_valley/content/mods/npc_mods.py b/worlds/stardew_valley/content/mods/npc_mods.py new file mode 100644 index 000000000000..3172a55dbf32 --- /dev/null +++ b/worlds/stardew_valley/content/mods/npc_mods.py @@ -0,0 +1,88 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data import villagers_data +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.alec, + villagers=( + villagers_data.alec, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.ayeisha, + villagers=( + villagers_data.ayeisha, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.delores, + villagers=( + villagers_data.delores, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.eugene, + villagers=( + villagers_data.eugene, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.juna, + villagers=( + villagers_data.juna, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.ginger, + villagers=( + villagers_data.kitty, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.shiko, + villagers=( + villagers_data.shiko, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.wellwick, + villagers=( + villagers_data.wellwick, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.yoba, + villagers=( + villagers_data.yoba, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.riley, + villagers=( + villagers_data.riley, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.alecto, + villagers=( + villagers_data.alecto, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.lacey, + villagers=( + villagers_data.lacey, + ) +)) diff --git a/worlds/stardew_valley/content/mods/skill_mods.py b/worlds/stardew_valley/content/mods/skill_mods.py new file mode 100644 index 000000000000..7f88b2ebf2dc --- /dev/null +++ b/worlds/stardew_valley/content/mods/skill_mods.py @@ -0,0 +1,25 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data.skill import Skill +from ...mods.mod_data import ModNames +from ...strings.skill_names import ModSkill + +register_mod_content_pack(ContentPack( + ModNames.luck_skill, + skills=(Skill(name=ModSkill.luck, has_mastery=False),) +)) + +register_mod_content_pack(ContentPack( + ModNames.socializing_skill, + skills=(Skill(name=ModSkill.socializing, has_mastery=False),) +)) + +register_mod_content_pack(ContentPack( + ModNames.cooking_skill, + skills=(Skill(name=ModSkill.cooking, has_mastery=False),) +)) + +register_mod_content_pack(ContentPack( + ModNames.binning_skill, + skills=(Skill(name=ModSkill.binning, has_mastery=False),) +)) diff --git a/worlds/stardew_valley/content/mods/skull_cavern_elevator.py b/worlds/stardew_valley/content/mods/skull_cavern_elevator.py new file mode 100644 index 000000000000..ff8c089608e5 --- /dev/null +++ b/worlds/stardew_valley/content/mods/skull_cavern_elevator.py @@ -0,0 +1,7 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.skull_cavern_elevator, +)) diff --git a/worlds/stardew_valley/content/mods/sve.py b/worlds/stardew_valley/content/mods/sve.py new file mode 100644 index 000000000000..f74b80948c96 --- /dev/null +++ b/worlds/stardew_valley/content/mods/sve.py @@ -0,0 +1,126 @@ +from ..game_content import ContentPack, StardewContent +from ..mod_registry import register_mod_content_pack +from ..override import override +from ..vanilla.ginger_island import ginger_island_content_pack as ginger_island_content_pack +from ...data import villagers_data, fish_data +from ...data.harvest import ForagingSource +from ...data.requirement import YearRequirement +from ...mods.mod_data import ModNames +from ...strings.crop_names import Fruit +from ...strings.fish_names import WaterItem +from ...strings.flower_names import Flower +from ...strings.forageable_names import Mushroom, Forageable +from ...strings.region_names import Region, SVERegion +from ...strings.season_names import Season + + +class SVEContentPack(ContentPack): + + def fish_hook(self, content: StardewContent): + if ginger_island_content_pack.name not in content.registered_packs: + content.fishes.pop(fish_data.baby_lunaloo.name) + content.fishes.pop(fish_data.clownfish.name) + content.fishes.pop(fish_data.lunaloo.name) + content.fishes.pop(fish_data.seahorse.name) + content.fishes.pop(fish_data.shiny_lunaloo.name) + content.fishes.pop(fish_data.starfish.name) + content.fishes.pop(fish_data.sea_sponge.name) + + # Remove Highlands fishes at it requires 2 Lance hearts for the quest to access it + content.fishes.pop(fish_data.daggerfish.name) + content.fishes.pop(fish_data.gemfish.name) + + # Remove Fable Reef fishes at it requires 8 Lance hearts for the event to access it + content.fishes.pop(fish_data.torpedo_trout.name) + + def villager_hook(self, content: StardewContent): + if ginger_island_content_pack.name not in content.registered_packs: + # Remove Lance if Ginger Island is not in content since he is first encountered in Volcano Forge + content.villagers.pop(villagers_data.lance.name) + + +register_mod_content_pack(SVEContentPack( + ModNames.sve, + weak_dependencies=( + ginger_island_content_pack.name, + ModNames.jasper, # To override Marlon and Gunther + ), + harvest_sources={ + Mushroom.red: ( + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.summer, Season.fall)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) + ), + Mushroom.purple: ( + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) + ), + Mushroom.morel: ( + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) + ), + Mushroom.chanterelle: ( + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) + ), + Flower.tulip: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring,)),), + Flower.blue_jazz: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring,)),), + Flower.summer_spangle: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.summer,)),), + Flower.sunflower: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.summer,)),), + Flower.fairy_rose: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.fall,)),), + Fruit.ancient_fruit: ( + ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring, Season.summer, Season.fall), other_requirements=(YearRequirement(3),)), + ForagingSource(regions=(SVERegion.sprite_spring_cave,)), + ), + Fruit.sweet_gem_berry: ( + ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring, Season.summer, Season.fall), other_requirements=(YearRequirement(3),)), + ), + + # Fable Reef + WaterItem.coral: (ForagingSource(regions=(SVERegion.fable_reef,)),), + Forageable.rainbow_shell: (ForagingSource(regions=(SVERegion.fable_reef,)),), + WaterItem.sea_urchin: (ForagingSource(regions=(SVERegion.fable_reef,)),), + }, + fishes=( + fish_data.baby_lunaloo, # Removed when no ginger island + fish_data.bonefish, + fish_data.bull_trout, + fish_data.butterfish, + fish_data.clownfish, # Removed when no ginger island + fish_data.daggerfish, + fish_data.frog, + fish_data.gemfish, + fish_data.goldenfish, + fish_data.grass_carp, + fish_data.king_salmon, + fish_data.kittyfish, + fish_data.lunaloo, # Removed when no ginger island + fish_data.meteor_carp, + fish_data.minnow, + fish_data.puppyfish, + fish_data.radioactive_bass, + fish_data.seahorse, # Removed when no ginger island + fish_data.shiny_lunaloo, # Removed when no ginger island + fish_data.snatcher_worm, + fish_data.starfish, # Removed when no ginger island + fish_data.torpedo_trout, + fish_data.undeadfish, + fish_data.void_eel, + fish_data.water_grub, + fish_data.sea_sponge, # Removed when no ginger island + + ), + villagers=( + villagers_data.claire, + villagers_data.lance, # Removed when no ginger island + villagers_data.mommy, + villagers_data.sophia, + villagers_data.victor, + villagers_data.andy, + villagers_data.apples, + villagers_data.gunther, + villagers_data.martin, + villagers_data.marlon, + villagers_data.morgan, + villagers_data.scarlett, + villagers_data.susan, + villagers_data.morris, + # The wizard leaves his tower on sunday, for like 1 hour... Good enough for entrance rando! + override(villagers_data.wizard, locations=(Region.wizard_tower, Region.forest), bachelor=True, mod_name=ModNames.sve), + ) +)) diff --git a/worlds/stardew_valley/content/mods/tractor.py b/worlds/stardew_valley/content/mods/tractor.py new file mode 100644 index 000000000000..8f143001791c --- /dev/null +++ b/worlds/stardew_valley/content/mods/tractor.py @@ -0,0 +1,7 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.tractor, +)) diff --git a/worlds/stardew_valley/content/override.py b/worlds/stardew_valley/content/override.py new file mode 100644 index 000000000000..adfc64c95b49 --- /dev/null +++ b/worlds/stardew_valley/content/override.py @@ -0,0 +1,7 @@ +from typing import Any + + +def override(content: Any, **kwargs) -> Any: + attributes = dict(content.__dict__) + attributes.update(kwargs) + return type(content)(**attributes) diff --git a/worlds/stardew_valley/content/unpacking.py b/worlds/stardew_valley/content/unpacking.py new file mode 100644 index 000000000000..f069866d56cd --- /dev/null +++ b/worlds/stardew_valley/content/unpacking.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from typing import Iterable, Mapping, Callable + +from .game_content import StardewContent, ContentPack, StardewFeatures +from .vanilla.base import base_game as base_game_content_pack +from ..data.game_item import GameItem, ItemSource + +try: + from graphlib import TopologicalSorter +except ImportError: + from graphlib_backport import TopologicalSorter # noqa + + +def unpack_content(features: StardewFeatures, packs: Iterable[ContentPack]) -> StardewContent: + # Base game is always registered first. + content = StardewContent(features) + packs_to_finalize = [base_game_content_pack] + register_pack(content, base_game_content_pack) + + # Content packs are added in order based on their dependencies + sorter = TopologicalSorter() + packs_by_name = {p.name: p for p in packs} + + # Build the dependency graph + for name, pack in packs_by_name.items(): + sorter.add(name, + *pack.dependencies, + *(wd for wd in pack.weak_dependencies if wd in packs_by_name)) + + # Graph is traversed in BFS + sorter.prepare() + while sorter.is_active(): + # Packs get shuffled in TopologicalSorter, most likely due to hash seeding. + for pack_name in sorted(sorter.get_ready()): + pack = packs_by_name[pack_name] + register_pack(content, pack) + sorter.done(pack_name) + packs_to_finalize.append(pack) + + prune_inaccessible_items(content) + + for pack in packs_to_finalize: + pack.finalize_hook(content) + + # Maybe items without source should be removed at some point + return content + + +def register_pack(content: StardewContent, pack: ContentPack): + # register regions + + # register entrances + + register_sources_and_call_hook(content, pack.harvest_sources, pack.harvest_source_hook) + register_sources_and_call_hook(content, pack.shop_sources, pack.shop_source_hook) + register_sources_and_call_hook(content, pack.crafting_sources, pack.crafting_hook) + register_sources_and_call_hook(content, pack.artisan_good_sources, pack.artisan_good_hook) + + for fish in pack.fishes: + content.fishes[fish.name] = fish + pack.fish_hook(content) + + for villager in pack.villagers: + content.villagers[villager.name] = villager + pack.villager_hook(content) + + for skill in pack.skills: + content.skills[skill.name] = skill + pack.skill_hook(content) + + # register_quests + + # ... + + content.registered_packs.add(pack.name) + + +def register_sources_and_call_hook(content: StardewContent, + sources_by_item_name: Mapping[str, Iterable[ItemSource]], + hook: Callable[[StardewContent], None]): + for item_name, sources in sources_by_item_name.items(): + item = content.game_items.setdefault(item_name, GameItem(item_name)) + item.add_sources(sources) + + for source in sources: + for requirement_name, tags in source.requirement_tags.items(): + requirement_item = content.game_items.setdefault(requirement_name, GameItem(requirement_name)) + requirement_item.add_tags(tags) + + hook(content) + + +def prune_inaccessible_items(content: StardewContent): + for item in list(content.game_items.values()): + if not item.sources: + content.game_items.pop(item.name) diff --git a/worlds/stardew_valley/content/vanilla/__init__.py b/worlds/stardew_valley/content/vanilla/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/content/vanilla/base.py b/worlds/stardew_valley/content/vanilla/base.py new file mode 100644 index 000000000000..2c910df5d00f --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/base.py @@ -0,0 +1,172 @@ +from ..game_content import ContentPack, StardewContent +from ...data.artisan import MachineSource +from ...data.game_item import ItemTag, CustomRuleSource, GameItem +from ...data.harvest import HarvestFruitTreeSource, HarvestCropSource +from ...data.skill import Skill +from ...strings.artisan_good_names import ArtisanGood +from ...strings.craftable_names import WildSeeds +from ...strings.crop_names import Fruit, Vegetable +from ...strings.flower_names import Flower +from ...strings.food_names import Beverage +from ...strings.forageable_names import all_edible_mushrooms, Mushroom, Forageable +from ...strings.fruit_tree_names import Sapling +from ...strings.machine_names import Machine +from ...strings.monster_names import Monster +from ...strings.season_names import Season +from ...strings.seed_names import Seed +from ...strings.skill_names import Skill as SkillName + +all_fruits = ( + Fruit.ancient_fruit, Fruit.apple, Fruit.apricot, Fruit.banana, Forageable.blackberry, Fruit.blueberry, Forageable.cactus_fruit, Fruit.cherry, + Forageable.coconut, Fruit.cranberries, Forageable.crystal_fruit, Fruit.grape, Fruit.hot_pepper, Fruit.mango, Fruit.melon, Fruit.orange, Fruit.peach, + Fruit.pineapple, Fruit.pomegranate, Fruit.powdermelon, Fruit.qi_fruit, Fruit.rhubarb, Forageable.salmonberry, Forageable.spice_berry, Fruit.starfruit, + Fruit.strawberry +) + +all_vegetables = ( + Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.bok_choy, Vegetable.broccoli, Vegetable.carrot, Vegetable.cauliflower, + Vegetable.corn, Vegetable.eggplant, Forageable.fiddlehead_fern, Vegetable.garlic, Vegetable.green_bean, Vegetable.hops, Vegetable.kale, + Vegetable.parsnip, Vegetable.potato, Vegetable.pumpkin, Vegetable.radish, Vegetable.red_cabbage, Vegetable.summer_squash, Vegetable.taro_root, + Vegetable.tea_leaves, Vegetable.tomato, Vegetable.unmilled_rice, Vegetable.wheat, Vegetable.yam +) + +non_juiceable_vegetables = (Vegetable.hops, Vegetable.tea_leaves, Vegetable.wheat, Vegetable.tea_leaves) + + +# This will hold items, skills and stuff that is available everywhere across the game, but not directly needing pelican town (crops, ore, foraging, etc.) +class BaseGameContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + coffee_starter = content.game_items[Seed.coffee_starter] + content.game_items[Seed.coffee_starter] = GameItem(Seed.coffee, sources=coffee_starter.sources, tags=coffee_starter.tags) + + content.untag_item(WildSeeds.ancient, ItemTag.CROPSANITY_SEED) + + for fruit in all_fruits: + content.tag_item(fruit, ItemTag.FRUIT) + + for vegetable in all_vegetables: + content.tag_item(vegetable, ItemTag.VEGETABLE) + + for edible_mushroom in all_edible_mushrooms: + if edible_mushroom == Mushroom.magma_cap: + continue + + content.tag_item(edible_mushroom, ItemTag.EDIBLE_MUSHROOM) + + def finalize_hook(self, content: StardewContent): + # FIXME I hate this design. A listener design pattern would be more appropriate so artisan good are register at the exact moment a FRUIT tag is added. + for fruit in tuple(content.find_tagged_items(ItemTag.FRUIT)): + wine = ArtisanGood.specific_wine(fruit.name) + content.source_item(wine, MachineSource(item=fruit.name, machine=Machine.keg)) + content.source_item(ArtisanGood.wine, MachineSource(item=fruit.name, machine=Machine.keg)) + + if fruit.name == Fruit.grape: + content.source_item(ArtisanGood.raisins, MachineSource(item=fruit.name, machine=Machine.dehydrator)) + else: + dried_fruit = ArtisanGood.specific_dried_fruit(fruit.name) + content.source_item(dried_fruit, MachineSource(item=fruit.name, machine=Machine.dehydrator)) + content.source_item(ArtisanGood.dried_fruit, MachineSource(item=fruit.name, machine=Machine.dehydrator)) + + jelly = ArtisanGood.specific_jelly(fruit.name) + content.source_item(jelly, MachineSource(item=fruit.name, machine=Machine.preserves_jar)) + content.source_item(ArtisanGood.jelly, MachineSource(item=fruit.name, machine=Machine.preserves_jar)) + + for vegetable in tuple(content.find_tagged_items(ItemTag.VEGETABLE)): + if vegetable.name not in non_juiceable_vegetables: + juice = ArtisanGood.specific_juice(vegetable.name) + content.source_item(juice, MachineSource(item=vegetable.name, machine=Machine.keg)) + content.source_item(ArtisanGood.juice, MachineSource(item=vegetable.name, machine=Machine.keg)) + + pickles = ArtisanGood.specific_pickles(vegetable.name) + content.source_item(pickles, MachineSource(item=vegetable.name, machine=Machine.preserves_jar)) + content.source_item(ArtisanGood.pickles, MachineSource(item=vegetable.name, machine=Machine.preserves_jar)) + + for mushroom in tuple(content.find_tagged_items(ItemTag.EDIBLE_MUSHROOM)): + dried_mushroom = ArtisanGood.specific_dried_mushroom(mushroom.name) + content.source_item(dried_mushroom, MachineSource(item=mushroom.name, machine=Machine.dehydrator)) + content.source_item(ArtisanGood.dried_mushroom, MachineSource(item=mushroom.name, machine=Machine.dehydrator)) + + # for fish in tuple(content.find_tagged_items(ItemTag.FISH)): + # smoked_fish = ArtisanGood.specific_smoked_fish(fish.name) + # content.source_item(smoked_fish, MachineSource(item=fish.name, machine=Machine.fish_smoker)) + # content.source_item(ArtisanGood.smoked_fish, MachineSource(item=fish.name, machine=Machine.fish_smoker)) + + +base_game = BaseGameContentPack( + "Base game (Vanilla)", + harvest_sources={ + # Fruit tree + Fruit.apple: (HarvestFruitTreeSource(sapling=Sapling.apple, seasons=(Season.fall,)),), + Fruit.apricot: (HarvestFruitTreeSource(sapling=Sapling.apricot, seasons=(Season.spring,)),), + Fruit.cherry: (HarvestFruitTreeSource(sapling=Sapling.cherry, seasons=(Season.spring,)),), + Fruit.orange: (HarvestFruitTreeSource(sapling=Sapling.orange, seasons=(Season.summer,)),), + Fruit.peach: (HarvestFruitTreeSource(sapling=Sapling.peach, seasons=(Season.summer,)),), + Fruit.pomegranate: (HarvestFruitTreeSource(sapling=Sapling.pomegranate, seasons=(Season.fall,)),), + + # Crops + Vegetable.parsnip: (HarvestCropSource(seed=Seed.parsnip, seasons=(Season.spring,)),), + Vegetable.green_bean: (HarvestCropSource(seed=Seed.bean, seasons=(Season.spring,)),), + Vegetable.cauliflower: (HarvestCropSource(seed=Seed.cauliflower, seasons=(Season.spring,)),), + Vegetable.potato: (HarvestCropSource(seed=Seed.potato, seasons=(Season.spring,)),), + Flower.tulip: (HarvestCropSource(seed=Seed.tulip, seasons=(Season.spring,)),), + Vegetable.kale: (HarvestCropSource(seed=Seed.kale, seasons=(Season.spring,)),), + Flower.blue_jazz: (HarvestCropSource(seed=Seed.jazz, seasons=(Season.spring,)),), + Vegetable.garlic: (HarvestCropSource(seed=Seed.garlic, seasons=(Season.spring,)),), + Vegetable.unmilled_rice: (HarvestCropSource(seed=Seed.rice, seasons=(Season.spring,)),), + + Fruit.melon: (HarvestCropSource(seed=Seed.melon, seasons=(Season.summer,)),), + Vegetable.tomato: (HarvestCropSource(seed=Seed.tomato, seasons=(Season.summer,)),), + Fruit.blueberry: (HarvestCropSource(seed=Seed.blueberry, seasons=(Season.summer,)),), + Fruit.hot_pepper: (HarvestCropSource(seed=Seed.pepper, seasons=(Season.summer,)),), + Vegetable.wheat: (HarvestCropSource(seed=Seed.wheat, seasons=(Season.summer, Season.fall)),), + Vegetable.radish: (HarvestCropSource(seed=Seed.radish, seasons=(Season.summer,)),), + Flower.poppy: (HarvestCropSource(seed=Seed.poppy, seasons=(Season.summer,)),), + Flower.summer_spangle: (HarvestCropSource(seed=Seed.spangle, seasons=(Season.summer,)),), + Vegetable.hops: (HarvestCropSource(seed=Seed.hops, seasons=(Season.summer,)),), + Vegetable.corn: (HarvestCropSource(seed=Seed.corn, seasons=(Season.summer, Season.fall)),), + Flower.sunflower: (HarvestCropSource(seed=Seed.sunflower, seasons=(Season.summer, Season.fall)),), + Vegetable.red_cabbage: (HarvestCropSource(seed=Seed.red_cabbage, seasons=(Season.summer,)),), + + Vegetable.eggplant: (HarvestCropSource(seed=Seed.eggplant, seasons=(Season.fall,)),), + Vegetable.pumpkin: (HarvestCropSource(seed=Seed.pumpkin, seasons=(Season.fall,)),), + Vegetable.bok_choy: (HarvestCropSource(seed=Seed.bok_choy, seasons=(Season.fall,)),), + Vegetable.yam: (HarvestCropSource(seed=Seed.yam, seasons=(Season.fall,)),), + Fruit.cranberries: (HarvestCropSource(seed=Seed.cranberry, seasons=(Season.fall,)),), + Flower.fairy_rose: (HarvestCropSource(seed=Seed.fairy, seasons=(Season.fall,)),), + Vegetable.amaranth: (HarvestCropSource(seed=Seed.amaranth, seasons=(Season.fall,)),), + Fruit.grape: (HarvestCropSource(seed=Seed.grape, seasons=(Season.fall,)),), + Vegetable.artichoke: (HarvestCropSource(seed=Seed.artichoke, seasons=(Season.fall,)),), + + Vegetable.broccoli: (HarvestCropSource(seed=Seed.broccoli, seasons=(Season.fall,)),), + Vegetable.carrot: (HarvestCropSource(seed=Seed.carrot, seasons=(Season.spring,)),), + Fruit.powdermelon: (HarvestCropSource(seed=Seed.powdermelon, seasons=(Season.summer,)),), + Vegetable.summer_squash: (HarvestCropSource(seed=Seed.summer_squash, seasons=(Season.summer,)),), + + Fruit.strawberry: (HarvestCropSource(seed=Seed.strawberry, seasons=(Season.spring,)),), + Fruit.sweet_gem_berry: (HarvestCropSource(seed=Seed.rare_seed, seasons=(Season.fall,)),), + Fruit.ancient_fruit: (HarvestCropSource(seed=WildSeeds.ancient, seasons=(Season.spring, Season.summer, Season.fall,)),), + + Seed.coffee_starter: (CustomRuleSource(lambda logic: logic.traveling_merchant.has_days(3) & logic.monster.can_kill_many(Monster.dust_sprite)),), + Seed.coffee: (HarvestCropSource(seed=Seed.coffee_starter, seasons=(Season.spring, Season.summer,)),), + + Vegetable.tea_leaves: (CustomRuleSource(lambda logic: logic.has(Sapling.tea) & logic.time.has_lived_months(2) & logic.season.has_any_not_winter()),), + }, + artisan_good_sources={ + Beverage.beer: (MachineSource(item=Vegetable.wheat, machine=Machine.keg),), + # Ingredient.vinegar: (MachineSource(item=Ingredient.rice, machine=Machine.keg),), + Beverage.coffee: (MachineSource(item=Seed.coffee, machine=Machine.keg), + CustomRuleSource(lambda logic: logic.has(Machine.coffee_maker)), + CustomRuleSource(lambda logic: logic.has("Hot Java Ring"))), + ArtisanGood.green_tea: (MachineSource(item=Vegetable.tea_leaves, machine=Machine.keg),), + ArtisanGood.mead: (MachineSource(item=ArtisanGood.honey, machine=Machine.keg),), + ArtisanGood.pale_ale: (MachineSource(item=Vegetable.hops, machine=Machine.keg),), + }, + skills=( + Skill(SkillName.farming, has_mastery=True), + Skill(SkillName.foraging, has_mastery=True), + Skill(SkillName.fishing, has_mastery=True), + Skill(SkillName.mining, has_mastery=True), + Skill(SkillName.combat, has_mastery=True), + ) +) diff --git a/worlds/stardew_valley/content/vanilla/ginger_island.py b/worlds/stardew_valley/content/vanilla/ginger_island.py new file mode 100644 index 000000000000..d824deff3903 --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/ginger_island.py @@ -0,0 +1,81 @@ +from .pelican_town import pelican_town as pelican_town_content_pack +from ..game_content import ContentPack, StardewContent +from ...data import villagers_data, fish_data +from ...data.game_item import ItemTag, Tag +from ...data.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource +from ...data.shop import ShopSource +from ...strings.book_names import Book +from ...strings.crop_names import Fruit, Vegetable +from ...strings.fish_names import Fish +from ...strings.forageable_names import Forageable, Mushroom +from ...strings.fruit_tree_names import Sapling +from ...strings.metal_names import Fossil, Mineral +from ...strings.region_names import Region +from ...strings.season_names import Season +from ...strings.seed_names import Seed + + +class GingerIslandContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + content.tag_item(Fruit.banana, ItemTag.FRUIT) + content.tag_item(Fruit.pineapple, ItemTag.FRUIT) + content.tag_item(Fruit.mango, ItemTag.FRUIT) + content.tag_item(Vegetable.taro_root, ItemTag.VEGETABLE) + content.tag_item(Mushroom.magma_cap, ItemTag.EDIBLE_MUSHROOM) + + +ginger_island_content_pack = GingerIslandContentPack( + "Ginger Island (Vanilla)", + weak_dependencies=( + pelican_town_content_pack.name, + ), + harvest_sources={ + # Foraging + Forageable.dragon_tooth: ( + ForagingSource(regions=(Region.volcano_floor_10,)), + ), + Forageable.ginger: ( + ForagingSource(regions=(Region.island_west,)), + ), + Mushroom.magma_cap: ( + ForagingSource(regions=(Region.volcano_floor_5,)), + ), + + # Fruit tree + Fruit.banana: (HarvestFruitTreeSource(sapling=Sapling.banana, seasons=(Season.summer,)),), + Fruit.mango: (HarvestFruitTreeSource(sapling=Sapling.mango, seasons=(Season.summer,)),), + + # Crop + Vegetable.taro_root: (HarvestCropSource(seed=Seed.taro, seasons=(Season.summer,)),), + Fruit.pineapple: (HarvestCropSource(seed=Seed.pineapple, seasons=(Season.summer,)),), + + }, + shop_sources={ + Seed.taro: (ShopSource(items_price=((2, Fossil.bone_fragment),), shop_region=Region.island_trader),), + Seed.pineapple: (ShopSource(items_price=((1, Mushroom.magma_cap),), shop_region=Region.island_trader),), + Sapling.banana: (ShopSource(items_price=((5, Forageable.dragon_tooth),), shop_region=Region.island_trader),), + Sapling.mango: (ShopSource(items_price=((75, Fish.mussel_node),), shop_region=Region.island_trader),), + + # This one is 10 diamonds, should maybe add time? + Book.the_diamond_hunter: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(items_price=((10, Mineral.diamond),), shop_region=Region.volcano_dwarf_shop), + ), + + }, + fishes=( + # TODO override region so no need to add inaccessible regions in logic + fish_data.blue_discus, + fish_data.lionfish, + fish_data.midnight_carp, + fish_data.pufferfish, + fish_data.stingray, + fish_data.super_cucumber, + fish_data.tilapia, + fish_data.tuna + ), + villagers=( + villagers_data.leo, + ) +) diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py new file mode 100644 index 000000000000..2c687eacbdde --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -0,0 +1,393 @@ +from ..game_content import ContentPack +from ...data import villagers_data, fish_data +from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource +from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource +from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement +from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource +from ...strings.book_names import Book +from ...strings.crop_names import Fruit +from ...strings.fish_names import WaterItem +from ...strings.food_names import Beverage, Meal +from ...strings.forageable_names import Forageable, Mushroom +from ...strings.fruit_tree_names import Sapling +from ...strings.generic_names import Generic +from ...strings.material_names import Material +from ...strings.region_names import Region, LogicRegion +from ...strings.season_names import Season +from ...strings.seed_names import Seed, TreeSeed +from ...strings.skill_names import Skill +from ...strings.tool_names import Tool, ToolMaterial + +pelican_town = ContentPack( + "Pelican Town (Vanilla)", + harvest_sources={ + # Spring + Forageable.daffodil: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.bus_stop, Region.town, Region.railroad)), + ), + Forageable.dandelion: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.bus_stop, Region.forest, Region.railroad)), + ), + Forageable.leek: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.railroad)), + ), + Forageable.wild_horseradish: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.backwoods, Region.mountain, Region.forest, Region.secret_woods)), + ), + Forageable.salmonberry: ( + SeasonalForagingSource(season=Season.spring, days=(15, 16, 17, 18), + regions=(Region.backwoods, Region.mountain, Region.town, Region.forest, Region.tunnel_entrance, Region.railroad)), + ), + Forageable.spring_onion: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.forest,)), + ), + + # Summer + Fruit.grape: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.railroad)), + ), + Forageable.spice_berry: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.forest, Region.railroad)), + ), + Forageable.sweet_pea: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.bus_stop, Region.town, Region.forest, Region.railroad)), + ), + Forageable.fiddlehead_fern: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.secret_woods,)), + ), + + # Fall + Forageable.blackberry: ( + ForagingSource(seasons=(Season.fall,), regions=(Region.backwoods, Region.town, Region.forest, Region.railroad)), + SeasonalForagingSource(season=Season.fall, days=(8, 9, 10, 11), + regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.tunnel_entrance, + Region.railroad)), + ), + Forageable.hazelnut: ( + ForagingSource(seasons=(Season.fall,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.railroad)), + ), + Forageable.wild_plum: ( + ForagingSource(seasons=(Season.fall,), regions=(Region.mountain, Region.bus_stop, Region.railroad)), + ), + + # Winter + Forageable.crocus: ( + ForagingSource(seasons=(Season.winter,), + regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.secret_woods)), + ), + Forageable.crystal_fruit: ( + ForagingSource(seasons=(Season.winter,), + regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad)), + ), + Forageable.holly: ( + ForagingSource(seasons=(Season.winter,), + regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad)), + ), + Forageable.snow_yam: ( + ForagingSource(seasons=(Season.winter,), + regions=(Region.farm, Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad, + Region.secret_woods, Region.beach), + other_requirements=(ToolRequirement(Tool.hoe),)), + ), + Forageable.winter_root: ( + ForagingSource(seasons=(Season.winter,), + regions=(Region.farm, Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad, + Region.secret_woods, Region.beach), + other_requirements=(ToolRequirement(Tool.hoe),)), + ), + + # Mushrooms + Mushroom.common: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.secret_woods,)), + ForagingSource(seasons=(Season.fall,), regions=(Region.backwoods, Region.mountain, Region.forest)), + ), + Mushroom.chanterelle: ( + ForagingSource(seasons=(Season.fall,), regions=(Region.secret_woods,)), + ), + Mushroom.morel: ( + ForagingSource(seasons=(Season.spring, Season.fall), regions=(Region.secret_woods,)), + ), + Mushroom.red: ( + ForagingSource(seasons=(Season.summer, Season.fall), regions=(Region.secret_woods,)), + ), + + # Beach + WaterItem.coral: ( + ForagingSource(regions=(Region.tide_pools,)), + SeasonalForagingSource(season=Season.summer, days=(12, 13, 14), regions=(Region.beach,)), + ), + WaterItem.nautilus_shell: ( + ForagingSource(seasons=(Season.winter,), regions=(Region.beach,)), + ), + Forageable.rainbow_shell: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.beach,)), + ), + WaterItem.sea_urchin: ( + ForagingSource(regions=(Region.tide_pools,)), + ), + + Seed.mixed: ( + ForagingSource(seasons=(Season.spring, Season.summer, Season.fall,), regions=(Region.town, Region.farm, Region.forest)), + ), + + Seed.mixed_flower: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.town, Region.farm, Region.forest)), + ), + + # Books + Book.jack_be_nimble_jack_be_thick: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ArtifactSpotSource(amount=22),), # After 22 spots, there are 50.48% chances player received the book. + Book.woodys_secret: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + GenericSource(regions=(Region.forest, Region.mountain), + other_requirements=(ToolRequirement(Tool.axe, ToolMaterial.iron), SkillRequirement(Skill.foraging, 5))),), + }, + shop_sources={ + # Saplings + Sapling.apple: (ShopSource(money_price=4000, shop_region=Region.pierre_store),), + Sapling.apricot: (ShopSource(money_price=2000, shop_region=Region.pierre_store),), + Sapling.cherry: (ShopSource(money_price=3400, shop_region=Region.pierre_store),), + Sapling.orange: (ShopSource(money_price=4000, shop_region=Region.pierre_store),), + Sapling.peach: (ShopSource(money_price=6000, shop_region=Region.pierre_store),), + Sapling.pomegranate: (ShopSource(money_price=6000, shop_region=Region.pierre_store),), + + # Crop seeds, assuming they are bought in season, otherwise price is different with missing stock list. + Seed.parsnip: (ShopSource(money_price=20, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.bean: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.cauliflower: (ShopSource(money_price=80, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.potato: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.tulip: (ShopSource(money_price=20, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.kale: (ShopSource(money_price=70, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.jazz: (ShopSource(money_price=30, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.garlic: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.rice: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + + Seed.melon: (ShopSource(money_price=80, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.tomato: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.blueberry: (ShopSource(money_price=80, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.pepper: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.wheat: (ShopSource(money_price=10, shop_region=Region.pierre_store, seasons=(Season.summer, Season.fall)),), + Seed.radish: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.poppy: (ShopSource(money_price=100, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.spangle: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.hops: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.corn: (ShopSource(money_price=150, shop_region=Region.pierre_store, seasons=(Season.summer, Season.fall)),), + Seed.sunflower: (ShopSource(money_price=200, shop_region=Region.pierre_store, seasons=(Season.summer, Season.fall)),), + Seed.red_cabbage: (ShopSource(money_price=100, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + + Seed.eggplant: (ShopSource(money_price=20, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.pumpkin: (ShopSource(money_price=100, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.bok_choy: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.yam: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.cranberry: (ShopSource(money_price=240, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.fairy: (ShopSource(money_price=200, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.amaranth: (ShopSource(money_price=70, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.grape: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.artichoke: (ShopSource(money_price=30, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + + Seed.broccoli: (ShopSource(items_price=((5, Material.moss),), shop_region=LogicRegion.raccoon_shop),), + Seed.carrot: (ShopSource(items_price=((1, TreeSeed.maple),), shop_region=LogicRegion.raccoon_shop),), + Seed.powdermelon: (ShopSource(items_price=((2, TreeSeed.acorn),), shop_region=LogicRegion.raccoon_shop),), + Seed.summer_squash: (ShopSource(items_price=((15, Material.sap),), shop_region=LogicRegion.raccoon_shop),), + + Seed.strawberry: (ShopSource(money_price=100, shop_region=LogicRegion.egg_festival, seasons=(Season.spring,)),), + Seed.rare_seed: (ShopSource(money_price=1000, shop_region=LogicRegion.traveling_cart, seasons=(Season.spring, Season.summer)),), + + # Saloon + Beverage.beer: (ShopSource(money_price=400, shop_region=Region.saloon),), + Meal.salad: (ShopSource(money_price=220, shop_region=Region.saloon),), + Meal.bread: (ShopSource(money_price=100, shop_region=Region.saloon),), + Meal.spaghetti: (ShopSource(money_price=240, shop_region=Region.saloon),), + Meal.pizza: (ShopSource(money_price=600, shop_region=Region.saloon),), + Beverage.coffee: (ShopSource(money_price=300, shop_region=Region.saloon),), + + # Books + Book.animal_catalogue: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=5000, shop_region=Region.ranch),), + Book.book_of_mysteries: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + MysteryBoxSource(amount=38),), # After 38 boxes, there are 49.99% chances player received the book. + Book.dwarvish_safety_manual: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=4000, shop_region=LogicRegion.mines_dwarf_shop), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.friendship_101: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + PrizeMachineSource(amount=9), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.horse_the_book: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=25000, shop_region=LogicRegion.bookseller_2),), + Book.jack_be_nimble_jack_be_thick: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.jewels_of_the_sea: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + FishingTreasureChestSource(amount=21), # After 21 chests, there are 49.44% chances player received the book. + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.mapping_cave_systems: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + GenericSource(regions=Region.adventurer_guild_bedroom), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.monster_compendium: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + CustomRuleSource(create_rule=lambda logic: logic.monster.can_kill_many(Generic.any)), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.ol_slitherlegs: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=25000, shop_region=LogicRegion.bookseller_2),), + Book.price_catalogue: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=3000, shop_region=LogicRegion.bookseller_2),), + Book.the_alleyway_buffet: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + GenericSource(regions=Region.town, + other_requirements=(ToolRequirement(Tool.axe, ToolMaterial.iron), ToolRequirement(Tool.pickaxe, ToolMaterial.iron))), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.the_art_o_crabbing: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + GenericSource(regions=Region.beach, + other_requirements=(ToolRequirement(Tool.fishing_rod, ToolMaterial.iridium), + SkillRequirement(Skill.fishing, 6), + SeasonRequirement(Season.winter))), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.treasure_appraisal_guide: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ArtifactTroveSource(amount=18), # After 18 troves, there is 49,88% chances player received the book. + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.raccoon_journal: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3), + ShopSource(items_price=((999, Material.fiber),), shop_region=LogicRegion.raccoon_shop),), + Book.way_of_the_wind_pt_1: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=15000, shop_region=LogicRegion.bookseller_2),), + Book.way_of_the_wind_pt_2: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=35000, shop_region=LogicRegion.bookseller_2, other_requirements=(BookRequirement(Book.way_of_the_wind_pt_1),)),), + Book.woodys_secret: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + + # Experience Books + Book.book_of_stars: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.bait_and_bobber: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.combat_quarterly: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.mining_monthly: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.stardew_valley_almanac: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.woodcutters_weekly: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.queen_of_sauce_cookbook: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=50000, shop_region=LogicRegion.bookseller_2),), # Worst book ever + }, + fishes=( + fish_data.albacore, + fish_data.anchovy, + fish_data.bream, + fish_data.bullhead, + fish_data.carp, + fish_data.catfish, + fish_data.chub, + fish_data.dorado, + fish_data.eel, + fish_data.flounder, + fish_data.goby, + fish_data.halibut, + fish_data.herring, + fish_data.largemouth_bass, + fish_data.lingcod, + fish_data.midnight_carp, # Ginger island override + fish_data.octopus, + fish_data.perch, + fish_data.pike, + fish_data.pufferfish, # Ginger island override + fish_data.rainbow_trout, + fish_data.red_mullet, + fish_data.red_snapper, + fish_data.salmon, + fish_data.sardine, + fish_data.sea_cucumber, + fish_data.shad, + fish_data.slimejack, + fish_data.smallmouth_bass, + fish_data.squid, + fish_data.sturgeon, + fish_data.sunfish, + fish_data.super_cucumber, # Ginger island override + fish_data.tiger_trout, + fish_data.tilapia, # Ginger island override + fish_data.tuna, # Ginger island override + fish_data.void_salmon, + fish_data.walleye, + fish_data.woodskip, + fish_data.blobfish, + fish_data.midnight_squid, + fish_data.spook_fish, + + # Legendaries + fish_data.angler, + fish_data.crimsonfish, + fish_data.glacierfish, + fish_data.legend, + fish_data.mutant_carp, + + # Crab pot + fish_data.clam, + fish_data.cockle, + fish_data.crab, + fish_data.crayfish, + fish_data.lobster, + fish_data.mussel, + fish_data.oyster, + fish_data.periwinkle, + fish_data.shrimp, + fish_data.snail, + ), + villagers=( + villagers_data.josh, + villagers_data.elliott, + villagers_data.harvey, + villagers_data.sam, + villagers_data.sebastian, + villagers_data.shane, + villagers_data.abigail, + villagers_data.emily, + villagers_data.haley, + villagers_data.leah, + villagers_data.maru, + villagers_data.penny, + villagers_data.caroline, + villagers_data.clint, + villagers_data.demetrius, + villagers_data.evelyn, + villagers_data.george, + villagers_data.gus, + villagers_data.jas, + villagers_data.jodi, + villagers_data.kent, + villagers_data.krobus, + villagers_data.lewis, + villagers_data.linus, + villagers_data.marnie, + villagers_data.pam, + villagers_data.pierre, + villagers_data.robin, + villagers_data.vincent, + villagers_data.willy, + villagers_data.wizard, + ) +) diff --git a/worlds/stardew_valley/content/vanilla/qi_board.py b/worlds/stardew_valley/content/vanilla/qi_board.py new file mode 100644 index 000000000000..d859d3b16ff7 --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/qi_board.py @@ -0,0 +1,36 @@ +from .ginger_island import ginger_island_content_pack as ginger_island_content_pack +from .pelican_town import pelican_town as pelican_town_content_pack +from ..game_content import ContentPack, StardewContent +from ...data import fish_data +from ...data.game_item import GenericSource, ItemTag +from ...data.harvest import HarvestCropSource +from ...strings.crop_names import Fruit +from ...strings.region_names import Region +from ...strings.season_names import Season +from ...strings.seed_names import Seed + + +class QiBoardContentPack(ContentPack): + def harvest_source_hook(self, content: StardewContent): + content.untag_item(Seed.qi_bean, ItemTag.CROPSANITY_SEED) + + +qi_board_content_pack = QiBoardContentPack( + "Qi Board (Vanilla)", + dependencies=( + pelican_town_content_pack.name, + ginger_island_content_pack.name, + ), + harvest_sources={ + # This one is a bit special, because it's only available during the special order, but it can be found from like, everywhere. + Seed.qi_bean: (GenericSource(regions=(Region.qi_walnut_room,)),), + Fruit.qi_fruit: (HarvestCropSource(seed=Seed.qi_bean),), + }, + fishes=( + fish_data.ms_angler, + fish_data.son_of_crimsonfish, + fish_data.glacierfish_jr, + fish_data.legend_ii, + fish_data.radioactive_carp, + ) +) diff --git a/worlds/stardew_valley/content/vanilla/the_desert.py b/worlds/stardew_valley/content/vanilla/the_desert.py new file mode 100644 index 000000000000..a207e169ca46 --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/the_desert.py @@ -0,0 +1,46 @@ +from .pelican_town import pelican_town as pelican_town_content_pack +from ..game_content import ContentPack +from ...data import fish_data, villagers_data +from ...data.harvest import ForagingSource, HarvestCropSource +from ...data.shop import ShopSource +from ...strings.crop_names import Fruit, Vegetable +from ...strings.forageable_names import Forageable, Mushroom +from ...strings.region_names import Region +from ...strings.season_names import Season +from ...strings.seed_names import Seed + +the_desert = ContentPack( + "The Desert (Vanilla)", + dependencies=( + pelican_town_content_pack.name, + ), + harvest_sources={ + Forageable.cactus_fruit: ( + ForagingSource(regions=(Region.desert,)), + HarvestCropSource(seed=Seed.cactus, seasons=()) + ), + Forageable.coconut: ( + ForagingSource(regions=(Region.desert,)), + ), + Mushroom.purple: ( + ForagingSource(regions=(Region.skull_cavern_25,)), + ), + + Fruit.rhubarb: (HarvestCropSource(seed=Seed.rhubarb, seasons=(Season.spring,)),), + Fruit.starfruit: (HarvestCropSource(seed=Seed.starfruit, seasons=(Season.summer,)),), + Vegetable.beet: (HarvestCropSource(seed=Seed.beet, seasons=(Season.fall,)),), + }, + shop_sources={ + Seed.cactus: (ShopSource(money_price=150, shop_region=Region.oasis),), + Seed.rhubarb: (ShopSource(money_price=100, shop_region=Region.oasis, seasons=(Season.spring,)),), + Seed.starfruit: (ShopSource(money_price=400, shop_region=Region.oasis, seasons=(Season.summer,)),), + Seed.beet: (ShopSource(money_price=20, shop_region=Region.oasis, seasons=(Season.fall,)),), + }, + fishes=( + fish_data.sandfish, + fish_data.scorpion_carp, + ), + villagers=( + villagers_data.sandy, + ), +) diff --git a/worlds/stardew_valley/content/vanilla/the_farm.py b/worlds/stardew_valley/content/vanilla/the_farm.py new file mode 100644 index 000000000000..68d0bf10f6b8 --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/the_farm.py @@ -0,0 +1,43 @@ +from .pelican_town import pelican_town as pelican_town_content_pack +from ..game_content import ContentPack +from ...data.harvest import FruitBatsSource, MushroomCaveSource +from ...strings.forageable_names import Forageable, Mushroom + +the_farm = ContentPack( + "The Farm (Vanilla)", + dependencies=( + pelican_town_content_pack.name, + ), + harvest_sources={ + # Fruit cave + Forageable.blackberry: ( + FruitBatsSource(), + ), + Forageable.salmonberry: ( + FruitBatsSource(), + ), + Forageable.spice_berry: ( + FruitBatsSource(), + ), + Forageable.wild_plum: ( + FruitBatsSource(), + ), + + # Mushrooms + Mushroom.common: ( + MushroomCaveSource(), + ), + Mushroom.chanterelle: ( + MushroomCaveSource(), + ), + Mushroom.morel: ( + MushroomCaveSource(), + ), + Mushroom.purple: ( + MushroomCaveSource(), + ), + Mushroom.red: ( + MushroomCaveSource(), + ), + } +) diff --git a/worlds/stardew_valley/content/vanilla/the_mines.py b/worlds/stardew_valley/content/vanilla/the_mines.py new file mode 100644 index 000000000000..729b195f7b06 --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/the_mines.py @@ -0,0 +1,35 @@ +from .pelican_town import pelican_town as pelican_town_content_pack +from ..game_content import ContentPack +from ...data import fish_data, villagers_data +from ...data.harvest import ForagingSource +from ...data.requirement import ToolRequirement +from ...strings.forageable_names import Forageable, Mushroom +from ...strings.region_names import Region +from ...strings.tool_names import Tool + +the_mines = ContentPack( + "The Mines (Vanilla)", + dependencies=( + pelican_town_content_pack.name, + ), + harvest_sources={ + Forageable.cave_carrot: ( + ForagingSource(regions=(Region.mines_floor_10,), other_requirements=(ToolRequirement(Tool.hoe),)), + ), + Mushroom.red: ( + ForagingSource(regions=(Region.mines_floor_95,)), + ), + Mushroom.purple: ( + ForagingSource(regions=(Region.mines_floor_95,)), + ) + }, + fishes=( + fish_data.ghostfish, + fish_data.ice_pip, + fish_data.lava_eel, + fish_data.stonefish, + ), + villagers=( + villagers_data.dwarf, + ), +) diff --git a/worlds/stardew_valley/data/__init__.py b/worlds/stardew_valley/data/__init__.py index d14d9cfb8e97..e69de29bb2d1 100644 --- a/worlds/stardew_valley/data/__init__.py +++ b/worlds/stardew_valley/data/__init__.py @@ -1,2 +0,0 @@ -from .crops_data import CropItem, SeedItem, all_crops, all_purchasable_seeds -from .fish_data import FishItem, all_fish diff --git a/worlds/stardew_valley/data/artisan.py b/worlds/stardew_valley/data/artisan.py new file mode 100644 index 000000000000..593ab6a3ddf0 --- /dev/null +++ b/worlds/stardew_valley/data/artisan.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from .game_item import kw_only, ItemSource + + +@dataclass(frozen=True, **kw_only) +class MachineSource(ItemSource): + item: str # this should be optional (worm bin) + machine: str + # seasons diff --git a/worlds/stardew_valley/data/bundle_data.py b/worlds/stardew_valley/data/bundle_data.py index 7e7a08c16b37..8b2e189c796e 100644 --- a/worlds/stardew_valley/data/bundle_data.py +++ b/worlds/stardew_valley/data/bundle_data.py @@ -1,17 +1,19 @@ from ..bundles.bundle import BundleTemplate, IslandBundleTemplate, DeepBundleTemplate, CurrencyBundleTemplate, MoneyBundleTemplate, FestivalBundleTemplate from ..bundles.bundle_item import BundleItem from ..bundles.bundle_room import BundleRoomTemplate +from ..content import content_packs +from ..content.vanilla.base import all_fruits, all_vegetables, all_edible_mushrooms from ..strings.animal_product_names import AnimalProduct from ..strings.artisan_good_names import ArtisanGood from ..strings.bundle_names import CCRoom, BundleName -from ..strings.craftable_names import Fishing, Craftable, Bomb +from ..strings.craftable_names import Fishing, Craftable, Bomb, Consumable, Lighting from ..strings.crop_names import Fruit, Vegetable from ..strings.currency_names import Currency from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro -from ..strings.fish_names import Fish, WaterItem, Trash +from ..strings.fish_names import Fish, WaterItem, Trash, all_fish from ..strings.flower_names import Flower from ..strings.food_names import Beverage, Meal -from ..strings.forageable_names import Forageable +from ..strings.forageable_names import Forageable, Mushroom from ..strings.geode_names import Geode from ..strings.gift_names import Gift from ..strings.ingredient_names import Ingredient @@ -19,27 +21,27 @@ from ..strings.metal_names import MetalBar, Artifact, Fossil, Ore, Mineral from ..strings.monster_drop_names import Loot from ..strings.quality_names import ForageQuality, ArtisanQuality, FishQuality -from ..strings.seed_names import Seed +from ..strings.seed_names import Seed, TreeSeed wild_horseradish = BundleItem(Forageable.wild_horseradish) daffodil = BundleItem(Forageable.daffodil) leek = BundleItem(Forageable.leek) dandelion = BundleItem(Forageable.dandelion) -morel = BundleItem(Forageable.morel) -common_mushroom = BundleItem(Forageable.common_mushroom) +morel = BundleItem(Mushroom.morel) +common_mushroom = BundleItem(Mushroom.common) salmonberry = BundleItem(Forageable.salmonberry) spring_onion = BundleItem(Forageable.spring_onion) grape = BundleItem(Fruit.grape) spice_berry = BundleItem(Forageable.spice_berry) sweet_pea = BundleItem(Forageable.sweet_pea) -red_mushroom = BundleItem(Forageable.red_mushroom) +red_mushroom = BundleItem(Mushroom.red) fiddlehead_fern = BundleItem(Forageable.fiddlehead_fern) wild_plum = BundleItem(Forageable.wild_plum) hazelnut = BundleItem(Forageable.hazelnut) blackberry = BundleItem(Forageable.blackberry) -chanterelle = BundleItem(Forageable.chanterelle) +chanterelle = BundleItem(Mushroom.chanterelle) winter_root = BundleItem(Forageable.winter_root) crystal_fruit = BundleItem(Forageable.crystal_fruit) @@ -50,7 +52,7 @@ coconut = BundleItem(Forageable.coconut) cactus_fruit = BundleItem(Forageable.cactus_fruit) cave_carrot = BundleItem(Forageable.cave_carrot) -purple_mushroom = BundleItem(Forageable.purple_mushroom) +purple_mushroom = BundleItem(Mushroom.purple) maple_syrup = BundleItem(ArtisanGood.maple_syrup) oak_resin = BundleItem(ArtisanGood.oak_resin) pine_tar = BundleItem(ArtisanGood.pine_tar) @@ -62,13 +64,25 @@ cockle = BundleItem(Fish.cockle) mussel = BundleItem(Fish.mussel) oyster = BundleItem(Fish.oyster) -seaweed = BundleItem(WaterItem.seaweed) +seaweed = BundleItem(WaterItem.seaweed, can_have_quality=False) wood = BundleItem(Material.wood, 99) stone = BundleItem(Material.stone, 99) hardwood = BundleItem(Material.hardwood, 10) clay = BundleItem(Material.clay, 10) fiber = BundleItem(Material.fiber, 99) +moss = BundleItem(Material.moss, 10) + +mixed_seeds = BundleItem(Seed.mixed) +acorn = BundleItem(TreeSeed.acorn) +maple_seed = BundleItem(TreeSeed.maple) +pine_cone = BundleItem(TreeSeed.pine) +mahogany_seed = BundleItem(TreeSeed.mahogany) +mushroom_tree_seed = BundleItem(TreeSeed.mushroom, source=BundleItem.Sources.island) +mystic_tree_seed = BundleItem(TreeSeed.mystic, source=BundleItem.Sources.masteries) +mossy_seed = BundleItem(TreeSeed.mossy) + +strawberry_seeds = BundleItem(Seed.strawberry) blue_jazz = BundleItem(Flower.blue_jazz) cauliflower = BundleItem(Vegetable.cauliflower) @@ -106,8 +120,13 @@ red_cabbage = BundleItem(Vegetable.red_cabbage) starfruit = BundleItem(Fruit.starfruit) artichoke = BundleItem(Vegetable.artichoke) -pineapple = BundleItem(Fruit.pineapple, source=BundleItem.Sources.island) -taro_root = BundleItem(Vegetable.taro_root, source=BundleItem.Sources.island, ) +pineapple = BundleItem(Fruit.pineapple, source=BundleItem.Sources.content) +taro_root = BundleItem(Vegetable.taro_root, source=BundleItem.Sources.content) + +carrot = BundleItem(Vegetable.carrot) +summer_squash = BundleItem(Vegetable.summer_squash) +broccoli = BundleItem(Vegetable.broccoli) +powdermelon = BundleItem(Fruit.powdermelon) egg = BundleItem(AnimalProduct.egg) large_egg = BundleItem(AnimalProduct.large_egg) @@ -151,8 +170,8 @@ peach = BundleItem(Fruit.peach) pomegranate = BundleItem(Fruit.pomegranate) cherry = BundleItem(Fruit.cherry) -banana = BundleItem(Fruit.banana, source=BundleItem.Sources.island) -mango = BundleItem(Fruit.mango, source=BundleItem.Sources.island) +banana = BundleItem(Fruit.banana, source=BundleItem.Sources.content) +mango = BundleItem(Fruit.mango, source=BundleItem.Sources.content) basic_fertilizer = BundleItem(Fertilizer.basic, 100) quality_fertilizer = BundleItem(Fertilizer.quality, 20) @@ -300,6 +319,13 @@ rhubarb_pie = BundleItem(Meal.rhubarb_pie) shrimp_cocktail = BundleItem(Meal.shrimp_cocktail) pina_colada = BundleItem(Beverage.pina_colada, source=BundleItem.Sources.island) +stuffing = BundleItem(Meal.stuffing) +magic_rock_candy = BundleItem(Meal.magic_rock_candy) +spicy_eel = BundleItem(Meal.spicy_eel) +crab_cakes = BundleItem(Meal.crab_cakes) +eggplant_parmesan = BundleItem(Meal.eggplant_parmesan) +pumpkin_soup = BundleItem(Meal.pumpkin_soup) +lucky_lunch = BundleItem(Meal.lucky_lunch) green_algae = BundleItem(WaterItem.green_algae) white_algae = BundleItem(WaterItem.white_algae) @@ -370,6 +396,7 @@ spinner = BundleItem(Fishing.spinner) dressed_spinner = BundleItem(Fishing.dressed_spinner) trap_bobber = BundleItem(Fishing.trap_bobber) +sonar_bobber = BundleItem(Fishing.sonar_bobber) cork_bobber = BundleItem(Fishing.cork_bobber) lead_bobber = BundleItem(Fishing.lead_bobber) treasure_hunter = BundleItem(Fishing.treasure_hunter) @@ -377,18 +404,67 @@ curiosity_lure = BundleItem(Fishing.curiosity_lure) quality_bobber = BundleItem(Fishing.quality_bobber) bait = BundleItem(Fishing.bait, 100) +deluxe_bait = BundleItem(Fishing.deluxe_bait, 50) magnet = BundleItem(Fishing.magnet) -wild_bait = BundleItem(Fishing.wild_bait, 10) -magic_bait = BundleItem(Fishing.magic_bait, 5, source=BundleItem.Sources.island) +wild_bait = BundleItem(Fishing.wild_bait, 20) +magic_bait = BundleItem(Fishing.magic_bait, 10, source=BundleItem.Sources.island) pearl = BundleItem(Gift.pearl) +challenge_bait = BundleItem(Fishing.challenge_bait, 25, source=BundleItem.Sources.masteries) +targeted_bait = BundleItem(ArtisanGood.targeted_bait, 25, source=BundleItem.Sources.content) -ginger = BundleItem(Forageable.ginger, source=BundleItem.Sources.island) -magma_cap = BundleItem(Forageable.magma_cap, source=BundleItem.Sources.island) +ginger = BundleItem(Forageable.ginger, source=BundleItem.Sources.content) +magma_cap = BundleItem(Mushroom.magma_cap, source=BundleItem.Sources.content) wheat_flour = BundleItem(Ingredient.wheat_flour) sugar = BundleItem(Ingredient.sugar) vinegar = BundleItem(Ingredient.vinegar) +jack_o_lantern = BundleItem(Lighting.jack_o_lantern) +prize_ticket = BundleItem(Currency.prize_ticket) +mystery_box = BundleItem(Consumable.mystery_box) +gold_mystery_box = BundleItem(Consumable.gold_mystery_box, source=BundleItem.Sources.masteries) +calico_egg = BundleItem(Currency.calico_egg) + +raccoon_crab_pot_fish_items = [periwinkle.as_amount(5), snail.as_amount(5), crayfish.as_amount(5), mussel.as_amount(5), + oyster.as_amount(5), cockle.as_amount(5), clam.as_amount(5)] +raccoon_smoked_fish_items = [BundleItem(ArtisanGood.smoked_fish, flavor=fish) for fish in + [Fish.largemouth_bass, Fish.bream, Fish.bullhead, Fish.chub, Fish.ghostfish, Fish.flounder, Fish.shad, + Fish.rainbow_trout, Fish.tilapia, Fish.red_mullet, Fish.tuna, Fish.midnight_carp, Fish.salmon, Fish.perch]] +raccoon_fish_items_flat = [*raccoon_crab_pot_fish_items, *raccoon_smoked_fish_items] +raccoon_fish_items_deep = [raccoon_crab_pot_fish_items, raccoon_smoked_fish_items] +raccoon_fish_bundle_vanilla = DeepBundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_fish, raccoon_fish_items_deep, 2, 2) +raccoon_fish_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_fish, raccoon_fish_items_flat, 3, 2) + +all_specific_jellies = [BundleItem(ArtisanGood.jelly, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits] +all_specific_pickles = [BundleItem(ArtisanGood.pickles, flavor=vegetable, source=BundleItem.Sources.content) for vegetable in all_vegetables] +all_specific_dried_fruits = [*[BundleItem(ArtisanGood.dried_fruit, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits], + BundleItem(ArtisanGood.raisins, source=BundleItem.Sources.content)] +all_specific_juices = [BundleItem(ArtisanGood.juice, flavor=vegetable, source=BundleItem.Sources.content) for vegetable in all_vegetables] +raccoon_artisan_items = [*all_specific_jellies, *all_specific_pickles, *all_specific_dried_fruits, *all_specific_juices] +raccoon_artisan_bundle_vanilla = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_artisan, raccoon_artisan_items, 2, 2) +raccoon_artisan_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_artisan, raccoon_artisan_items, 3, 2) + +all_specific_dried_mushrooms = [BundleItem(ArtisanGood.dried_mushroom, flavor=mushroom, source=BundleItem.Sources.content) for mushroom in all_edible_mushrooms] +raccoon_food_items = [egg.as_amount(5), cave_carrot.as_amount(5), white_algae.as_amount(5)] +raccoon_food_items_vanilla = [all_specific_dried_mushrooms, raccoon_food_items] +raccoon_food_items_thematic = [*all_specific_dried_mushrooms, *raccoon_food_items, brown_egg.as_amount(5), large_egg.as_amount(2), large_brown_egg.as_amount(2), + green_algae.as_amount(10)] +raccoon_food_bundle_vanilla = DeepBundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_food, raccoon_food_items_vanilla, 2, 2) +raccoon_food_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_food, raccoon_food_items_thematic, 3, 2) + +raccoon_foraging_items = [moss, rusty_spoon, trash.as_amount(5), slime.as_amount(99), bat_wing.as_amount(10), geode.as_amount(8), + frozen_geode.as_amount(5), magma_geode.as_amount(3), coral.as_amount(4), sea_urchin.as_amount(2), bug_meat.as_amount(10), + diamond, topaz.as_amount(3), ghostfish.as_amount(3)] +raccoon_foraging_bundle_vanilla = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_foraging, raccoon_foraging_items, 2, 2) +raccoon_foraging_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_foraging, raccoon_foraging_items, 3, 2) + +raccoon_bundles_vanilla = [raccoon_fish_bundle_vanilla, raccoon_artisan_bundle_vanilla, raccoon_food_bundle_vanilla, raccoon_foraging_bundle_vanilla] +raccoon_bundles_thematic = [raccoon_fish_bundle_thematic, raccoon_artisan_bundle_thematic, raccoon_food_bundle_thematic, raccoon_foraging_bundle_thematic] +raccoon_bundles_remixed = raccoon_bundles_thematic +raccoon_vanilla = BundleRoomTemplate(CCRoom.raccoon_requests, raccoon_bundles_vanilla, 8) +raccoon_thematic = BundleRoomTemplate(CCRoom.raccoon_requests, raccoon_bundles_thematic, 8) +raccoon_remixed = BundleRoomTemplate(CCRoom.raccoon_requests, raccoon_bundles_remixed, 8) + # Crafts Room spring_foraging_items_vanilla = [wild_horseradish, daffodil, leek, dandelion] spring_foraging_items_thematic = [*spring_foraging_items_vanilla, spring_onion, salmonberry, morel] @@ -436,42 +512,50 @@ sticky_items = [sap.as_amount(500), sap.as_amount(500)] sticky_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.sticky, sticky_items, 1, 1) +forest_items = [moss, fiber.as_amount(200), acorn.as_amount(10), maple_seed.as_amount(10), pine_cone.as_amount(10), mahogany_seed, + mushroom_tree_seed, mossy_seed.as_amount(5), mystic_tree_seed] +forest_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.forest, forest_items, 4, 2) + wild_medicine_items = [item.as_amount(5) for item in [purple_mushroom, fiddlehead_fern, white_algae, hops, blackberry, dandelion]] wild_medicine_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.wild_medicine, wild_medicine_items, 4, 3) -quality_foraging_items = sorted({item.as_quality(ForageQuality.gold).as_amount(1) +quality_foraging_items = sorted({item.as_quality(ForageQuality.gold).as_amount(3) for item in [*spring_foraging_items_thematic, *summer_foraging_items_thematic, *fall_foraging_items_thematic, - *winter_foraging_items_thematic, *beach_foraging_items, *desert_foraging_items, magma_cap]}) + *winter_foraging_items_thematic, *beach_foraging_items, *desert_foraging_items, magma_cap] if item.can_have_quality}) quality_foraging_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.quality_foraging, quality_foraging_items, 4, 3) +green_rain_items = [moss.as_amount(200), fiber.as_amount(200), mossy_seed.as_amount(20), fiddlehead_fern.as_amount(10)] +green_rain_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.green_rain, green_rain_items, 4, 3) + crafts_room_bundles_vanilla = [spring_foraging_bundle_vanilla, summer_foraging_bundle_vanilla, fall_foraging_bundle_vanilla, winter_foraging_bundle_vanilla, construction_bundle_vanilla, exotic_foraging_bundle_vanilla] crafts_room_bundles_thematic = [spring_foraging_bundle_thematic, summer_foraging_bundle_thematic, fall_foraging_bundle_thematic, winter_foraging_bundle_thematic, construction_bundle_thematic, exotic_foraging_bundle_thematic] crafts_room_bundles_remixed = [*crafts_room_bundles_thematic, beach_foraging_bundle, mines_foraging_bundle, desert_foraging_bundle, - island_foraging_bundle, sticky_bundle, wild_medicine_bundle, quality_foraging_bundle] + island_foraging_bundle, sticky_bundle, forest_bundle, wild_medicine_bundle, quality_foraging_bundle, green_rain_bundle] crafts_room_vanilla = BundleRoomTemplate(CCRoom.crafts_room, crafts_room_bundles_vanilla, 6) crafts_room_thematic = BundleRoomTemplate(CCRoom.crafts_room, crafts_room_bundles_thematic, 6) crafts_room_remixed = BundleRoomTemplate(CCRoom.crafts_room, crafts_room_bundles_remixed, 6) # Pantry spring_crops_items_vanilla = [parsnip, green_bean, cauliflower, potato] -spring_crops_items_thematic = [*spring_crops_items_vanilla, blue_jazz, coffee_bean, garlic, kale, rhubarb, strawberry, tulip, unmilled_rice] +spring_crops_items_thematic = [*spring_crops_items_vanilla, blue_jazz, coffee_bean, garlic, kale, rhubarb, strawberry, tulip, unmilled_rice, carrot] spring_crops_bundle_vanilla = BundleTemplate(CCRoom.pantry, BundleName.spring_crops, spring_crops_items_vanilla, 4, 4) spring_crops_bundle_thematic = BundleTemplate.extend_from(spring_crops_bundle_vanilla, spring_crops_items_thematic) summer_crops_items_vanilla = [tomato, hot_pepper, blueberry, melon] -summer_crops_items_thematic = [*summer_crops_items_vanilla, corn, hops, poppy, radish, red_cabbage, starfruit, summer_spangle, sunflower, wheat] +summer_crops_items_thematic = [*summer_crops_items_vanilla, corn, hops, poppy, radish, red_cabbage, starfruit, summer_spangle, sunflower, wheat, summer_squash] summer_crops_bundle_vanilla = BundleTemplate(CCRoom.pantry, BundleName.summer_crops, summer_crops_items_vanilla, 4, 4) summer_crops_bundle_thematic = BundleTemplate.extend_from(summer_crops_bundle_vanilla, summer_crops_items_thematic) fall_crops_items_vanilla = [corn, eggplant, pumpkin, yam] -fall_crops_items_thematic = [*fall_crops_items_vanilla, amaranth, artichoke, beet, bok_choy, cranberries, fairy_rose, grape, sunflower, wheat, sweet_gem_berry] +fall_crops_items_thematic = [*fall_crops_items_vanilla, amaranth, artichoke, beet, bok_choy, cranberries, fairy_rose, grape, + sunflower, wheat, sweet_gem_berry, broccoli] fall_crops_bundle_vanilla = BundleTemplate(CCRoom.pantry, BundleName.fall_crops, fall_crops_items_vanilla, 4, 4) fall_crops_bundle_thematic = BundleTemplate.extend_from(fall_crops_bundle_vanilla, fall_crops_items_thematic) -all_crops_items = sorted({*spring_crops_items_thematic, *summer_crops_items_thematic, *fall_crops_items_thematic}) +all_crops_items = sorted({*spring_crops_items_thematic, *summer_crops_items_thematic, *fall_crops_items_thematic, powdermelon}) quality_crops_items_vanilla = [item.as_quality_crop() for item in [parsnip, melon, pumpkin, corn]] quality_crops_items_thematic = [item.as_quality_crop() for item in all_crops_items] @@ -492,7 +576,8 @@ rare_crops_items = [ancient_fruit, sweet_gem_berry] rare_crops_bundle = BundleTemplate(CCRoom.pantry, BundleName.rare_crops, rare_crops_items, 2, 2) -fish_farmer_items = [roe.as_amount(15), aged_roe.as_amount(15), squid_ink] +# all_specific_roes = [BundleItem(AnimalProduct.roe, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fish] +fish_farmer_items = [roe.as_amount(15), aged_roe.as_amount(5), squid_ink, caviar.as_amount(5)] fish_farmer_bundle = BundleTemplate(CCRoom.pantry, BundleName.fish_farmer, fish_farmer_items, 3, 2) garden_items = [tulip, blue_jazz, summer_spangle, sunflower, fairy_rose, poppy, bouquet] @@ -516,12 +601,20 @@ purple_slime_egg, green_slime_egg, tiger_slime_egg] slime_farmer_bundle = BundleTemplate(CCRoom.pantry, BundleName.slime_farmer, slime_farmer_items, 4, 3) +sommelier_items = [BundleItem(ArtisanGood.wine, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits] +sommelier_bundle = BundleTemplate(CCRoom.pantry, BundleName.sommelier, sommelier_items, 6, 3) + +dry_items = [*[BundleItem(ArtisanGood.dried_fruit, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits], + *[BundleItem(ArtisanGood.dried_mushroom, flavor=mushroom, source=BundleItem.Sources.content) for mushroom in all_edible_mushrooms], + BundleItem(ArtisanGood.raisins, source=BundleItem.Sources.content)] +dry_bundle = BundleTemplate(CCRoom.pantry, BundleName.dry, dry_items, 6, 3) + pantry_bundles_vanilla = [spring_crops_bundle_vanilla, summer_crops_bundle_vanilla, fall_crops_bundle_vanilla, quality_crops_bundle_vanilla, animal_bundle_vanilla, artisan_bundle_vanilla] pantry_bundles_thematic = [spring_crops_bundle_thematic, summer_crops_bundle_thematic, fall_crops_bundle_thematic, quality_crops_bundle_thematic, animal_bundle_thematic, artisan_bundle_thematic] pantry_bundles_remixed = [*pantry_bundles_thematic, rare_crops_bundle, fish_farmer_bundle, garden_bundle, - brewer_bundle, orchard_bundle, island_crops_bundle, agronomist_bundle, slime_farmer_bundle] + brewer_bundle, orchard_bundle, island_crops_bundle, agronomist_bundle, slime_farmer_bundle, sommelier_bundle, dry_bundle] pantry_vanilla = BundleRoomTemplate(CCRoom.pantry, pantry_bundles_vanilla, 6) pantry_thematic = BundleRoomTemplate(CCRoom.pantry, pantry_bundles_thematic, 6) pantry_remixed = BundleRoomTemplate(CCRoom.pantry, pantry_bundles_remixed, 6) @@ -579,8 +672,11 @@ rain_fish_items = [red_snapper, shad, catfish, eel, walleye] rain_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.rain_fish, rain_fish_items, 3, 3) -quality_fish_items = sorted({item.as_quality(FishQuality.gold) for item in [*river_fish_items_thematic, *lake_fish_items_thematic, *ocean_fish_items_thematic]}) -quality_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.quality_fish, quality_fish_items, 4, 4) +quality_fish_items = sorted({ + item.as_quality(FishQuality.gold).as_amount(2) + for item in [*river_fish_items_thematic, *lake_fish_items_thematic, *ocean_fish_items_thematic] +}) +quality_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.quality_fish, quality_fish_items, 4, 3) master_fisher_items = [lava_eel, scorpion_carp, octopus, blobfish, lingcod, ice_pip, super_cucumber, stingray, void_salmon, pufferfish] master_fisher_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.master_fisher, master_fisher_items, 4, 2) @@ -591,21 +687,31 @@ island_fish_items = [lionfish, blue_discus, stingray] island_fish_bundle = IslandBundleTemplate(CCRoom.fish_tank, BundleName.island_fish, island_fish_items, 3, 3) -tackle_items = [spinner, dressed_spinner, trap_bobber, cork_bobber, lead_bobber, treasure_hunter, barbed_hook, curiosity_lure, quality_bobber] -tackle_bundle = IslandBundleTemplate(CCRoom.fish_tank, BundleName.tackle, tackle_items, 3, 2) +tackle_items = [spinner, dressed_spinner, trap_bobber, sonar_bobber, cork_bobber, lead_bobber, treasure_hunter, barbed_hook, curiosity_lure, quality_bobber] +tackle_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.tackle, tackle_items, 3, 2) -bait_items = [bait, magnet, wild_bait, magic_bait] -bait_bundle = IslandBundleTemplate(CCRoom.fish_tank, BundleName.bait, bait_items, 2, 2) +bait_items = [bait, magnet, wild_bait, magic_bait, challenge_bait, deluxe_bait, targeted_bait] +bait_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.bait, bait_items, 3, 2) + +# This bundle could change based on content packs, once the fish are properly in it. Until then, I'm not sure how, so pelican town only +specific_bait_items = [BundleItem(ArtisanGood.targeted_bait, flavor=fish.name).as_amount(10) for fish in content_packs.pelican_town.fishes] +specific_bait_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.specific_bait, specific_bait_items, 6, 3) deep_fishing_items = [blobfish, spook_fish, midnight_squid, sea_cucumber, super_cucumber, octopus, pearl, seaweed] deep_fishing_bundle = FestivalBundleTemplate(CCRoom.fish_tank, BundleName.deep_fishing, deep_fishing_items, 4, 3) +smokeable_fish = [Fish.largemouth_bass, Fish.bream, Fish.bullhead, Fish.chub, Fish.ghostfish, Fish.flounder, Fish.shad, Fish.rainbow_trout, Fish.tilapia, + Fish.red_mullet, Fish.tuna, Fish.midnight_carp, Fish.salmon, Fish.perch] +fish_smoker_items = [BundleItem(ArtisanGood.smoked_fish, flavor=fish) for fish in smokeable_fish] +fish_smoker_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.fish_smoker, fish_smoker_items, 6, 3) + fish_tank_bundles_vanilla = [river_fish_bundle_vanilla, lake_fish_bundle_vanilla, ocean_fish_bundle_vanilla, night_fish_bundle_vanilla, crab_pot_bundle_vanilla, specialty_fish_bundle_vanilla] fish_tank_bundles_thematic = [river_fish_bundle_thematic, lake_fish_bundle_thematic, ocean_fish_bundle_thematic, night_fish_bundle_thematic, crab_pot_bundle_thematic, specialty_fish_bundle_thematic] fish_tank_bundles_remixed = [*fish_tank_bundles_thematic, spring_fish_bundle, summer_fish_bundle, fall_fish_bundle, winter_fish_bundle, trash_bundle, - rain_fish_bundle, quality_fish_bundle, master_fisher_bundle, legendary_fish_bundle, tackle_bundle, bait_bundle] + rain_fish_bundle, quality_fish_bundle, master_fisher_bundle, legendary_fish_bundle, tackle_bundle, bait_bundle, + specific_bait_bundle, deep_fishing_bundle, fish_smoker_bundle] # In Remixed, the trash items are in the recycling bundle, so we don't use the thematic version of the crab pot bundle that added trash items to it fish_tank_bundles_remixed.remove(crab_pot_bundle_thematic) @@ -670,12 +776,12 @@ chef_bundle_thematic = BundleTemplate.extend_from(chef_bundle_vanilla, chef_items_thematic) dye_items_vanilla = [red_mushroom, sea_urchin, sunflower, duck_feather, aquamarine, red_cabbage] -dye_red_items = [cranberries, hot_pepper, radish, rhubarb, spaghetti, strawberry, tomato, tulip] +dye_red_items = [cranberries, hot_pepper, radish, rhubarb, spaghetti, strawberry, tomato, tulip, red_mushroom] dye_orange_items = [poppy, pumpkin, apricot, orange, spice_berry, winter_root] -dye_yellow_items = [corn, parsnip, summer_spangle, sunflower] -dye_green_items = [fiddlehead_fern, kale, artichoke, bok_choy, green_bean] -dye_blue_items = [blueberry, blue_jazz, blackberry, crystal_fruit] -dye_purple_items = [beet, crocus, eggplant, red_cabbage, sweet_pea] +dye_yellow_items = [corn, parsnip, summer_spangle, sunflower, starfruit] +dye_green_items = [fiddlehead_fern, kale, artichoke, bok_choy, green_bean, cactus_fruit, duck_feather, dinosaur_egg] +dye_blue_items = [blueberry, blue_jazz, blackberry, crystal_fruit, aquamarine] +dye_purple_items = [beet, crocus, eggplant, red_cabbage, sweet_pea, iridium_bar, sea_urchin, amaranth] dye_items_thematic = [dye_red_items, dye_orange_items, dye_yellow_items, dye_green_items, dye_blue_items, dye_purple_items] dye_bundle_vanilla = BundleTemplate(CCRoom.bulletin_board, BundleName.dye, dye_items_vanilla, 6, 6) dye_bundle_thematic = DeepBundleTemplate(CCRoom.bulletin_board, BundleName.dye, dye_items_thematic, 6, 6) @@ -710,12 +816,31 @@ chocolate_cake, pancakes, rhubarb_pie] home_cook_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.home_cook, home_cook_items, 3, 3) +helper_items = [prize_ticket, mystery_box.as_amount(5), gold_mystery_box] +helper_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.helper, helper_items, 2, 2) + +spirit_eve_items = [jack_o_lantern, corn.as_amount(10), bat_wing.as_amount(10)] +spirit_eve_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.spirit_eve, spirit_eve_items, 3, 3) + +winter_star_items = [holly.as_amount(5), plum_pudding, stuffing, powdermelon.as_amount(5)] +winter_star_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.winter_star, winter_star_items, 2, 2) + bartender_items = [shrimp_cocktail, triple_shot_espresso, ginger_ale, cranberry_candy, beer, pale_ale, pina_colada] bartender_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.bartender, bartender_items, 3, 3) +calico_items = [calico_egg.as_amount(200), calico_egg.as_amount(200), calico_egg.as_amount(200), calico_egg.as_amount(200), + magic_rock_candy, mega_bomb.as_amount(10), mystery_box.as_amount(10), mixed_seeds.as_amount(50), + strawberry_seeds.as_amount(20), + spicy_eel.as_amount(5), crab_cakes.as_amount(5), eggplant_parmesan.as_amount(5), + pumpkin_soup.as_amount(5), lucky_lunch.as_amount(5),] +calico_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.calico, calico_items, 2, 2) + +raccoon_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.raccoon, raccoon_foraging_items, 4, 4) + bulletin_board_bundles_vanilla = [chef_bundle_vanilla, dye_bundle_vanilla, field_research_bundle_vanilla, fodder_bundle_vanilla, enchanter_bundle_vanilla] bulletin_board_bundles_thematic = [chef_bundle_thematic, dye_bundle_thematic, field_research_bundle_thematic, fodder_bundle_thematic, enchanter_bundle_thematic] -bulletin_board_bundles_remixed = [*bulletin_board_bundles_thematic, children_bundle, forager_bundle, home_cook_bundle, bartender_bundle] +bulletin_board_bundles_remixed = [*bulletin_board_bundles_thematic, children_bundle, forager_bundle, home_cook_bundle, + helper_bundle, spirit_eve_bundle, winter_star_bundle, bartender_bundle, calico_bundle, raccoon_bundle] bulletin_board_vanilla = BundleRoomTemplate(CCRoom.bulletin_board, bulletin_board_bundles_vanilla, 5) bulletin_board_thematic = BundleRoomTemplate(CCRoom.bulletin_board, bulletin_board_bundles_thematic, 5) bulletin_board_remixed = BundleRoomTemplate(CCRoom.bulletin_board, bulletin_board_bundles_remixed, 5) @@ -738,16 +863,15 @@ abandoned_joja_mart_thematic = BundleRoomTemplate(CCRoom.abandoned_joja_mart, abandoned_joja_mart_bundles_thematic, 1) abandoned_joja_mart_remixed = abandoned_joja_mart_thematic -# Make thematic with other currencies vault_2500_gold = BundleItem.money_bundle(2500) vault_5000_gold = BundleItem.money_bundle(5000) vault_10000_gold = BundleItem.money_bundle(10000) vault_25000_gold = BundleItem.money_bundle(25000) -vault_2500_bundle = MoneyBundleTemplate(CCRoom.vault, vault_2500_gold) -vault_5000_bundle = MoneyBundleTemplate(CCRoom.vault, vault_5000_gold) -vault_10000_bundle = MoneyBundleTemplate(CCRoom.vault, vault_10000_gold) -vault_25000_bundle = MoneyBundleTemplate(CCRoom.vault, vault_25000_gold) +vault_2500_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_2500, vault_2500_gold) +vault_5000_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_5000, vault_5000_gold) +vault_10000_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_10000, vault_10000_gold) +vault_25000_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_25000, vault_25000_gold) vault_gambler_items = BundleItem(Currency.qi_coin, 10000) vault_gambler_bundle = CurrencyBundleTemplate(CCRoom.vault, BundleName.gambler, vault_gambler_items) @@ -768,9 +892,14 @@ vault_thematic = BundleRoomTemplate(CCRoom.vault, vault_bundles_thematic, 4) vault_remixed = BundleRoomTemplate(CCRoom.vault, vault_bundles_remixed, 4) +all_cc_remixed_bundles = [*crafts_room_bundles_remixed, *pantry_bundles_remixed, *fish_tank_bundles_remixed, + *boiler_room_bundles_remixed, *bulletin_board_bundles_remixed] +community_center_remixed_anywhere = BundleRoomTemplate("Community Center", all_cc_remixed_bundles, 26) + all_bundle_items_except_money = [] all_remixed_bundles = [*crafts_room_bundles_remixed, *pantry_bundles_remixed, *fish_tank_bundles_remixed, - *boiler_room_bundles_remixed, *bulletin_board_bundles_remixed, missing_bundle_thematic] + *boiler_room_bundles_remixed, *bulletin_board_bundles_remixed, missing_bundle_thematic, + *raccoon_bundles_remixed] for bundle in all_remixed_bundles: all_bundle_items_except_money.extend(bundle.items) diff --git a/worlds/stardew_valley/data/craftable_data.py b/worlds/stardew_valley/data/craftable_data.py index bfb2d25ec6b8..d83478a62051 100644 --- a/worlds/stardew_valley/data/craftable_data.py +++ b/worlds/stardew_valley/data/craftable_data.py @@ -1,25 +1,28 @@ from typing import Dict, List, Optional -from ..mods.mod_data import ModNames from .recipe_source import RecipeSource, StarterSource, QueenOfSauceSource, ShopSource, SkillSource, FriendshipSource, ShopTradeSource, CutsceneSource, \ - ArchipelagoSource, LogicSource, SpecialOrderSource, FestivalShopSource, QuestSource + ArchipelagoSource, LogicSource, SpecialOrderSource, FestivalShopSource, QuestSource, MasterySource +from ..mods.mod_data import ModNames +from ..strings.animal_product_names import AnimalProduct from ..strings.artisan_good_names import ArtisanGood -from ..strings.craftable_names import Bomb, Fence, Sprinkler, WildSeeds, Floor, Fishing, Ring, Consumable, Edible, Lighting, Storage, Furniture, Sign, Craftable, \ - ModEdible, ModCraftable, ModMachine, ModFloor, ModConsumable +from ..strings.craftable_names import Bomb, Fence, Sprinkler, WildSeeds, Floor, Fishing, Ring, Consumable, Edible, Lighting, Storage, Furniture, Sign, \ + Craftable, \ + ModEdible, ModCraftable, ModMachine, ModFloor, ModConsumable, Statue from ..strings.crop_names import Fruit, Vegetable from ..strings.currency_names import Currency from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro -from ..strings.fish_names import Fish, WaterItem +from ..strings.fish_names import Fish, WaterItem, ModTrash from ..strings.flower_names import Flower from ..strings.food_names import Meal -from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable +from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom +from ..strings.gift_names import Gift from ..strings.ingredient_names import Ingredient from ..strings.machine_names import Machine from ..strings.material_names import Material from ..strings.metal_names import Ore, MetalBar, Fossil, Artifact, Mineral, ModFossil -from ..strings.monster_drop_names import Loot +from ..strings.monster_drop_names import Loot, ModLoot from ..strings.quest_names import Quest -from ..strings.region_names import Region, SVERegion +from ..strings.region_names import Region, SVERegion, LogicRegion from ..strings.seed_names import Seed, TreeSeed from ..strings.skill_names import Skill, ModSkill from ..strings.special_order_names import SpecialOrder @@ -61,6 +64,11 @@ def skill_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int], return create_recipe(name, ingredients, source, mod_name) +def mastery_recipe(name: str, skill: str, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe: + source = MasterySource(skill) + return create_recipe(name, ingredients, source, mod_name) + + def shop_recipe(name: str, region: str, price: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe: source = ShopSource(region, price) return create_recipe(name, ingredients, source, mod_name) @@ -133,27 +141,37 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, cheese_press = skill_recipe(Machine.cheese_press, Skill.farming, 6, {Material.wood: 45, Material.stone: 45, Material.hardwood: 10, MetalBar.copper: 1}) keg = skill_recipe(Machine.keg, Skill.farming, 8, {Material.wood: 30, MetalBar.copper: 1, MetalBar.iron: 1, ArtisanGood.oak_resin: 1}) loom = skill_recipe(Machine.loom, Skill.farming, 7, {Material.wood: 60, Material.fiber: 30, ArtisanGood.pine_tar: 1}) -mayonnaise_machine = skill_recipe(Machine.mayonnaise_machine, Skill.farming, 2, {Material.wood: 15, Material.stone: 15, Mineral.earth_crystal: 10, MetalBar.copper: 1}) +mayonnaise_machine = skill_recipe(Machine.mayonnaise_machine, Skill.farming, 2, + {Material.wood: 15, Material.stone: 15, Mineral.earth_crystal: 10, MetalBar.copper: 1}) oil_maker = skill_recipe(Machine.oil_maker, Skill.farming, 8, {Loot.slime: 50, Material.hardwood: 20, MetalBar.gold: 1}) preserves_jar = skill_recipe(Machine.preserves_jar, Skill.farming, 4, {Material.wood: 50, Material.stone: 40, Material.coal: 8}) +fish_smoker = shop_recipe(Machine.fish_smoker, Region.fish_shop, 10000, + {Material.hardwood: 10, WaterItem.sea_jelly: 1, WaterItem.river_jelly: 1, WaterItem.cave_jelly: 1}) +dehydrator = shop_recipe(Machine.dehydrator, Region.pierre_store, 10000, {Material.wood: 30, Material.clay: 2, Mineral.fire_quartz: 1}) basic_fertilizer = skill_recipe(Fertilizer.basic, Skill.farming, 1, {Material.sap: 2}) -quality_fertilizer = skill_recipe(Fertilizer.quality, Skill.farming, 9, {Material.sap: 2, Fish.any: 1}) + +quality_fertilizer = skill_recipe(Fertilizer.quality, Skill.farming, 9, {Material.sap: 4, Fish.any: 1}) deluxe_fertilizer = ap_recipe(Fertilizer.deluxe, {MetalBar.iridium: 1, Material.sap: 40}) -basic_speed_gro = skill_recipe(SpeedGro.basic, Skill.farming, 3, {ArtisanGood.pine_tar: 1, Fish.clam: 1}) -deluxe_speed_gro = skill_recipe(SpeedGro.deluxe, Skill.farming, 8, {ArtisanGood.oak_resin: 1, WaterItem.coral: 1}) + +basic_speed_gro = skill_recipe(SpeedGro.basic, Skill.farming, 3, {ArtisanGood.pine_tar: 1, Material.moss: 5}) +deluxe_speed_gro = skill_recipe(SpeedGro.deluxe, Skill.farming, 8, {ArtisanGood.oak_resin: 1, Fossil.bone_fragment: 5}) hyper_speed_gro = ap_recipe(SpeedGro.hyper, {Ore.radioactive: 1, Fossil.bone_fragment: 3, Loot.solar_essence: 1}) basic_retaining_soil = skill_recipe(RetainingSoil.basic, Skill.farming, 4, {Material.stone: 2}) quality_retaining_soil = skill_recipe(RetainingSoil.quality, Skill.farming, 7, {Material.stone: 3, Material.clay: 1}) -deluxe_retaining_soil = shop_trade_recipe(RetainingSoil.deluxe, Region.island_trader, Currency.cinder_shard, 50, {Material.stone: 5, Material.fiber: 3, Material.clay: 1}) +deluxe_retaining_soil = shop_trade_recipe(RetainingSoil.deluxe, Region.island_trader, Currency.cinder_shard, 50, + {Material.stone: 5, Material.fiber: 3, Material.clay: 1}) tree_fertilizer = skill_recipe(Fertilizer.tree, Skill.foraging, 7, {Material.fiber: 5, Material.stone: 5}) -spring_seeds = skill_recipe(WildSeeds.spring, Skill.foraging, 1, {Forageable.wild_horseradish: 1, Forageable.daffodil: 1, Forageable.leek: 1, Forageable.dandelion: 1}) +spring_seeds = skill_recipe(WildSeeds.spring, Skill.foraging, 1, + {Forageable.wild_horseradish: 1, Forageable.daffodil: 1, Forageable.leek: 1, Forageable.dandelion: 1}) summer_seeds = skill_recipe(WildSeeds.summer, Skill.foraging, 4, {Forageable.spice_berry: 1, Fruit.grape: 1, Forageable.sweet_pea: 1}) -fall_seeds = skill_recipe(WildSeeds.fall, Skill.foraging, 6, {Forageable.common_mushroom: 1, Forageable.wild_plum: 1, Forageable.hazelnut: 1, Forageable.blackberry: 1}) -winter_seeds = skill_recipe(WildSeeds.winter, Skill.foraging, 7, {Forageable.winter_root: 1, Forageable.crystal_fruit: 1, Forageable.snow_yam: 1, Forageable.crocus: 1}) +fall_seeds = skill_recipe(WildSeeds.fall, Skill.foraging, 6, {Mushroom.common: 1, Forageable.wild_plum: 1, Forageable.hazelnut: 1, Forageable.blackberry: 1}) +winter_seeds = skill_recipe(WildSeeds.winter, Skill.foraging, 7, + {Forageable.winter_root: 1, Forageable.crystal_fruit: 1, Forageable.snow_yam: 1, Forageable.crocus: 1}) ancient_seeds = ap_recipe(WildSeeds.ancient, {Artifact.ancient_seed: 1}) grass_starter = shop_recipe(WildSeeds.grass_starter, Region.pierre_store, 1000, {Material.fiber: 10}) +blue_grass_starter = ap_recipe(WildSeeds.blue_grass_starter, {Material.fiber: 25, Material.moss: 10, ArtisanGood.mystic_syrup: 1}) for wild_seeds in [WildSeeds.spring, WildSeeds.summer, WildSeeds.fall, WildSeeds.winter]: tea_sapling = cutscene_recipe(WildSeeds.tea_sapling, Region.sunroom, NPC.caroline, 2, {wild_seeds: 2, Material.fiber: 5, Material.wood: 5}) fiber_seeds = special_order_recipe(WildSeeds.fiber, SpecialOrder.community_cleanup, {Seed.mixed: 1, Material.sap: 5, Material.clay: 1}) @@ -161,7 +179,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, wood_floor = shop_recipe(Floor.wood, Region.carpenter, 100, {Material.wood: 1}) rustic_floor = shop_recipe(Floor.rustic, Region.carpenter, 200, {Material.wood: 1}) straw_floor = shop_recipe(Floor.straw, Region.carpenter, 200, {Material.wood: 1, Material.fiber: 1}) -weathered_floor = shop_recipe(Floor.weathered, Region.mines_dwarf_shop, 500, {Material.wood: 1}) +weathered_floor = shop_recipe(Floor.weathered, LogicRegion.mines_dwarf_shop, 500, {Material.wood: 1}) crystal_floor = shop_recipe(Floor.crystal, Region.sewer, 500, {MetalBar.quartz: 1}) stone_floor = shop_recipe(Floor.stone, Region.carpenter, 100, {Material.stone: 1}) stone_walkway_floor = shop_recipe(Floor.stone_walkway, Region.carpenter, 200, {Material.stone: 1}) @@ -174,6 +192,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, spinner = skill_recipe(Fishing.spinner, Skill.fishing, 6, {MetalBar.iron: 2}) trap_bobber = skill_recipe(Fishing.trap_bobber, Skill.fishing, 6, {MetalBar.copper: 1, Material.sap: 10}) +sonar_bobber = skill_recipe(Fishing.sonar_bobber, Skill.fishing, 6, {MetalBar.iron: 1, MetalBar.quartz: 2}) cork_bobber = skill_recipe(Fishing.cork_bobber, Skill.fishing, 7, {Material.wood: 10, Material.hardwood: 5, Loot.slime: 10}) quality_bobber = special_order_recipe(Fishing.quality_bobber, SpecialOrder.juicy_bugs_wanted, {MetalBar.copper: 1, Material.sap: 20, Loot.solar_essence: 5}) treasure_hunter = skill_recipe(Fishing.treasure_hunter, Skill.fishing, 7, {MetalBar.gold: 2}) @@ -181,6 +200,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, barbed_hook = skill_recipe(Fishing.barbed_hook, Skill.fishing, 8, {MetalBar.copper: 1, MetalBar.iron: 1, MetalBar.gold: 1}) magnet = skill_recipe(Fishing.magnet, Skill.fishing, 9, {MetalBar.iron: 1}) bait = skill_recipe(Fishing.bait, Skill.fishing, 2, {Loot.bug_meat: 1}) +deluxe_bait = skill_recipe(Fishing.deluxe_bait, Skill.fishing, 4, {Fishing.bait: 5, Material.moss: 2}) wild_bait = cutscene_recipe(Fishing.wild_bait, Region.tent, NPC.linus, 4, {Material.fiber: 10, Loot.bug_meat: 5, Loot.slime: 5}) magic_bait = ap_recipe(Fishing.magic_bait, {Ore.radioactive: 1, Loot.bug_meat: 3}) crab_pot = skill_recipe(Machine.crab_pot, Skill.fishing, 3, {Material.wood: 40, MetalBar.iron: 3}) @@ -191,11 +211,11 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, thorns_ring = skill_recipe(Ring.thorns_ring, Skill.combat, 7, {Fossil.bone_fragment: 50, Material.stone: 50, MetalBar.gold: 1}) glowstone_ring = skill_recipe(Ring.glowstone_ring, Skill.mining, 4, {Loot.solar_essence: 5, MetalBar.iron: 5}) iridium_band = skill_recipe(Ring.iridium_band, Skill.combat, 9, {MetalBar.iridium: 5, Loot.solar_essence: 50, Loot.void_essence: 50}) -wedding_ring = shop_recipe(Ring.wedding_ring, Region.traveling_cart, 500, {MetalBar.iridium: 5, Mineral.prismatic_shard: 1}) +wedding_ring = shop_recipe(Ring.wedding_ring, LogicRegion.traveling_cart, 500, {MetalBar.iridium: 5, Mineral.prismatic_shard: 1}) field_snack = skill_recipe(Edible.field_snack, Skill.foraging, 1, {TreeSeed.acorn: 1, TreeSeed.maple: 1, TreeSeed.pine: 1}) bug_steak = skill_recipe(Edible.bug_steak, Skill.combat, 1, {Loot.bug_meat: 10}) -life_elixir = skill_recipe(Edible.life_elixir, Skill.combat, 2, {Forageable.red_mushroom: 1, Forageable.purple_mushroom: 1, Forageable.morel: 1, Forageable.chanterelle: 1}) +life_elixir = skill_recipe(Edible.life_elixir, Skill.combat, 2, {Mushroom.red: 1, Mushroom.purple: 1, Mushroom.morel: 1, Mushroom.chanterelle: 1}) oil_of_garlic = skill_recipe(Edible.oil_of_garlic, Skill.combat, 6, {Vegetable.garlic: 10, Ingredient.oil: 1}) monster_musk = special_order_recipe(Consumable.monster_musk, SpecialOrder.prismatic_jelly, {Loot.bat_wing: 30, Loot.slime: 30}) @@ -203,8 +223,10 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, warp_totem_beach = skill_recipe(Consumable.warp_totem_beach, Skill.foraging, 6, {Material.hardwood: 1, WaterItem.coral: 2, Material.fiber: 10}) warp_totem_mountains = skill_recipe(Consumable.warp_totem_mountains, Skill.foraging, 7, {Material.hardwood: 1, MetalBar.iron: 1, Material.stone: 25}) warp_totem_farm = skill_recipe(Consumable.warp_totem_farm, Skill.foraging, 8, {Material.hardwood: 1, ArtisanGood.honey: 1, Material.fiber: 20}) -warp_totem_desert = shop_trade_recipe(Consumable.warp_totem_desert, Region.desert, MetalBar.iridium, 10, {Material.hardwood: 2, Forageable.coconut: 1, Ore.iridium: 4}) -warp_totem_island = shop_recipe(Consumable.warp_totem_island, Region.volcano_dwarf_shop, 10000, {Material.hardwood: 5, Forageable.dragon_tooth: 1, Forageable.ginger: 1}) +warp_totem_desert = shop_trade_recipe(Consumable.warp_totem_desert, Region.desert, MetalBar.iridium, 10, + {Material.hardwood: 2, Forageable.coconut: 1, Ore.iridium: 4}) +warp_totem_island = shop_recipe(Consumable.warp_totem_island, Region.volcano_dwarf_shop, 10000, + {Material.hardwood: 5, Forageable.dragon_tooth: 1, Forageable.ginger: 1}) rain_totem = skill_recipe(Consumable.rain_totem, Skill.foraging, 9, {Material.hardwood: 1, ArtisanGood.truffle_oil: 1, ArtisanGood.pine_tar: 5}) torch = starter_recipe(Lighting.torch, {Material.wood: 1, Material.sap: 2}) @@ -219,13 +241,17 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, marble_brazier = shop_recipe(Lighting.marble_brazier, Region.carpenter, 5000, {Mineral.marble: 1, Mineral.aquamarine: 1, Material.stone: 100}) wood_lamp_post = shop_recipe(Lighting.wood_lamp_post, Region.carpenter, 500, {Material.wood: 50, ArtisanGood.battery_pack: 1}) iron_lamp_post = shop_recipe(Lighting.iron_lamp_post, Region.carpenter, 1000, {MetalBar.iron: 1, ArtisanGood.battery_pack: 1}) -jack_o_lantern = festival_shop_recipe(Lighting.jack_o_lantern, Region.spirit_eve, 2000, {Vegetable.pumpkin: 1, Lighting.torch: 1}) +jack_o_lantern = festival_shop_recipe(Lighting.jack_o_lantern, LogicRegion.spirit_eve, 2000, {Vegetable.pumpkin: 1, Lighting.torch: 1}) bone_mill = special_order_recipe(Machine.bone_mill, SpecialOrder.fragments_of_the_past, {Fossil.bone_fragment: 10, Material.clay: 3, Material.stone: 20}) -charcoal_kiln = skill_recipe(Machine.charcoal_kiln, Skill.foraging, 4, {Material.wood: 20, MetalBar.copper: 2}) +bait_maker = skill_recipe(Machine.bait_maker, Skill.fishing, 6, {MetalBar.iron: 3, WaterItem.coral: 3, WaterItem.sea_urchin: 1}) + +charcoal_kiln = skill_recipe(Machine.charcoal_kiln, Skill.foraging, 2, {Material.wood: 20, MetalBar.copper: 2}) + crystalarium = skill_recipe(Machine.crystalarium, Skill.mining, 9, {Material.stone: 99, MetalBar.gold: 5, MetalBar.iridium: 2, ArtisanGood.battery_pack: 1}) furnace = skill_recipe(Machine.furnace, Skill.mining, 1, {Ore.copper: 20, Material.stone: 25}) geode_crusher = special_order_recipe(Machine.geode_crusher, SpecialOrder.cave_patrol, {MetalBar.gold: 2, Material.stone: 50, Mineral.diamond: 1}) +mushroom_log = skill_recipe(Machine.mushroom_log, Skill.foraging, 4, {Material.hardwood: 10, Material.moss: 10}) heavy_tapper = ap_recipe(Machine.heavy_tapper, {Material.hardwood: 30, MetalBar.radioactive: 1}) lightning_rod = skill_recipe(Machine.lightning_rod, Skill.foraging, 6, {MetalBar.iron: 1, MetalBar.quartz: 1, Loot.bat_wing: 5}) ostrich_incubator = ap_recipe(Machine.ostrich_incubator, {Fossil.bone_fragment: 50, Material.hardwood: 50, Currency.cinder_shard: 20}) @@ -234,20 +260,27 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, slime_egg_press = skill_recipe(Machine.slime_egg_press, Skill.combat, 6, {Material.coal: 25, Mineral.fire_quartz: 1, ArtisanGood.battery_pack: 1}) slime_incubator = skill_recipe(Machine.slime_incubator, Skill.combat, 8, {MetalBar.iridium: 2, Loot.slime: 100}) solar_panel = special_order_recipe(Machine.solar_panel, SpecialOrder.island_ingredients, {MetalBar.quartz: 10, MetalBar.iron: 5, MetalBar.gold: 5}) -tapper = skill_recipe(Machine.tapper, Skill.foraging, 3, {Material.wood: 40, MetalBar.copper: 2}) -worm_bin = skill_recipe(Machine.worm_bin, Skill.fishing, 8, {Material.hardwood: 25, MetalBar.gold: 1, MetalBar.iron: 1, Material.fiber: 50}) -tub_o_flowers = festival_shop_recipe(Furniture.tub_o_flowers, Region.flower_dance, 2000, {Material.wood: 15, Seed.tulip: 1, Seed.jazz: 1, Seed.poppy: 1, Seed.spangle: 1}) +tapper = skill_recipe(Machine.tapper, Skill.foraging, 4, {Material.wood: 40, MetalBar.copper: 2}) + +worm_bin = skill_recipe(Machine.worm_bin, Skill.fishing, 4, {Material.hardwood: 25, MetalBar.gold: 1, MetalBar.iron: 1, Material.fiber: 50}) +deluxe_worm_bin = skill_recipe(Machine.deluxe_worm_bin, Skill.fishing, 8, {Machine.worm_bin: 1, Material.moss: 30}) + +tub_o_flowers = festival_shop_recipe(Furniture.tub_o_flowers, LogicRegion.flower_dance, 2000, + {Material.wood: 15, Seed.tulip: 1, Seed.jazz: 1, Seed.poppy: 1, Seed.spangle: 1}) wicked_statue = shop_recipe(Furniture.wicked_statue, Region.sewer, 1000, {Material.stone: 25, Material.coal: 5}) flute_block = cutscene_recipe(Furniture.flute_block, Region.carpenter, NPC.robin, 6, {Material.wood: 10, Ore.copper: 2, Material.fiber: 20}) drum_block = cutscene_recipe(Furniture.drum_block, Region.carpenter, NPC.robin, 6, {Material.stone: 10, Ore.copper: 2, Material.fiber: 20}) chest = starter_recipe(Storage.chest, {Material.wood: 50}) stone_chest = special_order_recipe(Storage.stone_chest, SpecialOrder.robins_resource_rush, {Material.stone: 50}) +big_chest = shop_recipe(Storage.big_chest, Region.carpenter, 5000, {Material.wood: 120, MetalBar.copper: 2}) +big_stone_chest = shop_recipe(Storage.big_stone_chest, LogicRegion.mines_dwarf_shop, 5000, {Material.stone: 250}) wood_sign = starter_recipe(Sign.wood, {Material.wood: 25}) stone_sign = starter_recipe(Sign.stone, {Material.stone: 25}) dark_sign = friendship_recipe(Sign.dark, NPC.krobus, 3, {Loot.bat_wing: 5, Fossil.bone_fragment: 5}) +text_sign = starter_recipe(Sign.text, {Material.wood: 25}) garden_pot = ap_recipe(Craftable.garden_pot, {Material.clay: 1, Material.stone: 10, MetalBar.quartz: 1}, "Greenhouse") scarecrow = skill_recipe(Craftable.scarecrow, Skill.farming, 1, {Material.wood: 50, Material.coal: 1, Material.fiber: 20}) @@ -258,56 +291,84 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, transmute_au = skill_recipe(Craftable.transmute_au, Skill.mining, 7, {MetalBar.iron: 2}) mini_jukebox = cutscene_recipe(Craftable.mini_jukebox, Region.saloon, NPC.gus, 5, {MetalBar.iron: 2, ArtisanGood.battery_pack: 1}) mini_obelisk = special_order_recipe(Craftable.mini_obelisk, SpecialOrder.a_curious_substance, {Material.hardwood: 30, Loot.solar_essence: 20, MetalBar.gold: 3}) -farm_computer = special_order_recipe(Craftable.farm_computer, SpecialOrder.aquatic_overpopulation, {Artifact.dwarf_gadget: 1, ArtisanGood.battery_pack: 1, MetalBar.quartz: 10}) +farm_computer = special_order_recipe(Craftable.farm_computer, SpecialOrder.aquatic_overpopulation, + {Artifact.dwarf_gadget: 1, ArtisanGood.battery_pack: 1, MetalBar.quartz: 10}) hopper = ap_recipe(Craftable.hopper, {Material.hardwood: 10, MetalBar.iridium: 1, MetalBar.radioactive: 1}) -cookout_kit = skill_recipe(Craftable.cookout_kit, Skill.foraging, 9, {Material.wood: 15, Material.fiber: 10, Material.coal: 3}) + +cookout_kit = skill_recipe(Craftable.cookout_kit, Skill.foraging, 3, {Material.wood: 15, Material.fiber: 10, Material.coal: 3}) +tent_kit = skill_recipe(Craftable.tent_kit, Skill.foraging, 8, {Material.hardwood: 10, Material.fiber: 25, ArtisanGood.cloth: 1}) + +statue_of_blessings = mastery_recipe(Statue.blessings, Skill.farming, {Material.sap: 999, Material.fiber: 999, Material.stone: 999}) +statue_of_dwarf_king = mastery_recipe(Statue.dwarf_king, Skill.mining, {MetalBar.iridium: 20}) +heavy_furnace = mastery_recipe(Machine.heavy_furnace, Skill.mining, {Machine.furnace: 2, MetalBar.iron: 3, Material.stone: 50}) +mystic_tree_seed = mastery_recipe(TreeSeed.mystic, Skill.foraging, {TreeSeed.acorn: 5, TreeSeed.maple: 5, TreeSeed.pine: 5, TreeSeed.mahogany: 5}) +treasure_totem = mastery_recipe(Consumable.treasure_totem, Skill.foraging, {Material.hardwood: 5, ArtisanGood.mystic_syrup: 1, Material.moss: 10}) +challenge_bait = mastery_recipe(Fishing.challenge_bait, Skill.fishing, {Fossil.bone_fragment: 5, Material.moss: 2}) +anvil = mastery_recipe(Machine.anvil, Skill.combat, {MetalBar.iron: 50}) +mini_forge = mastery_recipe(Machine.mini_forge, Skill.combat, {Forageable.dragon_tooth: 5, MetalBar.iron: 10, MetalBar.gold: 10, MetalBar.iridium: 5}) travel_charm = shop_recipe(ModCraftable.travel_core, Region.adventurer_guild, 250, {Loot.solar_essence: 1, Loot.void_essence: 1}, ModNames.magic) -preservation_chamber = skill_recipe(ModMachine.preservation_chamber, ModSkill.archaeology, 2, {MetalBar.copper: 1, Material.wood: 15, ArtisanGood.oak_resin: 30}, +preservation_chamber = skill_recipe(ModMachine.preservation_chamber, ModSkill.archaeology, 1, + {MetalBar.copper: 1, Material.wood: 15, ArtisanGood.oak_resin: 30}, ModNames.archaeology) -preservation_chamber_h = skill_recipe(ModMachine.hardwood_preservation_chamber, ModSkill.archaeology, 7, {MetalBar.copper: 1, Material.hardwood: 15, +restoration_table = skill_recipe(ModMachine.restoration_table, ModSkill.archaeology, 1, {Material.wood: 15, MetalBar.copper: 1, MetalBar.iron: 1}, ModNames.archaeology) +preservation_chamber_h = skill_recipe(ModMachine.hardwood_preservation_chamber, ModSkill.archaeology, 6, {MetalBar.copper: 1, Material.hardwood: 15, ArtisanGood.oak_resin: 30}, ModNames.archaeology) -grinder = skill_recipe(ModMachine.grinder, ModSkill.archaeology, 8, {Artifact.rusty_cog: 10, MetalBar.iron: 5, ArtisanGood.battery_pack: 1}, ModNames.archaeology) -ancient_battery = skill_recipe(ModMachine.ancient_battery, ModSkill.archaeology, 6, {Material.stone: 40, MetalBar.copper: 10, MetalBar.iron: 5}, +grinder = skill_recipe(ModMachine.grinder, ModSkill.archaeology, 2, {Artifact.rusty_cog: 10, MetalBar.iron: 5, ArtisanGood.battery_pack: 1}, + ModNames.archaeology) +ancient_battery = skill_recipe(ModMachine.ancient_battery, ModSkill.archaeology, 7, {Material.stone: 40, MetalBar.copper: 10, MetalBar.iron: 5}, ModNames.archaeology) -glass_bazier = skill_recipe(ModCraftable.glass_bazier, ModSkill.archaeology, 1, {Artifact.glass_shards: 10}, ModNames.archaeology) -glass_path = skill_recipe(ModFloor.glass_path, ModSkill.archaeology, 1, {Artifact.glass_shards: 1}, ModNames.archaeology) -glass_fence = skill_recipe(ModCraftable.glass_fence, ModSkill.archaeology, 1, {Artifact.glass_shards: 5}, ModNames.archaeology) -bone_path = skill_recipe(ModFloor.bone_path, ModSkill.archaeology, 3, {Fossil.bone_fragment: 1}, ModNames.archaeology) +glass_bazier = skill_recipe(ModCraftable.glass_brazier, ModSkill.archaeology, 4, {Artifact.glass_shards: 10}, ModNames.archaeology) +glass_path = skill_recipe(ModFloor.glass_path, ModSkill.archaeology, 3, {Artifact.glass_shards: 1}, ModNames.archaeology) +glass_fence = skill_recipe(ModCraftable.glass_fence, ModSkill.archaeology, 7, {Artifact.glass_shards: 5}, ModNames.archaeology) +bone_path = skill_recipe(ModFloor.bone_path, ModSkill.archaeology, 4, {Fossil.bone_fragment: 1}, ModNames.archaeology) +rust_path = skill_recipe(ModFloor.rusty_path, ModSkill.archaeology, 2, {ModTrash.rusty_scrap: 2}, ModNames.archaeology) +rusty_brazier = skill_recipe(ModCraftable.rusty_brazier, ModSkill.archaeology, 3, {ModTrash.rusty_scrap: 10, Material.coal: 1, Material.fiber: 1}, ModNames.archaeology) +bone_fence = skill_recipe(ModCraftable.bone_fence, ModSkill.archaeology, 8, {Fossil.bone_fragment: 2}, ModNames.archaeology) water_shifter = skill_recipe(ModCraftable.water_shifter, ModSkill.archaeology, 4, {Material.wood: 40, MetalBar.copper: 4}, ModNames.archaeology) -wooden_display = skill_recipe(ModCraftable.wooden_display, ModSkill.archaeology, 2, {Material.wood: 25}, ModNames.archaeology) +wooden_display = skill_recipe(ModCraftable.wooden_display, ModSkill.archaeology, 1, {Material.wood: 25}, ModNames.archaeology) hardwood_display = skill_recipe(ModCraftable.hardwood_display, ModSkill.archaeology, 7, {Material.hardwood: 10}, ModNames.archaeology) +lucky_ring = skill_recipe(Ring.lucky_ring, ModSkill.archaeology, 8, {Artifact.elvish_jewelry: 1, AnimalProduct.rabbit_foot: 5, Mineral.tigerseye: 1}, ModNames.archaeology) volcano_totem = skill_recipe(ModConsumable.volcano_totem, ModSkill.archaeology, 9, {Material.cinder_shard: 5, Artifact.rare_disc: 1, Artifact.dwarf_gadget: 1}, ModNames.archaeology) -haste_elixir = shop_recipe(ModEdible.haste_elixir, SVERegion.alesia_shop, 35000, {Loot.void_essence: 35, SVEForage.void_soul: 5, Ingredient.sugar: 1, +haste_elixir = shop_recipe(ModEdible.haste_elixir, SVERegion.alesia_shop, 35000, {Loot.void_essence: 35, ModLoot.void_soul: 5, Ingredient.sugar: 1, Meal.spicy_eel: 1}, ModNames.sve) -hero_elixir = shop_recipe(ModEdible.hero_elixir, SVERegion.isaac_shop, 65000, {SVEForage.void_pebble: 3, SVEForage.void_soul: 5, Ingredient.oil: 1, +hero_elixir = shop_recipe(ModEdible.hero_elixir, SVERegion.isaac_shop, 65000, {ModLoot.void_pebble: 3, ModLoot.void_soul: 5, Ingredient.oil: 1, Loot.slime: 10}, ModNames.sve) -armor_elixir = shop_recipe(ModEdible.armor_elixir, SVERegion.alesia_shop, 50000, {Loot.solar_essence: 30, SVEForage.void_soul: 5, Ingredient.vinegar: 5, +armor_elixir = shop_recipe(ModEdible.armor_elixir, SVERegion.alesia_shop, 50000, {Loot.solar_essence: 30, ModLoot.void_soul: 5, Ingredient.vinegar: 5, Fossil.bone_fragment: 5}, ModNames.sve) ginger_tincture = friendship_recipe(ModConsumable.ginger_tincture, ModNPC.goblin, 4, {DistantLandsForageable.brown_amanita: 1, Forageable.ginger: 5, - Material.cinder_shard: 1, DistantLandsForageable.swamp_herb: 1}, ModNames.distant_lands) - -neanderthal_skeleton = shop_recipe(ModCraftable.neanderthal_skeleton, Region.mines_dwarf_shop, 5000, - {ModFossil.neanderthal_skull: 1, ModFossil.neanderthal_ribs: 1, ModFossil.neanderthal_pelvis: 1, ModFossil.neanderthal_limb_bones: 1, - MetalBar.iron: 5, Material.hardwood: 10}, ModNames.boarding_house) -pterodactyl_skeleton_l = shop_recipe(ModCraftable.pterodactyl_skeleton_l, Region.mines_dwarf_shop, 5000, + Material.cinder_shard: 1, DistantLandsForageable.swamp_herb: 1}, + ModNames.distant_lands) + +neanderthal_skeleton = shop_recipe(ModCraftable.neanderthal_skeleton, LogicRegion.mines_dwarf_shop, 5000, + {ModFossil.neanderthal_skull: 1, ModFossil.neanderthal_ribs: 1, ModFossil.neanderthal_pelvis: 1, + ModFossil.neanderthal_limb_bones: 1, + MetalBar.iron: 5, Material.hardwood: 10}, ModNames.boarding_house) +pterodactyl_skeleton_l = shop_recipe(ModCraftable.pterodactyl_skeleton_l, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.pterodactyl_phalange: 1, ModFossil.pterodactyl_skull: 1, ModFossil.pterodactyl_l_wing_bone: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) -pterodactyl_skeleton_m = shop_recipe(ModCraftable.pterodactyl_skeleton_m, Region.mines_dwarf_shop, 5000, +pterodactyl_skeleton_m = shop_recipe(ModCraftable.pterodactyl_skeleton_m, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.pterodactyl_phalange: 1, ModFossil.pterodactyl_vertebra: 1, ModFossil.pterodactyl_ribs: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) -pterodactyl_skeleton_r = shop_recipe(ModCraftable.pterodactyl_skeleton_r, Region.mines_dwarf_shop, 5000, +pterodactyl_skeleton_r = shop_recipe(ModCraftable.pterodactyl_skeleton_r, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.pterodactyl_phalange: 1, ModFossil.pterodactyl_claw: 1, ModFossil.pterodactyl_r_wing_bone: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) -trex_skeleton_l = shop_recipe(ModCraftable.trex_skeleton_l, Region.mines_dwarf_shop, 5000, +trex_skeleton_l = shop_recipe(ModCraftable.trex_skeleton_l, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.dinosaur_vertebra: 1, ModFossil.dinosaur_tooth: 1, ModFossil.dinosaur_skull: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) -trex_skeleton_m = shop_recipe(ModCraftable.trex_skeleton_m, Region.mines_dwarf_shop, 5000, +trex_skeleton_m = shop_recipe(ModCraftable.trex_skeleton_m, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.dinosaur_vertebra: 1, ModFossil.dinosaur_ribs: 1, ModFossil.dinosaur_claw: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) -trex_skeleton_r = shop_recipe(ModCraftable.trex_skeleton_r, Region.mines_dwarf_shop, 5000, +trex_skeleton_r = shop_recipe(ModCraftable.trex_skeleton_r, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.dinosaur_vertebra: 1, ModFossil.dinosaur_femur: 1, ModFossil.dinosaur_pelvis: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) +bouquet = skill_recipe(Gift.bouquet, ModSkill.socializing, 3, {Flower.tulip: 3}, ModNames.socializing_skill) +trash_bin = skill_recipe(ModMachine.trash_bin, ModSkill.binning, 2, {Material.stone: 30, MetalBar.iron: 2}, ModNames.binning_skill) +composter = skill_recipe(ModMachine.composter, ModSkill.binning, 4, {Material.wood: 70, Material.sap: 20, Material.fiber: 30}, ModNames.binning_skill) +recycling_bin = skill_recipe(ModMachine.recycling_bin, ModSkill.binning, 7, {MetalBar.iron: 3, Material.fiber: 10, MetalBar.gold: 2}, ModNames.binning_skill) +advanced_recycling_machine = skill_recipe(ModMachine.advanced_recycling_machine, ModSkill.binning, 9, + {MetalBar.iridium: 5, ArtisanGood.battery_pack: 2, MetalBar.quartz: 10}, ModNames.binning_skill) + all_crafting_recipes_by_name = {recipe.item: recipe for recipe in all_crafting_recipes} diff --git a/worlds/stardew_valley/data/crops.csv b/worlds/stardew_valley/data/crops.csv deleted file mode 100644 index 0bf43a76764e..000000000000 --- a/worlds/stardew_valley/data/crops.csv +++ /dev/null @@ -1,41 +0,0 @@ -crop,farm_growth_seasons,seed,seed_seasons,seed_regions,requires_island -Amaranth,Fall,Amaranth Seeds,Fall,"Pierre's General Store",False -Artichoke,Fall,Artichoke Seeds,Fall,"Pierre's General Store",False -Beet,Fall,Beet Seeds,Fall,Oasis,False -Blue Jazz,Spring,Jazz Seeds,Spring,"Pierre's General Store",False -Blueberry,Summer,Blueberry Seeds,Summer,"Pierre's General Store",False -Bok Choy,Fall,Bok Choy Seeds,Fall,"Pierre's General Store",False -Cactus Fruit,,Cactus Seeds,,Oasis,False -Cauliflower,Spring,Cauliflower Seeds,Spring,"Pierre's General Store",False -Coffee Bean,"Spring,Summer",Coffee Bean,"Summer,Fall","Traveling Cart",False -Corn,"Summer,Fall",Corn Seeds,"Summer,Fall","Pierre's General Store",False -Cranberries,Fall,Cranberry Seeds,Fall,"Pierre's General Store",False -Eggplant,Fall,Eggplant Seeds,Fall,"Pierre's General Store",False -Fairy Rose,Fall,Fairy Seeds,Fall,"Pierre's General Store",False -Garlic,Spring,Garlic Seeds,Spring,"Pierre's General Store",False -Grape,Fall,Grape Starter,Fall,"Pierre's General Store",False -Green Bean,Spring,Bean Starter,Spring,"Pierre's General Store",False -Hops,Summer,Hops Starter,Summer,"Pierre's General Store",False -Hot Pepper,Summer,Pepper Seeds,Summer,"Pierre's General Store",False -Kale,Spring,Kale Seeds,Spring,"Pierre's General Store",False -Melon,Summer,Melon Seeds,Summer,"Pierre's General Store",False -Parsnip,Spring,Parsnip Seeds,Spring,"Pierre's General Store",False -Pineapple,Summer,Pineapple Seeds,Summer,"Island Trader",True -Poppy,Summer,Poppy Seeds,Summer,"Pierre's General Store",False -Potato,Spring,Potato Seeds,Spring,"Pierre's General Store",False -Qi Fruit,"Spring,Summer,Fall,Winter",Qi Bean,"Spring,Summer,Fall,Winter","Qi's Walnut Room",True -Pumpkin,Fall,Pumpkin Seeds,Fall,"Pierre's General Store",False -Radish,Summer,Radish Seeds,Summer,"Pierre's General Store",False -Red Cabbage,Summer,Red Cabbage Seeds,Summer,"Pierre's General Store",False -Rhubarb,Spring,Rhubarb Seeds,Spring,Oasis,False -Starfruit,Summer,Starfruit Seeds,Summer,Oasis,False -Strawberry,Spring,Strawberry Seeds,Spring,"Pierre's General Store",False -Summer Spangle,Summer,Spangle Seeds,Summer,"Pierre's General Store",False -Sunflower,"Summer,Fall",Sunflower Seeds,"Summer,Fall","Pierre's General Store",False -Sweet Gem Berry,Fall,Rare Seed,"Spring,Summer",Traveling Cart,False -Taro Root,Summer,Taro Tuber,Summer,"Island Trader",True -Tomato,Summer,Tomato Seeds,Summer,"Pierre's General Store",False -Tulip,Spring,Tulip Bulb,Spring,"Pierre's General Store",False -Unmilled Rice,Spring,Rice Shoot,Spring,"Pierre's General Store",False -Wheat,"Summer,Fall",Wheat Seeds,"Summer,Fall","Pierre's General Store",False -Yam,Fall,Yam Seeds,Fall,"Pierre's General Store",False diff --git a/worlds/stardew_valley/data/crops_data.py b/worlds/stardew_valley/data/crops_data.py deleted file mode 100644 index 7144ccfbcf9b..000000000000 --- a/worlds/stardew_valley/data/crops_data.py +++ /dev/null @@ -1,50 +0,0 @@ -from dataclasses import dataclass -from typing import Tuple - -from .. import data - - -@dataclass(frozen=True) -class SeedItem: - name: str - seasons: Tuple[str] - regions: Tuple[str] - requires_island: bool - - -@dataclass(frozen=True) -class CropItem: - name: str - farm_growth_seasons: Tuple[str] - seed: SeedItem - - -def load_crop_csv(): - import csv - try: - from importlib.resources import files - except ImportError: - from importlib_resources import files # noqa - - with files(data).joinpath("crops.csv").open() as file: - reader = csv.DictReader(file) - crops = [] - seeds = [] - - for item in reader: - seeds.append(SeedItem(item["seed"], - tuple(season for season in item["seed_seasons"].split(",")) - if item["seed_seasons"] else tuple(), - tuple(region for region in item["seed_regions"].split(",")) - if item["seed_regions"] else tuple(), - item["requires_island"] == "True")) - crops.append(CropItem(item["crop"], - tuple(season for season in item["farm_growth_seasons"].split(",")) - if item["farm_growth_seasons"] else tuple(), - seeds[-1])) - return crops, seeds - - -# TODO Those two should probably be split to we can include rest of seeds -all_crops, all_purchasable_seeds = load_crop_csv() -crops_by_name = {crop.name: crop for crop in all_crops} diff --git a/worlds/stardew_valley/data/fish_data.py b/worlds/stardew_valley/data/fish_data.py index aeb416733950..c6f0c30d41ff 100644 --- a/worlds/stardew_valley/data/fish_data.py +++ b/worlds/stardew_valley/data/fish_data.py @@ -1,10 +1,10 @@ from dataclasses import dataclass -from typing import List, Tuple, Union, Optional, Set +from typing import Tuple, Union, Optional from . import season_data as season -from ..strings.fish_names import Fish, SVEFish, DistantLandsFish -from ..strings.region_names import Region, SVERegion from ..mods.mod_data import ModNames +from ..strings.fish_names import Fish, SVEFish, DistantLandsFish +from ..strings.region_names import Region, SVERegion, LogicRegion @dataclass(frozen=True) @@ -30,6 +30,7 @@ def __repr__(self): mountain_lake = (Region.mountain,) forest_pond = (Region.forest,) forest_river = (Region.forest,) +forest_waterfall = (LogicRegion.forest_waterfall,) secret_woods = (Region.secret_woods,) mines_floor_20 = (Region.mines_floor_20,) mines_floor_60 = (Region.mines_floor_60,) @@ -50,8 +51,6 @@ def __repr__(self): fable_reef = (SVERegion.fable_reef,) vineyard = (SVERegion.blue_moon_vineyard,) -all_fish: List[FishItem] = [] - def create_fish(name: str, locations: Tuple[str, ...], seasons: Union[str, Tuple[str, ...]], difficulty: int, legendary: bool = False, extended_family: bool = False, mod_name: Optional[str] = None) -> FishItem: @@ -59,63 +58,63 @@ def create_fish(name: str, locations: Tuple[str, ...], seasons: Union[str, Tuple seasons = (seasons,) fish_item = FishItem(name, locations, seasons, difficulty, legendary, extended_family, mod_name) - all_fish.append(fish_item) return fish_item -albacore = create_fish("Albacore", ocean, (season.fall, season.winter), 60) -anchovy = create_fish("Anchovy", ocean, (season.spring, season.fall), 30) -blue_discus = create_fish("Blue Discus", ginger_island_river, season.all_seasons, 60) -bream = create_fish("Bream", town_river + forest_river, season.all_seasons, 35) -bullhead = create_fish("Bullhead", mountain_lake, season.all_seasons, 46) +albacore = create_fish(Fish.albacore, ocean, (season.fall, season.winter), 60) +anchovy = create_fish(Fish.anchovy, ocean, (season.spring, season.fall), 30) +blue_discus = create_fish(Fish.blue_discus, ginger_island_river, season.all_seasons, 60) +bream = create_fish(Fish.bream, town_river + forest_river, season.all_seasons, 35) +bullhead = create_fish(Fish.bullhead, mountain_lake, season.all_seasons, 46) carp = create_fish(Fish.carp, mountain_lake + secret_woods + sewers + mutant_bug_lair, season.not_winter, 15) -catfish = create_fish("Catfish", town_river + forest_river + secret_woods, (season.spring, season.fall), 75) -chub = create_fish("Chub", forest_river + mountain_lake, season.all_seasons, 35) -dorado = create_fish("Dorado", forest_river, season.summer, 78) -eel = create_fish("Eel", ocean, (season.spring, season.fall), 70) -flounder = create_fish("Flounder", ocean, (season.spring, season.summer), 50) -ghostfish = create_fish("Ghostfish", mines_floor_20 + mines_floor_60, season.all_seasons, 50) -halibut = create_fish("Halibut", ocean, season.not_fall, 50) -herring = create_fish("Herring", ocean, (season.spring, season.winter), 25) -ice_pip = create_fish("Ice Pip", mines_floor_60, season.all_seasons, 85) -largemouth_bass = create_fish("Largemouth Bass", mountain_lake, season.all_seasons, 50) -lava_eel = create_fish("Lava Eel", mines_floor_100, season.all_seasons, 90) -lingcod = create_fish("Lingcod", town_river + forest_river + mountain_lake, season.winter, 85) -lionfish = create_fish("Lionfish", ginger_island_ocean, season.all_seasons, 50) -midnight_carp = create_fish("Midnight Carp", mountain_lake + forest_pond + ginger_island_river, +catfish = create_fish(Fish.catfish, town_river + forest_river + secret_woods, (season.spring, season.fall), 75) +chub = create_fish(Fish.chub, forest_river + mountain_lake, season.all_seasons, 35) +dorado = create_fish(Fish.dorado, forest_river, season.summer, 78) +eel = create_fish(Fish.eel, ocean, (season.spring, season.fall), 70) +flounder = create_fish(Fish.flounder, ocean, (season.spring, season.summer), 50) +ghostfish = create_fish(Fish.ghostfish, mines_floor_20 + mines_floor_60, season.all_seasons, 50) +goby = create_fish(Fish.goby, forest_waterfall, season.all_seasons, 55) +halibut = create_fish(Fish.halibut, ocean, season.not_fall, 50) +herring = create_fish(Fish.herring, ocean, (season.spring, season.winter), 25) +ice_pip = create_fish(Fish.ice_pip, mines_floor_60, season.all_seasons, 85) +largemouth_bass = create_fish(Fish.largemouth_bass, mountain_lake, season.all_seasons, 50) +lava_eel = create_fish(Fish.lava_eel, mines_floor_100, season.all_seasons, 90) +lingcod = create_fish(Fish.lingcod, town_river + forest_river + mountain_lake, season.winter, 85) +lionfish = create_fish(Fish.lionfish, ginger_island_ocean, season.all_seasons, 50) +midnight_carp = create_fish(Fish.midnight_carp, mountain_lake + forest_pond + ginger_island_river, (season.fall, season.winter), 55) -octopus = create_fish("Octopus", ocean, season.summer, 95) -perch = create_fish("Perch", town_river + forest_river + forest_pond + mountain_lake, season.winter, 35) -pike = create_fish("Pike", town_river + forest_river + forest_pond, (season.summer, season.winter), 60) -pufferfish = create_fish("Pufferfish", ocean + ginger_island_ocean, season.summer, 80) -rainbow_trout = create_fish("Rainbow Trout", town_river + forest_river + mountain_lake, season.summer, 45) -red_mullet = create_fish("Red Mullet", ocean, (season.summer, season.winter), 55) -red_snapper = create_fish("Red Snapper", ocean, (season.summer, season.fall), 40) -salmon = create_fish("Salmon", town_river + forest_river, season.fall, 50) -sandfish = create_fish("Sandfish", desert, season.all_seasons, 65) -sardine = create_fish("Sardine", ocean, (season.spring, season.fall, season.winter), 30) -scorpion_carp = create_fish("Scorpion Carp", desert, season.all_seasons, 90) -sea_cucumber = create_fish("Sea Cucumber", ocean, (season.fall, season.winter), 40) -shad = create_fish("Shad", town_river + forest_river, season.not_winter, 45) -slimejack = create_fish("Slimejack", mutant_bug_lair, season.all_seasons, 55) -smallmouth_bass = create_fish("Smallmouth Bass", town_river + forest_river, (season.spring, season.fall), 28) -squid = create_fish("Squid", ocean, season.winter, 75) -stingray = create_fish("Stingray", pirate_cove, season.all_seasons, 80) -stonefish = create_fish("Stonefish", mines_floor_20, season.all_seasons, 65) -sturgeon = create_fish("Sturgeon", mountain_lake, (season.summer, season.winter), 78) -sunfish = create_fish("Sunfish", town_river + forest_river, (season.spring, season.summer), 30) -super_cucumber = create_fish("Super Cucumber", ocean + ginger_island_ocean, (season.summer, season.fall), 80) -tiger_trout = create_fish("Tiger Trout", town_river + forest_river, (season.fall, season.winter), 60) -tilapia = create_fish("Tilapia", ocean + ginger_island_ocean, (season.summer, season.fall), 50) +octopus = create_fish(Fish.octopus, ocean, season.summer, 95) +perch = create_fish(Fish.perch, town_river + forest_river + forest_pond + mountain_lake, season.winter, 35) +pike = create_fish(Fish.pike, town_river + forest_river + forest_pond, (season.summer, season.winter), 60) +pufferfish = create_fish(Fish.pufferfish, ocean + ginger_island_ocean, season.summer, 80) +rainbow_trout = create_fish(Fish.rainbow_trout, town_river + forest_river + mountain_lake, season.summer, 45) +red_mullet = create_fish(Fish.red_mullet, ocean, (season.summer, season.winter), 55) +red_snapper = create_fish(Fish.red_snapper, ocean, (season.summer, season.fall), 40) +salmon = create_fish(Fish.salmon, town_river + forest_river, season.fall, 50) +sandfish = create_fish(Fish.sandfish, desert, season.all_seasons, 65) +sardine = create_fish(Fish.sardine, ocean, (season.spring, season.fall, season.winter), 30) +scorpion_carp = create_fish(Fish.scorpion_carp, desert, season.all_seasons, 90) +sea_cucumber = create_fish(Fish.sea_cucumber, ocean, (season.fall, season.winter), 40) +shad = create_fish(Fish.shad, town_river + forest_river, season.not_winter, 45) +slimejack = create_fish(Fish.slimejack, mutant_bug_lair, season.all_seasons, 55) +smallmouth_bass = create_fish(Fish.smallmouth_bass, town_river + forest_river, (season.spring, season.fall), 28) +squid = create_fish(Fish.squid, ocean, season.winter, 75) +stingray = create_fish(Fish.stingray, pirate_cove, season.all_seasons, 80) +stonefish = create_fish(Fish.stonefish, mines_floor_20, season.all_seasons, 65) +sturgeon = create_fish(Fish.sturgeon, mountain_lake, (season.summer, season.winter), 78) +sunfish = create_fish(Fish.sunfish, town_river + forest_river, (season.spring, season.summer), 30) +super_cucumber = create_fish(Fish.super_cucumber, ocean + ginger_island_ocean, (season.summer, season.fall), 80) +tiger_trout = create_fish(Fish.tiger_trout, town_river + forest_river, (season.fall, season.winter), 60) +tilapia = create_fish(Fish.tilapia, ocean + ginger_island_ocean, (season.summer, season.fall), 50) # Tuna has different seasons on ginger island. Should be changed when the whole fish thing is refactored -tuna = create_fish("Tuna", ocean + ginger_island_ocean, (season.summer, season.winter), 70) -void_salmon = create_fish("Void Salmon", witch_swamp, season.all_seasons, 80) -walleye = create_fish("Walleye", town_river + forest_river + forest_pond + mountain_lake, season.fall, 45) -woodskip = create_fish("Woodskip", secret_woods, season.all_seasons, 50) +tuna = create_fish(Fish.tuna, ocean + ginger_island_ocean, (season.summer, season.winter), 70) +void_salmon = create_fish(Fish.void_salmon, witch_swamp, season.all_seasons, 80) +walleye = create_fish(Fish.walleye, town_river + forest_river + forest_pond + mountain_lake, season.fall, 45) +woodskip = create_fish(Fish.woodskip, secret_woods, season.all_seasons, 50) -blob_fish = create_fish("Blobfish", night_market, season.winter, 75) -midnight_squid = create_fish("Midnight Squid", night_market, season.winter, 55) -spook_fish = create_fish("Spook Fish", night_market, season.winter, 60) +blobfish = create_fish(Fish.blobfish, night_market, season.winter, 75) +midnight_squid = create_fish(Fish.midnight_squid, night_market, season.winter, 55) +spook_fish = create_fish(Fish.spook_fish, night_market, season.winter, 60) angler = create_fish(Fish.angler, town_river, season.fall, 85, True, False) crimsonfish = create_fish(Fish.crimsonfish, ocean, season.summer, 95, True, False) @@ -155,37 +154,21 @@ def create_fish(name: str, locations: Tuple[str, ...], seasons: Union[str, Tuple void_eel = create_fish(SVEFish.void_eel, witch_swamp, season.all_seasons, 100, mod_name=ModNames.sve) water_grub = create_fish(SVEFish.water_grub, mutant_bug_lair, season.all_seasons, 60, mod_name=ModNames.sve) sea_sponge = create_fish(SVEFish.sea_sponge, ginger_island_ocean, season.all_seasons, 40, mod_name=ModNames.sve) -dulse_seaweed = create_fish(SVEFish.dulse_seaweed, vineyard, season.all_seasons, 50, mod_name=ModNames.sve) void_minnow = create_fish(DistantLandsFish.void_minnow, witch_swamp, season.all_seasons, 15, mod_name=ModNames.distant_lands) purple_algae = create_fish(DistantLandsFish.purple_algae, witch_swamp, season.all_seasons, 15, mod_name=ModNames.distant_lands) swamp_leech = create_fish(DistantLandsFish.swamp_leech, witch_swamp, season.all_seasons, 15, mod_name=ModNames.distant_lands) giant_horsehoe_crab = create_fish(DistantLandsFish.giant_horsehoe_crab, witch_swamp, season.all_seasons, 90, mod_name=ModNames.distant_lands) - -clam = create_fish("Clam", ocean, season.all_seasons, -1) -cockle = create_fish("Cockle", ocean, season.all_seasons, -1) -crab = create_fish("Crab", ocean, season.all_seasons, -1) -crayfish = create_fish("Crayfish", fresh_water, season.all_seasons, -1) -lobster = create_fish("Lobster", ocean, season.all_seasons, -1) -mussel = create_fish("Mussel", ocean, season.all_seasons, -1) -oyster = create_fish("Oyster", ocean, season.all_seasons, -1) -periwinkle = create_fish("Periwinkle", fresh_water, season.all_seasons, -1) -shrimp = create_fish("Shrimp", ocean, season.all_seasons, -1) -snail = create_fish("Snail", fresh_water, season.all_seasons, -1) - -legendary_fish = [angler, crimsonfish, glacierfish, legend, mutant_carp] -extended_family = [ms_angler, son_of_crimsonfish, glacierfish_jr, legend_ii, radioactive_carp] -special_fish = [*legendary_fish, blob_fish, lava_eel, octopus, scorpion_carp, ice_pip, super_cucumber, dorado] -island_fish = [lionfish, blue_discus, stingray, *extended_family] - -all_fish_by_name = {fish.name: fish for fish in all_fish} - - -def get_fish_for_mods(mods: Set[str]) -> List[FishItem]: - fish_for_mods = [] - for fish in all_fish: - if fish.mod_name and fish.mod_name not in mods: - continue - fish_for_mods.append(fish) - return fish_for_mods +clam = create_fish(Fish.clam, ocean, season.all_seasons, -1) +cockle = create_fish(Fish.cockle, ocean, season.all_seasons, -1) +crab = create_fish(Fish.crab, ocean, season.all_seasons, -1) +crayfish = create_fish(Fish.crayfish, fresh_water, season.all_seasons, -1) +lobster = create_fish(Fish.lobster, ocean, season.all_seasons, -1) +mussel = create_fish(Fish.mussel, ocean, season.all_seasons, -1) +oyster = create_fish(Fish.oyster, ocean, season.all_seasons, -1) +periwinkle = create_fish(Fish.periwinkle, fresh_water, season.all_seasons, -1) +shrimp = create_fish(Fish.shrimp, ocean, season.all_seasons, -1) +snail = create_fish(Fish.snail, fresh_water, season.all_seasons, -1) + +vanilla_legendary_fish = [angler, crimsonfish, glacierfish, legend, mutant_carp] diff --git a/worlds/stardew_valley/data/game_item.py b/worlds/stardew_valley/data/game_item.py new file mode 100644 index 000000000000..2107ca30d33a --- /dev/null +++ b/worlds/stardew_valley/data/game_item.py @@ -0,0 +1,86 @@ +import enum +import sys +from abc import ABC +from dataclasses import dataclass, field +from types import MappingProxyType +from typing import List, Iterable, Set, ClassVar, Tuple, Mapping, Callable, Any + +from ..stardew_rule.protocol import StardewRule + +if sys.version_info >= (3, 10): + kw_only = {"kw_only": True} +else: + kw_only = {} + +DEFAULT_REQUIREMENT_TAGS = MappingProxyType({}) + + +@dataclass(frozen=True) +class Requirement(ABC): + ... + + +class ItemTag(enum.Enum): + CROPSANITY_SEED = enum.auto() + CROPSANITY = enum.auto() + FISH = enum.auto() + FRUIT = enum.auto() + VEGETABLE = enum.auto() + EDIBLE_MUSHROOM = enum.auto() + BOOK = enum.auto() + BOOK_POWER = enum.auto() + BOOK_SKILL = enum.auto() + + +@dataclass(frozen=True) +class ItemSource(ABC): + add_tags: ClassVar[Tuple[ItemTag]] = () + + @property + def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: + return DEFAULT_REQUIREMENT_TAGS + + # FIXME this should just be an optional field, but kw_only requires python 3.10... + @property + def other_requirements(self) -> Iterable[Requirement]: + return () + + +@dataclass(frozen=True, **kw_only) +class GenericSource(ItemSource): + regions: Tuple[str, ...] = () + """No region means it's available everywhere.""" + other_requirements: Tuple[Requirement, ...] = () + + +@dataclass(frozen=True) +class CustomRuleSource(ItemSource): + """Hopefully once everything is migrated to sources, we won't need these custom logic anymore.""" + create_rule: Callable[[Any], StardewRule] + + +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.""" + tag: Tuple[ItemTag, ...] + + def __init__(self, *tag: ItemTag): + self.tag = tag # noqa + + @property + def add_tags(self): + return self.tag + + +@dataclass(frozen=True) +class GameItem: + name: str + sources: List[ItemSource] = field(default_factory=list) + tags: Set[ItemTag] = field(default_factory=set) + + def add_sources(self, sources: Iterable[ItemSource]): + self.sources.extend(source for source in sources if type(source) is not Tag) + for source in sources: + self.add_tags(source.add_tags) + + def add_tags(self, tags: Iterable[ItemTag]): + self.tags.update(tags) diff --git a/worlds/stardew_valley/data/harvest.py b/worlds/stardew_valley/data/harvest.py new file mode 100644 index 000000000000..087d7c3fa86b --- /dev/null +++ b/worlds/stardew_valley/data/harvest.py @@ -0,0 +1,66 @@ +from dataclasses import dataclass +from typing import Tuple, Sequence, Mapping + +from .game_item import ItemSource, kw_only, ItemTag, Requirement +from ..strings.season_names import Season + + +@dataclass(frozen=True, **kw_only) +class ForagingSource(ItemSource): + regions: Tuple[str, ...] + seasons: Tuple[str, ...] = Season.all + other_requirements: Tuple[Requirement, ...] = () + + +@dataclass(frozen=True, **kw_only) +class SeasonalForagingSource(ItemSource): + season: str + days: Sequence[int] + regions: Tuple[str, ...] + + def as_foraging_source(self) -> ForagingSource: + return ForagingSource(seasons=(self.season,), regions=self.regions) + + +@dataclass(frozen=True, **kw_only) +class FruitBatsSource(ItemSource): + ... + + +@dataclass(frozen=True, **kw_only) +class MushroomCaveSource(ItemSource): + ... + + +@dataclass(frozen=True, **kw_only) +class HarvestFruitTreeSource(ItemSource): + add_tags = (ItemTag.CROPSANITY,) + + sapling: str + seasons: Tuple[str, ...] = Season.all + + @property + def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: + return { + self.sapling: (ItemTag.CROPSANITY_SEED,) + } + + +@dataclass(frozen=True, **kw_only) +class HarvestCropSource(ItemSource): + add_tags = (ItemTag.CROPSANITY,) + + seed: str + seasons: Tuple[str, ...] = Season.all + """Empty means it can't be grown on the farm.""" + + @property + def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: + return { + self.seed: (ItemTag.CROPSANITY_SEED,) + } + + +@dataclass(frozen=True, **kw_only) +class ArtifactSpotSource(ItemSource): + amount: int diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index 9ecb2ba3649e..2604ad2c46bd 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -54,7 +54,7 @@ id,name,classification,groups,mod_name 68,Progressive Watering Can,progression,PROGRESSIVE_TOOLS, 69,Progressive Trash Can,progression,PROGRESSIVE_TOOLS, 70,Progressive Fishing Rod,progression,PROGRESSIVE_TOOLS, -71,Golden Scythe,useful,, +71,Golden Scythe,useful,DEPRECATED, 72,Progressive Mine Elevator,progression,, 73,Farming Level,progression,SKILL_LEVEL_UP, 74,Fishing Level,progression,SKILL_LEVEL_UP, @@ -92,8 +92,8 @@ id,name,classification,groups,mod_name 106,Galaxy Sword,filler,"WEAPON,DEPRECATED", 107,Galaxy Dagger,filler,"WEAPON,DEPRECATED", 108,Galaxy Hammer,filler,"WEAPON,DEPRECATED", -109,Movement Speed Bonus,progression,, -110,Luck Bonus,progression,, +109,Movement Speed Bonus,useful,, +110,Luck Bonus,filler,PLAYER_BUFF, 111,Lava Katana,filler,"WEAPON,DEPRECATED", 112,Progressive House,progression,, 113,Traveling Merchant: Sunday,progression,TRAVELING_MERCHANT_DAY, @@ -104,7 +104,7 @@ id,name,classification,groups,mod_name 118,Traveling Merchant: Friday,progression,TRAVELING_MERCHANT_DAY, 119,Traveling Merchant: Saturday,progression,TRAVELING_MERCHANT_DAY, 120,Traveling Merchant Stock Size,useful,, -121,Traveling Merchant Discount,useful,, +121,Traveling Merchant Discount,useful,DEPRECATED, 122,Return Scepter,useful,, 123,Progressive Season,progression,, 124,Spring,progression,SEASON, @@ -398,6 +398,7 @@ id,name,classification,groups,mod_name 417,Tropical Curry Recipe,progression,"CHEFSANITY,GINGER_ISLAND,CHEFSANITY_PURCHASE", 418,Trout Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS", 419,Vegetable Medley Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP", +420,Moss Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL", 425,Gate Recipe,progression,CRAFTSANITY, 426,Wood Fence Recipe,progression,CRAFTSANITY, 427,Deluxe Retaining Soil Recipe,progression,"CRAFTSANITY,GINGER_ISLAND", @@ -430,7 +431,7 @@ id,name,classification,groups,mod_name 454,Marble Brazier Recipe,progression,CRAFTSANITY, 455,Wood Lamp-post Recipe,progression,CRAFTSANITY, 456,Iron Lamp-post Recipe,progression,CRAFTSANITY, -457,Furnace Recipe,progression,CRAFTSANITY, +457,Furnace Recipe,progression,"CRAFTSANITY", 458,Wicked Statue Recipe,progression,CRAFTSANITY, 459,Chest Recipe,progression,CRAFTSANITY, 460,Wood Sign Recipe,progression,CRAFTSANITY, @@ -439,6 +440,75 @@ id,name,classification,groups,mod_name 470,Fruit Bats,progression,, 471,Mushroom Boxes,progression,, 475,The Gateway Gazette,progression,TV_CHANNEL, +476,Carrot Seeds,progression,CROPSANITY, +477,Summer Squash Seeds,progression,CROPSANITY, +478,Broccoli Seeds,progression,CROPSANITY, +479,Powdermelon Seeds,progression,CROPSANITY, +480,Progressive Raccoon,progression,, +481,Farming Mastery,progression,SKILL_MASTERY, +482,Mining Mastery,progression,SKILL_MASTERY, +483,Foraging Mastery,progression,SKILL_MASTERY, +484,Fishing Mastery,progression,SKILL_MASTERY, +485,Combat Mastery,progression,SKILL_MASTERY, +486,Fish Smoker Recipe,progression,CRAFTSANITY, +487,Dehydrator Recipe,progression,CRAFTSANITY, +488,Big Chest Recipe,progression,CRAFTSANITY, +489,Big Stone Chest Recipe,progression,CRAFTSANITY, +490,Text Sign Recipe,progression,CRAFTSANITY, +491,Blue Grass Starter Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND", +492,Mastery Of The Five Ways,progression,SKILL_MASTERY, +493,Progressive Scythe,useful,, +494,Progressive Pan,progression,PROGRESSIVE_TOOLS, +495,Calico Statue,filler,FESTIVAL, +496,Mummy Mask,filler,FESTIVAL, +497,Free Cactis,filler,FESTIVAL, +498,Gil's Hat,filler,FESTIVAL, +499,Bucket Hat,filler,FESTIVAL, +500,Mounted Trout,filler,FESTIVAL, +501,'Squid Kid',filler,FESTIVAL, +502,Squid Hat,filler,FESTIVAL, +503,Resource Pack: 200 Calico Egg,useful,"FESTIVAL", +504,Resource Pack: 120 Calico Egg,useful,"FESTIVAL", +505,Resource Pack: 100 Calico Egg,useful,"FESTIVAL", +506,Resource Pack: 50 Calico Egg,useful,"FESTIVAL", +507,Resource Pack: 40 Calico Egg,useful,"FESTIVAL", +508,Resource Pack: 35 Calico Egg,useful,"FESTIVAL", +509,Resource Pack: 30 Calico Egg,useful,"FESTIVAL", +510,Book: The Art O' Crabbing,useful,"FESTIVAL", +511,Mr Qi's Plane Ride,progression,, +521,Power: Price Catalogue,useful,"BOOK_POWER", +522,Power: Mapping Cave Systems,useful,"BOOK_POWER", +523,Power: Way Of The Wind pt. 1,progression,"BOOK_POWER", +524,Power: Way Of The Wind pt. 2,useful,"BOOK_POWER", +525,Power: Monster Compendium,useful,"BOOK_POWER", +526,Power: Friendship 101,useful,"BOOK_POWER", +527,"Power: Jack Be Nimble, Jack Be Thick",useful,"BOOK_POWER", +528,Power: Woody's Secret,useful,"BOOK_POWER", +529,Power: Raccoon Journal,useful,"BOOK_POWER", +530,Power: Jewels Of The Sea,useful,"BOOK_POWER", +531,Power: Dwarvish Safety Manual,useful,"BOOK_POWER", +532,Power: The Art O' Crabbing,useful,"BOOK_POWER", +533,Power: The Alleyway Buffet,useful,"BOOK_POWER", +534,Power: The Diamond Hunter,useful,"BOOK_POWER", +535,Power: Book of Mysteries,progression,"BOOK_POWER", +536,Power: Horse: The Book,useful,"BOOK_POWER", +537,Power: Treasure Appraisal Guide,useful,"BOOK_POWER", +538,Power: Ol' Slitherlegs,useful,"BOOK_POWER", +539,Power: Animal Catalogue,useful,"BOOK_POWER", +541,Progressive Lost Book,progression,"LOST_BOOK", +551,Golden Walnut,progression,"RESOURCE_PACK,GINGER_ISLAND", +552,3 Golden Walnuts,progression,"GINGER_ISLAND", +553,5 Golden Walnuts,progression,"GINGER_ISLAND", +554,Damage Bonus,filler,PLAYER_BUFF, +555,Defense Bonus,filler,PLAYER_BUFF, +556,Immunity Bonus,filler,PLAYER_BUFF, +557,Health Bonus,filler,PLAYER_BUFF, +558,Energy Bonus,filler,PLAYER_BUFF, +559,Bite Rate Bonus,filler,PLAYER_BUFF, +560,Fish Trap Bonus,filler,PLAYER_BUFF, +561,Fishing Bar Size Bonus,filler,PLAYER_BUFF, +562,Quality Bonus,filler,PLAYER_BUFF, +563,Glow Bonus,filler,PLAYER_BUFF, 4001,Burnt Trap,trap,TRAP, 4002,Darkness Trap,trap,TRAP, 4003,Frozen Trap,trap,TRAP, @@ -464,6 +534,8 @@ id,name,classification,groups,mod_name 4023,Benjamin Budton Trap,trap,TRAP, 4024,Inflation Trap,trap,TRAP, 4025,Bomb Trap,trap,TRAP, +4026,Nudge Trap,trap,TRAP, +4501,Deflation Bonus,filler,, 5000,Resource Pack: 500 Money,useful,"BASE_RESOURCE,RESOURCE_PACK", 5001,Resource Pack: 1000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK", 5002,Resource Pack: 1500 Money,useful,"BASE_RESOURCE,RESOURCE_PACK", @@ -701,9 +773,9 @@ id,name,classification,groups,mod_name 5234,Resource Pack: 10 Qi Seasoning,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5235,Mr. Qi's Hat,filler,"MAXIMUM_ONE,RESOURCE_PACK", 5236,Aquatic Sanctuary,filler,RESOURCE_PACK, +5237,Leprechaun Hat,filler,"MAXIMUM_ONE,RESOURCE_PACK", 5242,Exotic Double Bed,filler,RESOURCE_PACK, 5243,Resource Pack: 2 Qi Gem,filler,"GINGER_ISLAND,RESOURCE_PACK,DEPRECATED", -5245,Golden Walnut,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL,GINGER_ISLAND", 5247,Fairy Dust,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5248,Seed Maker,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5249,Keg,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", @@ -726,6 +798,27 @@ id,name,classification,groups,mod_name 5266,Resource Pack: 5 Staircase,filler,"RESOURCE_PACK", 5267,Auto-Petter,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5268,Auto-Grabber,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", +5269,Resource Pack: 10 Calico Egg,filler,"RESOURCE_PACK", +5270,Resource Pack: 20 Calico Egg,filler,"RESOURCE_PACK", +5272,Tent Kit,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", +5273,Resource Pack: 4 Mystery Box,filler,"RESOURCE_PACK", +5274,Prize Ticket,filler,"RESOURCE_PACK", +5275,Resource Pack: 20 Deluxe Bait,filler,"RESOURCE_PACK", +5276,Resource Pack: 2 Triple Shot Espresso,filler,"RESOURCE_PACK", +5277,Dish O' The Sea,filler,"RESOURCE_PACK", +5278,Seafoam Pudding,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", +5279,Trap Bobber,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", +5280,Treasure Chest,filler,"RESOURCE_PACK", +5281,Resource Pack: 15 Mixed Seeds,filler,"RESOURCE_PACK", +5282,Resource Pack: 15 Mixed Flower Seeds,filler,"RESOURCE_PACK", +5283,Resource Pack: 5 Cherry Bomb,filler,"RESOURCE_PACK", +5284,Resource Pack: 3 Bomb,filler,"RESOURCE_PACK", +5285,Resource Pack: 2 Mega Bomb,filler,"RESOURCE_PACK", +5286,Resource Pack: 2 Life Elixir,filler,"RESOURCE_PACK", +5287,Resource Pack: 5 Coffee,filler,"RESOURCE_PACK", +5289,Prismatic Shard,filler,"RESOURCE_PACK", +5290,Stardrop Tea,filler,"RESOURCE_PACK", +5291,Resource Pack: 2 Artifact Trove,filler,"RESOURCE_PACK", 10001,Luck Level,progression,SKILL_LEVEL_UP,Luck Skill 10002,Magic Level,progression,SKILL_LEVEL_UP,Magic 10003,Socializing Level,progression,SKILL_LEVEL_UP,Socializing Skill @@ -802,11 +895,16 @@ id,name,classification,groups,mod_name 10409,Void Delight Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded 10410,Void Salmon Sushi Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded 10411,Mushroom Kebab Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul -10412,Crayfish Soup Recipe,progression,,Distant Lands - Witch Swamp Overhaul +10412,Crayfish Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul 10413,Pemmican Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul 10414,Void Mint Tea Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul -10415,Ginger Tincture Recipe,progression,GINGER_ISLAND,Distant Lands - Witch Swamp Overhaul -10416,Special Pumpkin Soup Recipe,progression,,Boarding House and Bus Stop Extension +10415,Ginger Tincture Recipe,progression,"GINGER_ISLAND,CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul +10416,Special Pumpkin Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Boarding House and Bus Stop Extension +10417,Rocky Root Coffee Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +10418,Digger's Delight Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +10419,Ancient Jello Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +10420,Grilled Cheese Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill +10421,Fish Casserole Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill 10450,Void Mint Seeds,progression,DEPRECATED,Distant Lands - Witch Swamp Overhaul 10451,Vile Ancient Fruit Seeds,progression,DEPRECATED,Distant Lands - Witch Swamp Overhaul 10501,Marlon's Boat Paddle,progression,GINGER_ISLAND,Stardew Valley Expanded @@ -850,10 +948,15 @@ id,name,classification,groups,mod_name 10707,Resource Pack: 5 Wooden Display,filler,RESOURCE_PACK,Archaeology 10708,Grinder,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Archaeology 10709,Ancient Battery Production Station,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Archaeology -10710,Hero Elixir,filler,RESOURCE_PACK,Starde Valley Expanded +10710,Hero Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10711,Aegis Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10712,Haste Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10713,Lightning Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10714,Armor Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10715,Gravity Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10716,Barbarian Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded +10717,Restoration Table,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Archaeology +10718,Trash Bin,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill +10719,Composter,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill +10720,Recycling Bin,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill +10721,Advanced Recycling Machine,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 68667ac5c4bf..bb2ed2e2ce1f 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -36,6 +36,7 @@ id,region,name,tags,mod_name 35,Boiler Room,Complete Boiler Room,COMMUNITY_CENTER_ROOM, 36,Bulletin Board,Complete Bulletin Board,COMMUNITY_CENTER_ROOM, 37,Vault,Complete Vault,COMMUNITY_CENTER_ROOM, +38,Crafts Room,Forest Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE", 39,Fish Tank,Deep Fishing Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE", 40,Crafts Room,Beach Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE", 41,Crafts Room,Mines Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE", @@ -78,7 +79,6 @@ id,region,name,tags,mod_name 78,Vault,500g Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", 79,Vault,"1,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", 80,Vault,"2,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", -81,Vault,"5,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", 82,Vault,"1,500g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", 83,Vault,"3,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", 84,Vault,"3,500g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", @@ -124,6 +124,20 @@ id,region,name,tags,mod_name 124,Beach,Bamboo Pole Cutscene,"FISHING_ROD_UPGRADE,TOOL_UPGRADE", 125,Willy's Fish Shop,Purchase Fiberglass Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE", 126,Willy's Fish Shop,Purchase Iridium Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE", +127,Mountain,Copper Pan Cutscene,"TOOL_UPGRADE,PAN_UPGRADE", +128,Blacksmith Iron Upgrades,Iron Pan Upgrade,"TOOL_UPGRADE,PAN_UPGRADE", +129,Blacksmith Gold Upgrades,Gold Pan Upgrade,"TOOL_UPGRADE,PAN_UPGRADE", +130,Blacksmith Iridium Upgrades,Iridium Pan Upgrade,"TOOL_UPGRADE,PAN_UPGRADE", +151,Bulletin Board,Helper's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +152,Bulletin Board,Spirit's Eve Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +153,Bulletin Board,Winter Star Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +154,Bulletin Board,Calico Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +155,Pantry,Sommelier Bundle,"PANTRY_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +156,Pantry,Dry Bundle,"PANTRY_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +157,Fish Tank,Fish Smoker Bundle,"FISH_TANK_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +158,Bulletin Board,Raccoon Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +159,Crafts Room,Green Rain Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE", +160,Fish Tank,Specific Fishing Bundle,"FISH_TANK_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", 201,The Mines - Floor 10,The Mines Floor 10 Treasure,"MANDATORY,THE_MINES_TREASURE", 202,The Mines - Floor 20,The Mines Floor 20 Treasure,"MANDATORY,THE_MINES_TREASURE", 203,The Mines - Floor 40,The Mines Floor 40 Treasure,"MANDATORY,THE_MINES_TREASURE", @@ -161,18 +175,18 @@ id,region,name,tags,mod_name 235,The Mines - Floor 110,Floor 110 Elevator,ELEVATOR, 236,The Mines - Floor 115,Floor 115 Elevator,ELEVATOR, 237,The Mines - Floor 120,Floor 120 Elevator,ELEVATOR, -250,Shipping,Demetrius's Breakthrough,MANDATORY +250,Shipping,Demetrius's Breakthrough,MANDATORY, 251,Volcano - Floor 10,Volcano Caldera Treasure,"GINGER_ISLAND,MANDATORY", -301,Farming,Level 1 Farming,"FARMING_LEVEL,SKILL_LEVEL", -302,Farming,Level 2 Farming,"FARMING_LEVEL,SKILL_LEVEL", -303,Farming,Level 3 Farming,"FARMING_LEVEL,SKILL_LEVEL", -304,Farming,Level 4 Farming,"FARMING_LEVEL,SKILL_LEVEL", -305,Farming,Level 5 Farming,"FARMING_LEVEL,SKILL_LEVEL", -306,Farming,Level 6 Farming,"FARMING_LEVEL,SKILL_LEVEL", -307,Farming,Level 7 Farming,"FARMING_LEVEL,SKILL_LEVEL", -308,Farming,Level 8 Farming,"FARMING_LEVEL,SKILL_LEVEL", -309,Farming,Level 9 Farming,"FARMING_LEVEL,SKILL_LEVEL", -310,Farming,Level 10 Farming,"FARMING_LEVEL,SKILL_LEVEL", +301,Farm,Level 1 Farming,"FARMING_LEVEL,SKILL_LEVEL", +302,Farm,Level 2 Farming,"FARMING_LEVEL,SKILL_LEVEL", +303,Farm,Level 3 Farming,"FARMING_LEVEL,SKILL_LEVEL", +304,Farm,Level 4 Farming,"FARMING_LEVEL,SKILL_LEVEL", +305,Farm,Level 5 Farming,"FARMING_LEVEL,SKILL_LEVEL", +306,Farm,Level 6 Farming,"FARMING_LEVEL,SKILL_LEVEL", +307,Farm,Level 7 Farming,"FARMING_LEVEL,SKILL_LEVEL", +308,Farm,Level 8 Farming,"FARMING_LEVEL,SKILL_LEVEL", +309,Farm,Level 9 Farming,"FARMING_LEVEL,SKILL_LEVEL", +310,Farm,Level 10 Farming,"FARMING_LEVEL,SKILL_LEVEL", 311,Fishing,Level 1 Fishing,"FISHING_LEVEL,SKILL_LEVEL", 312,Fishing,Level 2 Fishing,"FISHING_LEVEL,SKILL_LEVEL", 313,Fishing,Level 3 Fishing,"FISHING_LEVEL,SKILL_LEVEL", @@ -213,6 +227,11 @@ id,region,name,tags,mod_name 348,The Mines - Floor 90,Level 8 Combat,"COMBAT_LEVEL,SKILL_LEVEL", 349,The Mines - Floor 100,Level 9 Combat,"COMBAT_LEVEL,SKILL_LEVEL", 350,The Mines - Floor 110,Level 10 Combat,"COMBAT_LEVEL,SKILL_LEVEL", +351,Mastery Cave,Farming Mastery,"MASTERY_LEVEL,SKILL_LEVEL", +352,Mastery Cave,Fishing Mastery,"MASTERY_LEVEL,SKILL_LEVEL", +353,Mastery Cave,Foraging Mastery,"MASTERY_LEVEL,SKILL_LEVEL", +354,Mastery Cave,Mining Mastery,"MASTERY_LEVEL,SKILL_LEVEL", +355,Mastery Cave,Combat Mastery,"MASTERY_LEVEL,SKILL_LEVEL", 401,Carpenter Shop,Coop Blueprint,BUILDING_BLUEPRINT, 402,Carpenter Shop,Big Coop Blueprint,BUILDING_BLUEPRINT, 403,Carpenter Shop,Deluxe Coop Blueprint,BUILDING_BLUEPRINT, @@ -279,6 +298,8 @@ id,region,name,tags,mod_name 546,Mutant Bug Lair,Dark Talisman,"STORY_QUEST", 547,Witch's Swamp,Goblin Problem,"STORY_QUEST", 548,Witch's Hut,Magic Ink,"STORY_QUEST", +549,Forest,The Giant Stump,"STORY_QUEST", +550,Farm,Feeding Animals,"STORY_QUEST", 601,JotPK World 1,JotPK: Boots 1,"ARCADE_MACHINE,JOTPK", 602,JotPK World 1,JotPK: Boots 2,"ARCADE_MACHINE,JOTPK", 603,JotPK World 1,JotPK: Gun 1,"ARCADE_MACHINE,JOTPK", @@ -307,6 +328,7 @@ id,region,name,tags,mod_name 705,Farmhouse,Have Another Baby,BABY, 706,Farmhouse,Spouse Stardrop,, 707,Sewer,Krobus Stardrop,MANDATORY, +708,Forest,Pot Of Gold,MANDATORY, 801,Forest,Help Wanted: Gathering 1,HELP_WANTED, 802,Forest,Help Wanted: Gathering 2,HELP_WANTED, 803,Forest,Help Wanted: Gathering 3,HELP_WANTED, @@ -454,6 +476,7 @@ id,region,name,tags,mod_name 1068,Beach,Fishsanity: Legend II,"FISHSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS", 1069,Beach,Fishsanity: Ms. Angler,"FISHSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS", 1070,Beach,Fishsanity: Radioactive Carp,"FISHSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS", +1071,Fishing,Fishsanity: Goby,FISHSANITY, 1100,Museum,Museumsanity: 5 Donations,MUSEUM_MILESTONES, 1101,Museum,Museumsanity: 10 Donations,MUSEUM_MILESTONES, 1102,Museum,Museumsanity: 15 Donations,MUSEUM_MILESTONES, @@ -1021,6 +1044,57 @@ id,region,name,tags,mod_name 2034,Dance of the Moonlight Jellies,Moonlight Jellies Banner,FESTIVAL, 2035,Dance of the Moonlight Jellies,Starport Decal,FESTIVAL, 2036,Casino,Rarecrow #3 (Alien),FESTIVAL, +2041,Desert Festival,Calico Race,FESTIVAL, +2042,Desert Festival,Mummy Mask,FESTIVAL_HARD, +2043,Desert Festival,Calico Statue,FESTIVAL, +2044,Desert Festival,Emily's Outfit Services,FESTIVAL, +2045,Desert Festival,Earthy Mousse,DESERT_FESTIVAL_CHEF, +2046,Desert Festival,Sweet Bean Cake,DESERT_FESTIVAL_CHEF, +2047,Desert Festival,Skull Cave Casserole,DESERT_FESTIVAL_CHEF, +2048,Desert Festival,Spicy Tacos,DESERT_FESTIVAL_CHEF, +2049,Desert Festival,Mountain Chili,DESERT_FESTIVAL_CHEF, +2050,Desert Festival,Crystal Cake,DESERT_FESTIVAL_CHEF, +2051,Desert Festival,Cave Kebab,DESERT_FESTIVAL_CHEF, +2052,Desert Festival,Hot Log,DESERT_FESTIVAL_CHEF, +2053,Desert Festival,Sour Salad,DESERT_FESTIVAL_CHEF, +2054,Desert Festival,Superfood Cake,DESERT_FESTIVAL_CHEF, +2055,Desert Festival,Warrior Smoothie,DESERT_FESTIVAL_CHEF, +2056,Desert Festival,Rumpled Fruit Skin,DESERT_FESTIVAL_CHEF, +2057,Desert Festival,Calico Pizza,DESERT_FESTIVAL_CHEF, +2058,Desert Festival,Stuffed Mushrooms,DESERT_FESTIVAL_CHEF, +2059,Desert Festival,Elf Quesadilla,DESERT_FESTIVAL_CHEF, +2060,Desert Festival,Nachos Of The Desert,DESERT_FESTIVAL_CHEF, +2061,Desert Festival,Cioppino,DESERT_FESTIVAL_CHEF, +2062,Desert Festival,Rainforest Shrimp,DESERT_FESTIVAL_CHEF, +2063,Desert Festival,Shrimp Donut,DESERT_FESTIVAL_CHEF, +2064,Desert Festival,Smell Of The Sea,DESERT_FESTIVAL_CHEF, +2065,Desert Festival,Desert Gumbo,DESERT_FESTIVAL_CHEF, +2066,Desert Festival,Free Cactis,FESTIVAL, +2067,Desert Festival,Monster Hunt,FESTIVAL_HARD, +2068,Desert Festival,Deep Dive,FESTIVAL_HARD, +2069,Desert Festival,Treasure Hunt,FESTIVAL_HARD, +2070,Desert Festival,Touch A Calico Statue,FESTIVAL, +2071,Desert Festival,Real Calico Egg Hunter,FESTIVAL, +2072,Desert Festival,Willy's Challenge,FESTIVAL_HARD, +2073,Desert Festival,Desert Scholar,FESTIVAL, +2074,Trout Derby,Trout Derby Reward 1,FESTIVAL, +2075,Trout Derby,Trout Derby Reward 2,FESTIVAL, +2076,Trout Derby,Trout Derby Reward 3,FESTIVAL, +2077,Trout Derby,Trout Derby Reward 4,FESTIVAL_HARD, +2078,Trout Derby,Trout Derby Reward 5,FESTIVAL_HARD, +2079,Trout Derby,Trout Derby Reward 6,FESTIVAL_HARD, +2080,Trout Derby,Trout Derby Reward 7,FESTIVAL_HARD, +2081,Trout Derby,Trout Derby Reward 8,FESTIVAL_HARD, +2082,Trout Derby,Trout Derby Reward 9,FESTIVAL_HARD, +2083,Trout Derby,Trout Derby Reward 10,FESTIVAL_HARD, +2084,SquidFest,SquidFest Day 1 Copper,FESTIVAL, +2085,SquidFest,SquidFest Day 1 Iron,FESTIVAL, +2086,SquidFest,SquidFest Day 1 Gold,FESTIVAL_HARD, +2087,SquidFest,SquidFest Day 1 Iridium,FESTIVAL_HARD, +2088,SquidFest,SquidFest Day 2 Copper,FESTIVAL, +2089,SquidFest,SquidFest Day 2 Iron,FESTIVAL, +2090,SquidFest,SquidFest Day 2 Gold,FESTIVAL_HARD, +2091,SquidFest,SquidFest Day 2 Iridium,FESTIVAL_HARD, 2101,Town,Island Ingredients,"GINGER_ISLAND,SPECIAL_ORDER_BOARD", 2102,The Mines - Floor 75,Cave Patrol,SPECIAL_ORDER_BOARD, 2103,Fishing,Aquatic Overpopulation,SPECIAL_ORDER_BOARD, @@ -1065,53 +1139,59 @@ id,region,name,tags,mod_name 2214,Island West,Parrot Express,"GINGER_ISLAND,WALNUT_PURCHASE", 2215,Dig Site,Open Professor Snail Cave,GINGER_ISLAND, 2216,Field Office,Complete Island Field Office,GINGER_ISLAND, -2301,Farming,Harvest Amaranth,CROPSANITY, -2302,Farming,Harvest Artichoke,CROPSANITY, -2303,Farming,Harvest Beet,CROPSANITY, -2304,Farming,Harvest Blue Jazz,CROPSANITY, -2305,Farming,Harvest Blueberry,CROPSANITY, -2306,Farming,Harvest Bok Choy,CROPSANITY, -2307,Farming,Harvest Cauliflower,CROPSANITY, -2308,Farming,Harvest Corn,CROPSANITY, -2309,Farming,Harvest Cranberries,CROPSANITY, -2310,Farming,Harvest Eggplant,CROPSANITY, -2311,Farming,Harvest Fairy Rose,CROPSANITY, -2312,Farming,Harvest Garlic,CROPSANITY, -2313,Farming,Harvest Grape,CROPSANITY, -2314,Farming,Harvest Green Bean,CROPSANITY, -2315,Farming,Harvest Hops,CROPSANITY, -2316,Farming,Harvest Hot Pepper,CROPSANITY, -2317,Farming,Harvest Kale,CROPSANITY, -2318,Farming,Harvest Melon,CROPSANITY, -2319,Farming,Harvest Parsnip,CROPSANITY, -2320,Farming,Harvest Poppy,CROPSANITY, -2321,Farming,Harvest Potato,CROPSANITY, -2322,Farming,Harvest Pumpkin,CROPSANITY, -2323,Farming,Harvest Radish,CROPSANITY, -2324,Farming,Harvest Red Cabbage,CROPSANITY, -2325,Farming,Harvest Rhubarb,CROPSANITY, -2326,Farming,Harvest Starfruit,CROPSANITY, -2327,Farming,Harvest Strawberry,CROPSANITY, -2328,Farming,Harvest Summer Spangle,CROPSANITY, -2329,Farming,Harvest Sunflower,CROPSANITY, -2330,Farming,Harvest Tomato,CROPSANITY, -2331,Farming,Harvest Tulip,CROPSANITY, -2332,Farming,Harvest Unmilled Rice,CROPSANITY, -2333,Farming,Harvest Wheat,CROPSANITY, -2334,Farming,Harvest Yam,CROPSANITY, -2335,Farming,Harvest Cactus Fruit,CROPSANITY, -2336,Farming,Harvest Pineapple,"CROPSANITY,GINGER_ISLAND", -2337,Farming,Harvest Taro Root,"CROPSANITY,GINGER_ISLAND", -2338,Farming,Harvest Sweet Gem Berry,CROPSANITY, -2339,Farming,Harvest Apple,CROPSANITY, -2340,Farming,Harvest Apricot,CROPSANITY, -2341,Farming,Harvest Cherry,CROPSANITY, -2342,Farming,Harvest Orange,CROPSANITY, -2343,Farming,Harvest Pomegranate,CROPSANITY, -2344,Farming,Harvest Peach,CROPSANITY, -2345,Farming,Harvest Banana,"CROPSANITY,GINGER_ISLAND", -2346,Farming,Harvest Mango,"CROPSANITY,GINGER_ISLAND", -2347,Farming,Harvest Coffee Bean,CROPSANITY, +2301,Fall Farming,Harvest Amaranth,CROPSANITY, +2302,Fall Farming,Harvest Artichoke,CROPSANITY, +2303,Fall Farming,Harvest Beet,CROPSANITY, +2304,Spring Farming,Harvest Blue Jazz,CROPSANITY, +2305,Summer Farming,Harvest Blueberry,CROPSANITY, +2306,Fall Farming,Harvest Bok Choy,CROPSANITY, +2307,Spring Farming,Harvest Cauliflower,CROPSANITY, +2308,Summer or Fall Farming,Harvest Corn,CROPSANITY, +2309,Fall Farming,Harvest Cranberries,CROPSANITY, +2310,Fall Farming,Harvest Eggplant,CROPSANITY, +2311,Fall Farming,Harvest Fairy Rose,CROPSANITY, +2312,Spring Farming,Harvest Garlic,CROPSANITY, +2313,Fall Farming,Harvest Grape,CROPSANITY, +2314,Spring Farming,Harvest Green Bean,CROPSANITY, +2315,Summer Farming,Harvest Hops,CROPSANITY, +2316,Summer Farming,Harvest Hot Pepper,CROPSANITY, +2317,Spring Farming,Harvest Kale,CROPSANITY, +2318,Summer Farming,Harvest Melon,CROPSANITY, +2319,Spring Farming,Harvest Parsnip,CROPSANITY, +2320,Summer Farming,Harvest Poppy,CROPSANITY, +2321,Spring Farming,Harvest Potato,CROPSANITY, +2322,Fall Farming,Harvest Pumpkin,CROPSANITY, +2323,Summer Farming,Harvest Radish,CROPSANITY, +2324,Summer Farming,Harvest Red Cabbage,CROPSANITY, +2325,Spring Farming,Harvest Rhubarb,CROPSANITY, +2326,Summer Farming,Harvest Starfruit,CROPSANITY, +2327,Spring Farming,Harvest Strawberry,CROPSANITY, +2328,Summer Farming,Harvest Summer Spangle,CROPSANITY, +2329,Summer or Fall Farming,Harvest Sunflower,CROPSANITY, +2330,Summer Farming,Harvest Tomato,CROPSANITY, +2331,Spring Farming,Harvest Tulip,CROPSANITY, +2332,Spring Farming,Harvest Unmilled Rice,CROPSANITY, +2333,Summer or Fall Farming,Harvest Wheat,CROPSANITY, +2334,Fall Farming,Harvest Yam,CROPSANITY, +2335,Indoor Farming,Harvest Cactus Fruit,CROPSANITY, +2336,Summer Farming,Harvest Pineapple,"CROPSANITY,GINGER_ISLAND", +2337,Summer Farming,Harvest Taro Root,"CROPSANITY,GINGER_ISLAND", +2338,Fall Farming,Harvest Sweet Gem Berry,CROPSANITY, +2339,Fall Farming,Harvest Apple,CROPSANITY, +2340,Spring Farming,Harvest Apricot,CROPSANITY, +2341,Spring Farming,Harvest Cherry,CROPSANITY, +2342,Summer Farming,Harvest Orange,CROPSANITY, +2343,Fall Farming,Harvest Pomegranate,CROPSANITY, +2344,Summer Farming,Harvest Peach,CROPSANITY, +2345,Summer Farming,Harvest Banana,"CROPSANITY,GINGER_ISLAND", +2346,Summer Farming,Harvest Mango,"CROPSANITY,GINGER_ISLAND", +2347,Indoor Farming,Harvest Coffee Bean,CROPSANITY, +2348,Fall Farming,Harvest Broccoli,CROPSANITY, +2349,Spring Farming,Harvest Carrot,CROPSANITY, +2350,Summer Farming,Harvest Powdermelon,CROPSANITY, +2351,Summer Farming,Harvest Summer Squash,CROPSANITY, +2352,Indoor Farming,Harvest Ancient Fruit,CROPSANITY, +2353,Indoor Farming,Harvest Qi Fruit,"CROPSANITY,GINGER_ISLAND", 2401,Shipping,Shipsanity: Duck Egg,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", 2402,Shipping,Shipsanity: Duck Feather,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", 2403,Shipping,Shipsanity: Egg,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", @@ -1431,7 +1511,7 @@ id,region,name,tags,mod_name 2717,Shipping,Shipsanity: Cactus Fruit,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", 2718,Shipping,Shipsanity: Cave Carrot,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", 2719,Shipping,Shipsanity: Chanterelle,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", -2720,Shipping,Shipsanity: Clam,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +2720,Shipping,Shipsanity: Clam,"SHIPSANITY,SHIPSANITY_FISH", 2721,Shipping,Shipsanity: Coconut,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", 2722,Shipping,Shipsanity: Common Mushroom,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", 2723,Shipping,Shipsanity: Coral,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", @@ -1683,7 +1763,7 @@ id,region,name,tags,mod_name 2969,Shipping,Shipsanity: Mango Sapling,"GINGER_ISLAND,SHIPSANITY", 2970,Shipping,Shipsanity: Mushroom Tree Seed,"GINGER_ISLAND,SHIPSANITY,REQUIRES_QI_ORDERS", 2971,Shipping,Shipsanity: Pineapple Seeds,"GINGER_ISLAND,SHIPSANITY", -2972,Shipping,Shipsanity: Qi Bean,"GINGER_ISLAND,SHIPSANITY", +2972,Shipping,Shipsanity: Qi Bean,"GINGER_ISLAND,SHIPSANITY,REQUIRES_QI_ORDERS", 2973,Shipping,Shipsanity: Taro Tuber,"GINGER_ISLAND,SHIPSANITY", 3001,Adventurer's Guild,Monster Eradication: Slimes,"MONSTERSANITY,MONSTERSANITY_GOALS", 3002,Adventurer's Guild,Monster Eradication: Void Spirits,"MONSTERSANITY,MONSTERSANITY_GOALS", @@ -1852,6 +1932,7 @@ id,region,name,tags,mod_name 3278,Kitchen,Cook Tropical Curry,"COOKSANITY,GINGER_ISLAND", 3279,Kitchen,Cook Trout Soup,"COOKSANITY,COOKSANITY_QOS", 3280,Kitchen,Cook Vegetable Medley,COOKSANITY, +3281,Kitchen,Cook Moss Soup,COOKSANITY, 3301,Farm,Algae Soup Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP", 3302,The Queen of Sauce,Artichoke Dip Recipe,"CHEFSANITY,CHEFSANITY_QOS", 3303,Farm,Autumn's Bounty Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP", @@ -1932,6 +2013,7 @@ id,region,name,tags,mod_name 3378,Island Resort,Tropical Curry Recipe,"CHEFSANITY,GINGER_ISLAND,CHEFSANITY_PURCHASE", 3379,The Queen of Sauce,Trout Soup Recipe,"CHEFSANITY,CHEFSANITY_QOS", 3380,Farm,Vegetable Medley Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP", +3381,Farm,Moss Soup Recipe,"CHEFSANITY,CHEFSANITY_SKILL", 3401,Farm,Craft Cherry Bomb,CRAFTSANITY, 3402,Farm,Craft Bomb,CRAFTSANITY, 3403,Farm,Craft Mega Bomb,CRAFTSANITY, @@ -2062,6 +2144,26 @@ id,region,name,tags,mod_name 3528,Farm,Craft Farm Computer,CRAFTSANITY, 3529,Farm,Craft Hopper,"CRAFTSANITY,GINGER_ISLAND", 3530,Farm,Craft Cookout Kit,CRAFTSANITY, +3531,Farm,Craft Fish Smoker,"CRAFTSANITY", +3532,Farm,Craft Dehydrator,"CRAFTSANITY", +3533,Farm,Craft Blue Grass Starter,"CRAFTSANITY,GINGER_ISLAND", +3534,Farm,Craft Mystic Tree Seed,"CRAFTSANITY,REQUIRES_MASTERIES", +3535,Farm,Craft Sonar Bobber,"CRAFTSANITY", +3536,Farm,Craft Challenge Bait,"CRAFTSANITY,REQUIRES_MASTERIES", +3537,Farm,Craft Treasure Totem,"CRAFTSANITY,REQUIRES_MASTERIES", +3538,Farm,Craft Heavy Furnace,"CRAFTSANITY,REQUIRES_MASTERIES", +3539,Farm,Craft Deluxe Worm Bin,"CRAFTSANITY", +3540,Farm,Craft Mushroom Log,"CRAFTSANITY", +3541,Farm,Craft Big Chest,"CRAFTSANITY", +3542,Farm,Craft Big Stone Chest,"CRAFTSANITY", +3543,Farm,Craft Text Sign,"CRAFTSANITY", +3544,Farm,Craft Tent Kit,"CRAFTSANITY", +3545,Farm,Craft Statue Of The Dwarf King,"CRAFTSANITY,REQUIRES_MASTERIES", +3546,Farm,Craft Statue Of Blessings,"CRAFTSANITY,REQUIRES_MASTERIES", +3547,Farm,Craft Anvil,"CRAFTSANITY,REQUIRES_MASTERIES", +3548,Farm,Craft Mini-Forge,"CRAFTSANITY,GINGER_ISLAND,REQUIRES_MASTERIES", +3549,Farm,Craft Deluxe Bait,"CRAFTSANITY", +3550,Farm,Craft Bait Maker,"CRAFTSANITY", 3551,Pierre's General Store,Grass Starter Recipe,CRAFTSANITY, 3552,Carpenter Shop,Wood Floor Recipe,CRAFTSANITY, 3553,Carpenter Shop,Rustic Plank Floor Recipe,CRAFTSANITY, @@ -2088,6 +2190,226 @@ id,region,name,tags,mod_name 3574,Sewer,Wicked Statue Recipe,CRAFTSANITY, 3575,Desert,Warp Totem: Desert Recipe,"CRAFTSANITY", 3576,Island Trader,Deluxe Retaining Soil Recipe,"CRAFTSANITY,GINGER_ISLAND", +3577,Willy's Fish Shop,Fish Smoker Recipe,CRAFTSANITY, +3578,Pierre's General Store,Dehydrator Recipe,CRAFTSANITY, +3579,Carpenter Shop,Big Chest Recipe,CRAFTSANITY, +3580,Mines Dwarf Shop,Big Stone Chest Recipe,CRAFTSANITY, +3701,Raccoon Bundles,Raccoon Request 1,"BUNDLE,RACCOON_BUNDLES", +3702,Raccoon Bundles,Raccoon Request 2,"BUNDLE,RACCOON_BUNDLES", +3703,Raccoon Bundles,Raccoon Request 3,"BUNDLE,RACCOON_BUNDLES", +3704,Raccoon Bundles,Raccoon Request 4,"BUNDLE,RACCOON_BUNDLES", +3705,Raccoon Bundles,Raccoon Request 5,"BUNDLE,RACCOON_BUNDLES", +3706,Raccoon Bundles,Raccoon Request 6,"BUNDLE,RACCOON_BUNDLES", +3707,Raccoon Bundles,Raccoon Request 7,"BUNDLE,RACCOON_BUNDLES", +3708,Raccoon Bundles,Raccoon Request 8,"BUNDLE,RACCOON_BUNDLES", +3801,Shipping,Shipsanity: Goby,"SHIPSANITY,SHIPSANITY_FISH", +3802,Shipping,Shipsanity: Fireworks (Red),"SHIPSANITY", +3803,Shipping,Shipsanity: Fireworks (Purple),"SHIPSANITY", +3804,Shipping,Shipsanity: Fireworks (Green),"SHIPSANITY", +3805,Shipping,Shipsanity: Far Away Stone,"SHIPSANITY", +3806,Shipping,Shipsanity: Calico Egg,"SHIPSANITY", +3807,Shipping,Shipsanity: Mixed Flower Seeds,"SHIPSANITY", +3808,Shipping,Shipsanity: Mystery Box,"SHIPSANITY", +3809,Shipping,Shipsanity: Golden Tag,"SHIPSANITY", +3810,Shipping,Shipsanity: Deluxe Bait,"SHIPSANITY", +3811,Shipping,Shipsanity: Moss,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +3812,Shipping,Shipsanity: Mossy Seed,"SHIPSANITY", +3813,Shipping,Shipsanity: Sonar Bobber,"SHIPSANITY", +3814,Shipping,Shipsanity: Tent Kit,"SHIPSANITY", +3815,Shipping,Shipsanity: Mystic Tree Seed,"SHIPSANITY,REQUIRES_MASTERIES", +3816,Shipping,Shipsanity: Mystic Syrup,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3817,Shipping,Shipsanity: Raisins,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3818,Shipping,Shipsanity: Dried Fruit,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3819,Shipping,Shipsanity: Dried Mushrooms,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3820,Shipping,Shipsanity: Stardrop Tea,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3821,Shipping,Shipsanity: Prize Ticket,"SHIPSANITY", +3822,Shipping,Shipsanity: Treasure Totem,"SHIPSANITY,REQUIRES_MASTERIES", +3823,Shipping,Shipsanity: Challenge Bait,"SHIPSANITY,REQUIRES_MASTERIES", +3824,Shipping,Shipsanity: Carrot Seeds,"SHIPSANITY", +3825,Shipping,Shipsanity: Carrot,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +3826,Shipping,Shipsanity: Summer Squash Seeds,"SHIPSANITY", +3827,Shipping,Shipsanity: Summer Squash,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +3828,Shipping,Shipsanity: Broccoli Seeds,"SHIPSANITY", +3829,Shipping,Shipsanity: Broccoli,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +3830,Shipping,Shipsanity: Powdermelon Seeds,"SHIPSANITY", +3831,Shipping,Shipsanity: Powdermelon,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +3832,Shipping,Shipsanity: Smoked Fish,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3833,Shipping,Shipsanity: Book Of Stars,"SHIPSANITY", +3834,Shipping,Shipsanity: Stardew Valley Almanac,"SHIPSANITY", +3835,Shipping,Shipsanity: Woodcutter's Weekly,"SHIPSANITY", +3836,Shipping,Shipsanity: Bait And Bobber,"SHIPSANITY", +3837,Shipping,Shipsanity: Mining Monthly,"SHIPSANITY", +3838,Shipping,Shipsanity: Combat Quarterly,"SHIPSANITY", +3839,Shipping,Shipsanity: The Alleyway Buffet,"SHIPSANITY", +3840,Shipping,Shipsanity: The Art O' Crabbing,"SHIPSANITY", +3841,Shipping,Shipsanity: Dwarvish Safety Manual,"SHIPSANITY", +3842,Shipping,Shipsanity: Jewels Of The Sea,"SHIPSANITY", +3843,Shipping,Shipsanity: Raccoon Journal,"SHIPSANITY", +3844,Shipping,Shipsanity: Woody's Secret,"SHIPSANITY", +3845,Shipping,"Shipsanity: Jack Be Nimble, Jack Be Thick","SHIPSANITY", +3846,Shipping,Shipsanity: Friendship 101,"SHIPSANITY", +3847,Shipping,Shipsanity: Monster Compendium,"SHIPSANITY", +3848,Shipping,Shipsanity: Way Of The Wind pt. 1,"SHIPSANITY", +3849,Shipping,Shipsanity: Mapping Cave Systems,"SHIPSANITY", +3850,Shipping,Shipsanity: Price Catalogue,"SHIPSANITY", +3851,Shipping,Shipsanity: Queen Of Sauce Cookbook,"SHIPSANITY", +3852,Shipping,Shipsanity: The Diamond Hunter,"SHIPSANITY,GINGER_ISLAND", +3853,Shipping,Shipsanity: Book of Mysteries,"SHIPSANITY", +3854,Shipping,Shipsanity: Animal Catalogue,"SHIPSANITY", +3855,Shipping,Shipsanity: Way Of The Wind pt. 2,"SHIPSANITY", +3856,Shipping,Shipsanity: Golden Animal Cracker,"SHIPSANITY,REQUIRES_MASTERIES", +3857,Shipping,Shipsanity: Golden Mystery Box,"SHIPSANITY,REQUIRES_MASTERIES", +3858,Shipping,Shipsanity: Sea Jelly,"SHIPSANITY,SHIPSANITY_FISH", +3859,Shipping,Shipsanity: Cave Jelly,"SHIPSANITY,SHIPSANITY_FISH", +3860,Shipping,Shipsanity: River Jelly,"SHIPSANITY,SHIPSANITY_FISH", +3861,Shipping,Shipsanity: Treasure Appraisal Guide,"SHIPSANITY", +3862,Shipping,Shipsanity: Horse: The Book,"SHIPSANITY", +3863,Shipping,Shipsanity: Butterfly Powder,"SHIPSANITY", +3864,Shipping,Shipsanity: Blue Grass Starter,"SHIPSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS", +3865,Shipping,Shipsanity: Moss Soup,"SHIPSANITY", +3866,Shipping,Shipsanity: Ol' Slitherlegs,"SHIPSANITY", +3867,Shipping,Shipsanity: Targeted Bait,"SHIPSANITY", +4001,Farm,Read Price Catalogue,"BOOKSANITY,BOOKSANITY_POWER", +4002,Farm,Read Mapping Cave Systems,"BOOKSANITY,BOOKSANITY_POWER", +4003,Farm,Read Way Of The Wind pt. 1,"BOOKSANITY,BOOKSANITY_POWER", +4004,Farm,Read Way Of The Wind pt. 2,"BOOKSANITY,BOOKSANITY_POWER", +4005,Farm,Read Monster Compendium,"BOOKSANITY,BOOKSANITY_POWER", +4006,Farm,Read Friendship 101,"BOOKSANITY,BOOKSANITY_POWER", +4007,Farm,"Read Jack Be Nimble, Jack Be Thick","BOOKSANITY,BOOKSANITY_POWER", +4008,Farm,Read Woody's Secret,"BOOKSANITY,BOOKSANITY_POWER", +4009,Farm,Read Raccoon Journal,"BOOKSANITY,BOOKSANITY_POWER", +4010,Farm,Read Jewels Of The Sea,"BOOKSANITY,BOOKSANITY_POWER", +4011,Farm,Read Dwarvish Safety Manual,"BOOKSANITY,BOOKSANITY_POWER", +4012,Farm,Read The Art O' Crabbing,"BOOKSANITY,BOOKSANITY_POWER", +4013,Farm,Read The Alleyway Buffet,"BOOKSANITY,BOOKSANITY_POWER", +4014,Farm,Read The Diamond Hunter,"BOOKSANITY,BOOKSANITY_POWER,GINGER_ISLAND", +4015,Farm,Read Book of Mysteries,"BOOKSANITY,BOOKSANITY_POWER", +4016,Farm,Read Horse: The Book,"BOOKSANITY,BOOKSANITY_POWER", +4017,Farm,Read Treasure Appraisal Guide,"BOOKSANITY,BOOKSANITY_POWER", +4018,Farm,Read Ol' Slitherlegs,"BOOKSANITY,BOOKSANITY_POWER", +4019,Farm,Read Animal Catalogue,"BOOKSANITY,BOOKSANITY_POWER", +4031,Farm,Read Bait And Bobber,"BOOKSANITY,BOOKSANITY_SKILL", +4032,Farm,Read Book Of Stars,"BOOKSANITY,BOOKSANITY_SKILL", +4033,Farm,Read Combat Quarterly,"BOOKSANITY,BOOKSANITY_SKILL", +4034,Farm,Read Mining Monthly,"BOOKSANITY,BOOKSANITY_SKILL", +4035,Farm,Read Queen Of Sauce Cookbook,"BOOKSANITY,BOOKSANITY_SKILL", +4036,Farm,Read Stardew Valley Almanac,"BOOKSANITY,BOOKSANITY_SKILL", +4037,Farm,Read Woodcutter's Weekly,"BOOKSANITY,BOOKSANITY_SKILL", +4051,Museum,Read Tips on Farming,"BOOKSANITY,BOOKSANITY_LOST", +4052,Museum,Read This is a book by Marnie,"BOOKSANITY,BOOKSANITY_LOST", +4053,Museum,Read On Foraging,"BOOKSANITY,BOOKSANITY_LOST", +4054,Museum,"Read The Fisherman, Act 1","BOOKSANITY,BOOKSANITY_LOST", +4055,Museum,Read How Deep do the mines go?,"BOOKSANITY,BOOKSANITY_LOST", +4056,Museum,Read An Old Farmer's Journal,"BOOKSANITY,BOOKSANITY_LOST", +4057,Museum,Read Scarecrows,"BOOKSANITY,BOOKSANITY_LOST", +4058,Museum,Read The Secret of the Stardrop,"BOOKSANITY,BOOKSANITY_LOST", +4059,Museum,Read Journey of the Prairie King -- The Smash Hit Video Game!,"BOOKSANITY,BOOKSANITY_LOST", +4060,Museum,Read A Study on Diamond Yields,"BOOKSANITY,BOOKSANITY_LOST", +4061,Museum,Read Brewmaster's Guide,"BOOKSANITY,BOOKSANITY_LOST", +4062,Museum,Read Mysteries of the Dwarves,"BOOKSANITY,BOOKSANITY_LOST", +4063,Museum,Read Highlights From The Book of Yoba,"BOOKSANITY,BOOKSANITY_LOST", +4064,Museum,Read Marriage Guide for Farmers,"BOOKSANITY,BOOKSANITY_LOST", +4065,Museum,"Read The Fisherman, Act II","BOOKSANITY,BOOKSANITY_LOST", +4066,Museum,Read Technology Report!,"BOOKSANITY,BOOKSANITY_LOST", +4067,Museum,Read Secrets of the Legendary Fish,"BOOKSANITY,BOOKSANITY_LOST", +4068,Museum,Read Gunther Tunnel Notice,"BOOKSANITY,BOOKSANITY_LOST", +4069,Museum,Read Note From Gunther,"BOOKSANITY,BOOKSANITY_LOST", +4070,Museum,Read Goblins by M. Jasper,"BOOKSANITY,BOOKSANITY_LOST", +4071,Museum,Read Secret Statues Acrostics,"BOOKSANITY,BOOKSANITY_LOST", +4101,Clint's Blacksmith,Open Golden Coconut,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4102,Island West,Fishing Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4103,Island West,Fishing Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4104,Island North,Fishing Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4105,Island North,Fishing Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4106,Island Southeast,Fishing Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4107,Island East,Jungle Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4108,Island East,Banana Altar,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4109,Leo's Hut,Leo's Tree,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4110,Island Shrine,Gem Birds Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4111,Island Shrine,Gem Birds Shrine,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4112,Island West,Harvesting Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4113,Island West,Harvesting Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4114,Island West,Harvesting Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4115,Island West,Harvesting Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4116,Island West,Harvesting Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4117,Gourmand Frog Cave,Gourmand Frog Melon,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4118,Gourmand Frog Cave,Gourmand Frog Wheat,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4119,Gourmand Frog Cave,Gourmand Frog Garlic,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4120,Island West,Journal Scrap #6,"WALNUTSANITY,WALNUTSANITY_DIG", +4121,Island West,Mussel Node Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4122,Island West,Mussel Node Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4123,Island West,Mussel Node Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4124,Island West,Mussel Node Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4125,Island West,Mussel Node Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4126,Shipwreck,Shipwreck Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4127,Island West,Whack A Mole,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4128,Island West,Starfish Triangle,"WALNUTSANITY,WALNUTSANITY_DIG", +4129,Island West,Starfish Diamond,"WALNUTSANITY,WALNUTSANITY_DIG", +4130,Island West,X in the sand,"WALNUTSANITY,WALNUTSANITY_DIG", +4131,Island West,Diamond Of Indents,"WALNUTSANITY,WALNUTSANITY_DIG", +4132,Island West,Bush Behind Coconut Tree,"WALNUTSANITY,WALNUTSANITY_BUSH", +4133,Island West,Journal Scrap #4,"WALNUTSANITY,WALNUTSANITY_DIG", +4134,Island West,Walnut Room Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4135,Island West,Coast Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4136,Island West,Tiger Slime Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4137,Island West,Bush Behind Mahogany Tree,"WALNUTSANITY,WALNUTSANITY_BUSH", +4138,Island West,Circle Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG", +4139,Island West,Below Colored Crystals Cave Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4140,Colored Crystals Cave,Colored Crystals,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4141,Island West,Cliff Edge Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4142,Island West,Diamond Of Pebbles,"WALNUTSANITY,WALNUTSANITY_DIG", +4143,Island West,Farm Parrot Express Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4144,Island West,Farmhouse Cliff Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4145,Island North,Big Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4146,Island North,Grove Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4147,Island North,Diamond Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG", +4148,Island North,Small Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4149,Island North,Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG", +4150,Dig Site,Crooked Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4151,Dig Site,Above Dig Site Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4152,Dig Site,Above Field Office Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH", +4153,Dig Site,Above Field Office Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH", +4154,Field Office,Complete Large Animal Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4155,Field Office,Complete Snake Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4156,Field Office,Complete Mummified Frog Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4157,Field Office,Complete Mummified Bat Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4158,Field Office,Purple Flowers Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4159,Field Office,Purple Starfish Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4160,Island North,Bush Behind Volcano Tree,"WALNUTSANITY,WALNUTSANITY_BUSH", +4161,Island North,Arc Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4162,Island North,Protruding Tree Walnut,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4163,Island North,Journal Scrap #10,"WALNUTSANITY,WALNUTSANITY_DIG", +4164,Island North,Northmost Point Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4165,Island North,Hidden Passage Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4166,Volcano Secret Beach,Secret Beach Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH", +4167,Volcano Secret Beach,Secret Beach Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH", +4168,Volcano - Floor 5,Volcano Rocks Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4169,Volcano - Floor 5,Volcano Rocks Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4170,Volcano - Floor 10,Volcano Rocks Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4171,Volcano - Floor 10,Volcano Rocks Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4172,Volcano - Floor 10,Volcano Rocks Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4173,Volcano - Floor 5,Volcano Monsters Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4174,Volcano - Floor 5,Volcano Monsters Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4175,Volcano - Floor 10,Volcano Monsters Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4176,Volcano - Floor 10,Volcano Monsters Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4177,Volcano - Floor 10,Volcano Monsters Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4178,Volcano - Floor 5,Volcano Crates Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4179,Volcano - Floor 5,Volcano Crates Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4180,Volcano - Floor 10,Volcano Crates Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4181,Volcano - Floor 10,Volcano Crates Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4182,Volcano - Floor 10,Volcano Crates Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4183,Volcano - Floor 5,Volcano Common Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4184,Volcano - Floor 10,Volcano Rare Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4185,Volcano - Floor 10,Forge Entrance Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4186,Volcano - Floor 10,Forge Exit Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4187,Island North,Cliff Over Island South Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4188,Island Southeast,Starfish Tide Pool,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4189,Island Southeast,Diamond Of Yellow Starfish,"WALNUTSANITY,WALNUTSANITY_DIG", +4190,Island Southeast,Mermaid Song,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4191,Pirate Cove,Pirate Darts 1,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4192,Pirate Cove,Pirate Darts 2,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4193,Pirate Cove,Pirate Darts 3,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4194,Pirate Cove,Pirate Cove Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG", 5001,Stardew Valley,Level 1 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill 5002,Stardew Valley,Level 2 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill 5003,Stardew Valley,Level 3 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill @@ -2578,6 +2900,7 @@ id,region,name,tags,mod_name 7055,Abandoned Mines - 3,Abandoned Treasure - Floor 3,MANDATORY,Boarding House and Bus Stop Extension 7056,Abandoned Mines - 4,Abandoned Treasure - Floor 4,MANDATORY,Boarding House and Bus Stop Extension 7057,Abandoned Mines - 5,Abandoned Treasure - Floor 5,MANDATORY,Boarding House and Bus Stop Extension +7351,Farm,Read Digging Like Worms,"BOOKSANITY,BOOKSANITY_SKILL",Archaeology 7401,Farm,Cook Magic Elixir,COOKSANITY,Magic 7402,Farm,Craft Travel Core,CRAFTSANITY,Magic 7403,Farm,Craft Haste Elixir,CRAFTSANITY,Stardew Valley Expanded @@ -2585,7 +2908,7 @@ id,region,name,tags,mod_name 7405,Farm,Craft Armor Elixir,CRAFTSANITY,Stardew Valley Expanded 7406,Witch's Swamp,Craft Ginger Tincture,"CRAFTSANITY,GINGER_ISLAND",Distant Lands - Witch Swamp Overhaul 7407,Farm,Craft Glass Path,CRAFTSANITY,Archaeology -7408,Farm,Craft Glass Bazier,CRAFTSANITY,Archaeology +7408,Farm,Craft Glass Brazier,CRAFTSANITY,Archaeology 7409,Farm,Craft Glass Fence,CRAFTSANITY,Archaeology 7410,Farm,Craft Bone Path,CRAFTSANITY,Archaeology 7411,Farm,Craft Water Shifter,CRAFTSANITY,Archaeology @@ -2603,13 +2926,23 @@ id,region,name,tags,mod_name 7423,Farm,Craft T-Rex Skeleton L,CRAFTSANITY,Boarding House and Bus Stop Extension 7424,Farm,Craft T-Rex Skeleton M,CRAFTSANITY,Boarding House and Bus Stop Extension 7425,Farm,Craft T-Rex Skeleton R,CRAFTSANITY,Boarding House and Bus Stop Extension +7426,Farm,Craft Restoration Table,CRAFTSANITY,Archaeology +7427,Farm,Craft Rusty Path,CRAFTSANITY,Archaeology +7428,Farm,Craft Rusty Brazier,CRAFTSANITY,Archaeology +7429,Farm,Craft Lucky Ring,CRAFTSANITY,Archaeology +7430,Farm,Craft Bone Fence,CRAFTSANITY,Archaeology +7431,Farm,Craft Bouquet,CRAFTSANITY,Socializing Skill +7432,Farm,Craft Trash Bin,CRAFTSANITY,Binning Skill +7433,Farm,Craft Composter,CRAFTSANITY,Binning Skill +7434,Farm,Craft Recycling Bin,CRAFTSANITY,Binning Skill +7435,Farm,Craft Advanced Recycling Machine,CRAFTSANITY,Binning Skill 7451,Adventurer's Guild,Magic Elixir Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Magic 7452,Adventurer's Guild,Travel Core Recipe,CRAFTSANITY,Magic 7453,Alesia Shop,Haste Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded 7454,Isaac Shop,Hero Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded 7455,Alesia Shop,Armor Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded 7501,Mountain,Missing Envelope,"STORY_QUEST",Ayeisha - The Postal Worker (Custom NPC) -7502,Forest,Lost Emerald Ring,"STORY_QUEST",Ayeisha - The Postal Worker (Custom NPC) +7502,Forest,Ayeisha's Lost Ring,"STORY_QUEST",Ayeisha - The Postal Worker (Custom NPC) 7503,Forest,Mr.Ginger's request,"STORY_QUEST",Mister Ginger (cat npc) 7504,Forest,Juna's Drink Request,"STORY_QUEST",Juna - Roommate NPC 7505,Forest,Juna's BFF Request,"STORY_QUEST",Juna - Roommate NPC @@ -2648,6 +2981,11 @@ id,region,name,tags,mod_name 7563,Kitchen,Cook Pemmican,COOKSANITY,Distant Lands - Witch Swamp Overhaul 7564,Kitchen,Cook Void Mint Tea,COOKSANITY,Distant Lands - Witch Swamp Overhaul 7565,Kitchen,Cook Special Pumpkin Soup,COOKSANITY,Boarding House and Bus Stop Extension +7566,Kitchen,Cook Digger's Delight,COOKSANITY,Archaeology +7567,Kitchen,Cook Rocky Root Coffee,COOKSANITY,Archaeology +7568,Kitchen,Cook Ancient Jello,COOKSANITY,Archaeology +7569,Kitchen,Cook Grilled Cheese,COOKSANITY,Binning Skill +7570,Kitchen,Cook Fish Casserole,COOKSANITY,Binning Skill 7601,Bear Shop,Baked Berry Oatmeal Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded 7602,Bear Shop,Flower Cookie Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded 7603,Saloon,Big Bark Burger Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Stardew Valley Expanded @@ -2668,6 +3006,11 @@ id,region,name,tags,mod_name 7620,Mines Dwarf Shop,T-Rex Skeleton L Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension 7621,Mines Dwarf Shop,T-Rex Skeleton M Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension 7622,Mines Dwarf Shop,T-Rex Skeleton R Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension +7623,Farm,Digger's Delight Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +7624,Farm,Rocky Root Coffee Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +7625,Farm,Ancient Jello Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +7627,Farm,Grilled Cheese Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill +7628,Farm,Fish Casserole Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill 7651,Alesia Shop,Tempered Galaxy Dagger,MANDATORY,Stardew Valley Expanded 7652,Isaac Shop,Tempered Galaxy Sword,MANDATORY,Stardew Valley Expanded 7653,Isaac Shop,Tempered Galaxy Hammer,MANDATORY,Stardew Valley Expanded @@ -2697,7 +3040,6 @@ id,region,name,tags,mod_name 7724,Mutant Bug Lair,Fishsanity: Water Grub,FISHSANITY,Stardew Valley Expanded 7725,Crimson Badlands,Fishsanity: Undeadfish,FISHSANITY,Stardew Valley Expanded 7726,Shearwater Bridge,Fishsanity: Kittyfish,FISHSANITY,Stardew Valley Expanded -7727,Blue Moon Vineyard,Fishsanity: Dulse Seaweed,FISHSANITY,Stardew Valley Expanded 7728,Witch's Swamp,Fishsanity: Void Minnow,FISHSANITY,Distant Lands - Witch Swamp Overhaul 7729,Witch's Swamp,Fishsanity: Swamp Leech,FISHSANITY,Distant Lands - Witch Swamp Overhaul 7730,Witch's Swamp,Fishsanity: Giant Horsehoe Crab,FISHSANITY,Distant Lands - Witch Swamp Overhaul @@ -2714,15 +3056,15 @@ id,region,name,tags,mod_name 8002,Shipping,Shipsanity: Travel Core,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Magic 8003,Shipping,Shipsanity: Aegis Elixir,SHIPSANITY,Stardew Valley Expanded 8004,Shipping,Shipsanity: Aged Blue Moon Wine,SHIPSANITY,Stardew Valley Expanded -8005,Shipping,Shipsanity: Ancient Ferns Seed,SHIPSANITY,Stardew Valley Expanded +8005,Shipping,Shipsanity: Ancient Fern Seed,SHIPSANITY,Stardew Valley Expanded 8006,Shipping,Shipsanity: Ancient Fiber,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8007,Shipping,Shipsanity: Armor Elixir,SHIPSANITY,Stardew Valley Expanded 8008,Shipping,Shipsanity: Baby Lunaloo,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded 8009,Shipping,Shipsanity: Baked Berry Oatmeal,SHIPSANITY,Stardew Valley Expanded 8010,Shipping,Shipsanity: Barbarian Elixir,SHIPSANITY,Stardew Valley Expanded -8011,Shipping,Shipsanity: Bearberrys,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded +8011,Shipping,Shipsanity: Bearberry,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8012,Shipping,Shipsanity: Big Bark Burger,SHIPSANITY,Stardew Valley Expanded -8013,Shipping,Shipsanity: Big Conch,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded +8013,Shipping,Shipsanity: Conch,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8014,Shipping,Shipsanity: Blue Moon Wine,SHIPSANITY,Stardew Valley Expanded 8015,Shipping,Shipsanity: Bonefish,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded 8016,Shipping,Shipsanity: Bull Trout,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded @@ -2730,8 +3072,7 @@ id,region,name,tags,mod_name 8018,Shipping,Shipsanity: Clownfish,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded 8019,Shipping,Shipsanity: Daggerfish,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded 8020,Shipping,Shipsanity: Dewdrop Berry,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded -8021,Shipping,Shipsanity: Dried Sand Dollar,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded -8022,Shipping,Shipsanity: Dulse Seaweed,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded +8021,Shipping,Shipsanity: Sand Dollar,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8023,Shipping,Shipsanity: Ferngill Primrose,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8024,Shipping,Shipsanity: Flower Cookie,SHIPSANITY,Stardew Valley Expanded 8025,Shipping,Shipsanity: Frog,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded @@ -2751,7 +3092,7 @@ id,region,name,tags,mod_name 8040,Shipping,Shipsanity: King Salmon,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded 8050,Shipping,Shipsanity: Kittyfish,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded 8051,Shipping,Shipsanity: Lightning Elixir,SHIPSANITY,Stardew Valley Expanded -8052,Shipping,Shipsanity: Lucky Four Leaf Clover,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded +8052,Shipping,Shipsanity: Four Leaf Clover,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8053,Shipping,Shipsanity: Lunaloo,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded 8054,Shipping,Shipsanity: Meteor Carp,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded 8055,Shipping,Shipsanity: Minnow,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded @@ -2774,7 +3115,7 @@ id,region,name,tags,mod_name 8072,Shipping,Shipsanity: Shrub Seed,"SHIPSANITY,GINGER_ISLAND",Stardew Valley Expanded 8073,Shipping,Shipsanity: Slime Berry,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT,GINGER_ISLAND",Stardew Valley Expanded 8074,Shipping,Shipsanity: Slime Seed,"SHIPSANITY,GINGER_ISLAND",Stardew Valley Expanded -8075,Shipping,Shipsanity: Smelly Rafflesia,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded +8075,Shipping,Shipsanity: Rafflesia,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8076,Shipping,Shipsanity: Sports Drink,SHIPSANITY,Stardew Valley Expanded 8077,Shipping,Shipsanity: Stalk Seed,"SHIPSANITY,GINGER_ISLAND",Stardew Valley Expanded 8078,Shipping,Shipsanity: Stamina Capsule,SHIPSANITY,Stardew Valley Expanded @@ -2937,3 +3278,12 @@ id,region,name,tags,mod_name 8235,Shipping,Shipsanity: Pterodactyl Claw,SHIPSANITY,Boarding House and Bus Stop Extension 8236,Shipping,Shipsanity: Neanderthal Skull,SHIPSANITY,Boarding House and Bus Stop Extension 8237,Shipping,Shipsanity: Pterodactyl R Wing Bone,SHIPSANITY,Boarding House and Bus Stop Extension +8238,Shipping,Shipsanity: Scrap Rust,SHIPSANITY,Archaeology +8239,Shipping,Shipsanity: Rusty Path,SHIPSANITY,Archaeology +8240,Shipping,Shipsanity: Digging Like Worms,SHIPSANITY,Archaeology +8241,Shipping,Shipsanity: Digger's Delight,SHIPSANITY,Archaeology +8242,Shipping,Shipsanity: Rocky Root Coffee,SHIPSANITY,Archaeology +8243,Shipping,Shipsanity: Ancient Jello,SHIPSANITY,Archaeology +8244,Shipping,Shipsanity: Bone Fence,SHIPSANITY,Archaeology +8245,Shipping,Shipsanity: Grilled Cheese,SHIPSANITY,Binning Skill +8246,Shipping,Shipsanity: Fish Casserole,SHIPSANITY,Binning Skill diff --git a/worlds/stardew_valley/data/museum_data.py b/worlds/stardew_valley/data/museum_data.py index 544bb92e6e55..b81c518a37c9 100644 --- a/worlds/stardew_valley/data/museum_data.py +++ b/worlds/stardew_valley/data/museum_data.py @@ -76,6 +76,8 @@ def create_mineral(name: str, difficulty += 1.0 / 26.0 * 100 if "Omni Geode" in geodes: difficulty += 31.0 / 2750.0 * 100 + if "Fishing Chest" in geodes: + difficulty += 4.3 mineral_item = MuseumItem.of(name, difficulty, locations, geodes, monsters) all_museum_minerals.append(mineral_item) @@ -95,7 +97,7 @@ class Artifact: geodes=Geode.artifact_trove) arrowhead = create_artifact("Arrowhead", 8.5, (Region.mountain, Region.forest, Region.bus_stop), geodes=Geode.artifact_trove) - ancient_doll = create_artifact("Ancient Doll", 13.1, (Region.mountain, Region.forest, Region.bus_stop), + ancient_doll = create_artifact(Artifact.ancient_doll, 13.1, (Region.mountain, Region.forest, Region.bus_stop), geodes=(Geode.artifact_trove, WaterChest.fishing_chest)) elvish_jewelry = create_artifact("Elvish Jewelry", 5.3, Region.forest, geodes=(Geode.artifact_trove, WaterChest.fishing_chest)) @@ -103,8 +105,7 @@ class Artifact: geodes=(Geode.artifact_trove, WaterChest.fishing_chest)) ornamental_fan = create_artifact("Ornamental Fan", 7.4, (Region.beach, Region.forest, Region.town), geodes=(Geode.artifact_trove, WaterChest.fishing_chest)) - dinosaur_egg = create_artifact("Dinosaur Egg", 11.4, (Region.mountain, Region.skull_cavern), - geodes=WaterChest.fishing_chest, + dinosaur_egg = create_artifact("Dinosaur Egg", 11.4, (Region.skull_cavern), monsters=Monster.pepper_rex) rare_disc = create_artifact("Rare Disc", 5.6, Region.stardew_valley, geodes=(Geode.artifact_trove, WaterChest.fishing_chest), @@ -170,18 +171,18 @@ class Artifact: class Mineral: - quartz = create_mineral(Mineral.quartz, Region.mines_floor_20) + quartz = create_mineral(Mineral.quartz, Region.mines_floor_20, difficulty=100.0 / 5.0) fire_quartz = create_mineral("Fire Quartz", Region.mines_floor_100, geodes=(Geode.magma, Geode.omni, WaterChest.fishing_chest), - difficulty=1.0 / 12.0) + difficulty=100.0 / 5.0) frozen_tear = create_mineral("Frozen Tear", Region.mines_floor_60, geodes=(Geode.frozen, Geode.omni, WaterChest.fishing_chest), monsters=unlikely, - difficulty=1.0 / 12.0) + difficulty=100.0 / 5.0) earth_crystal = create_mineral("Earth Crystal", Region.mines_floor_20, geodes=(Geode.geode, Geode.omni, WaterChest.fishing_chest), monsters=Monster.duggy, - difficulty=1.0 / 12.0) + difficulty=100.0 / 5.0) emerald = create_mineral("Emerald", Region.mines_floor_100, geodes=WaterChest.fishing_chest) aquamarine = create_mineral("Aquamarine", Region.mines_floor_60, diff --git a/worlds/stardew_valley/data/recipe_data.py b/worlds/stardew_valley/data/recipe_data.py index 62dcd8709c64..b48246876271 100644 --- a/worlds/stardew_valley/data/recipe_data.py +++ b/worlds/stardew_valley/data/recipe_data.py @@ -7,15 +7,16 @@ from ..strings.crop_names import Fruit, Vegetable, SVEFruit, DistantLandsCrop from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish from ..strings.flower_names import Flower -from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable +from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom from ..strings.ingredient_names import Ingredient -from ..strings.food_names import Meal, SVEMeal, Beverage, DistantLandsMeal, BoardingHouseMeal +from ..strings.food_names import Meal, SVEMeal, Beverage, DistantLandsMeal, BoardingHouseMeal, ArchaeologyMeal, TrashyMeal from ..strings.material_names import Material -from ..strings.metal_names import Fossil +from ..strings.metal_names import Fossil, Artifact from ..strings.monster_drop_names import Loot from ..strings.region_names import Region, SVERegion from ..strings.season_names import Season -from ..strings.skill_names import Skill +from ..strings.seed_names import Seed +from ..strings.skill_names import Skill, ModSkill from ..strings.villager_names import NPC, ModNPC @@ -49,9 +50,9 @@ def friendship_and_shop_recipe(name: str, friend: str, hearts: int, region: str, return create_recipe(name, ingredients, source, mod_name) -def skill_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int]) -> CookingRecipe: +def skill_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CookingRecipe: source = SkillSource(skill, level) - return create_recipe(name, ingredients, source) + return create_recipe(name, ingredients, source, mod_name) def shop_recipe(name: str, region: str, price: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CookingRecipe: @@ -116,7 +117,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, fried_calamari = friendship_recipe(Meal.fried_calamari, NPC.jodi, 3, {Fish.squid: 1, Ingredient.wheat_flour: 1, Ingredient.oil: 1}) fried_eel = friendship_recipe(Meal.fried_eel, NPC.george, 3, {Fish.eel: 1, Ingredient.oil: 1}) fried_egg = starter_recipe(Meal.fried_egg, {AnimalProduct.chicken_egg: 1}) -fried_mushroom = friendship_recipe(Meal.fried_mushroom, NPC.demetrius, 3, {Forageable.common_mushroom: 1, Forageable.morel: 1, Ingredient.oil: 1}) +fried_mushroom = friendship_recipe(Meal.fried_mushroom, NPC.demetrius, 3, {Mushroom.common: 1, Mushroom.morel: 1, Ingredient.oil: 1}) fruit_salad = queen_of_sauce_recipe(Meal.fruit_salad, 2, Season.fall, 7, {Fruit.blueberry: 1, Fruit.melon: 1, Fruit.apricot: 1}) ginger_ale = shop_recipe(Beverage.ginger_ale, Region.volcano_dwarf_shop, 1000, {Forageable.ginger: 3, Ingredient.sugar: 1}) glazed_yams = queen_of_sauce_recipe(Meal.glazed_yams, 1, Season.fall, 21, {Vegetable.yam: 1, Ingredient.sugar: 1}) @@ -130,6 +131,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, mango_sticky_rice = friendship_recipe(Meal.mango_sticky_rice, NPC.leo, 7, {Fruit.mango: 1, Forageable.coconut: 1, Ingredient.rice: 1}) maple_bar = queen_of_sauce_recipe(Meal.maple_bar, 2, Season.summer, 14, {ArtisanGood.maple_syrup: 1, Ingredient.sugar: 1, Ingredient.wheat_flour: 1}) miners_treat = skill_recipe(Meal.miners_treat, Skill.mining, 3, {Forageable.cave_carrot: 2, Ingredient.sugar: 1, AnimalProduct.cow_milk: 1}) +moss_soup = skill_recipe(Meal.moss_soup, Skill.foraging, 3, {Material.moss: 20}) omelet = queen_of_sauce_recipe(Meal.omelet, 1, Season.spring, 28, {AnimalProduct.chicken_egg: 1, AnimalProduct.cow_milk: 1}) pale_broth = friendship_recipe(Meal.pale_broth, NPC.marnie, 3, {WaterItem.white_algae: 2}) pancakes = queen_of_sauce_recipe(Meal.pancakes, 1, Season.summer, 14, {Ingredient.wheat_flour: 1, AnimalProduct.chicken_egg: 1}) @@ -160,13 +162,14 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, spaghetti = friendship_recipe(Meal.spaghetti, NPC.lewis, 3, {Vegetable.tomato: 1, Ingredient.wheat_flour: 1}) spicy_eel = friendship_recipe(Meal.spicy_eel, NPC.george, 7, {Fish.eel: 1, Fruit.hot_pepper: 1}) squid_ink_ravioli = skill_recipe(Meal.squid_ink_ravioli, Skill.combat, 9, {AnimalProduct.squid_ink: 1, Ingredient.wheat_flour: 1, Vegetable.tomato: 1}) -stir_fry_ingredients = {Forageable.cave_carrot: 1, Forageable.common_mushroom: 1, Vegetable.kale: 1, Ingredient.sugar: 1} +stir_fry_ingredients = {Forageable.cave_carrot: 1, Mushroom.common: 1, Vegetable.kale: 1, Ingredient.sugar: 1} stir_fry_qos = queen_of_sauce_recipe(Meal.stir_fry, 1, Season.spring, 7, stir_fry_ingredients) strange_bun = friendship_recipe(Meal.strange_bun, NPC.shane, 7, {Ingredient.wheat_flour: 1, Fish.periwinkle: 1, ArtisanGood.void_mayonnaise: 1}) stuffing = friendship_recipe(Meal.stuffing, NPC.pam, 7, {Meal.bread: 1, Fruit.cranberries: 1, Forageable.hazelnut: 1}) super_meal = friendship_recipe(Meal.super_meal, NPC.kent, 7, {Vegetable.bok_choy: 1, Fruit.cranberries: 1, Vegetable.artichoke: 1}) -survival_burger = skill_recipe(Meal.survival_burger, Skill.foraging, 2, {Meal.bread: 1, Forageable.cave_carrot: 1, Vegetable.eggplant: 1}) -tom_kha_soup = friendship_recipe(Meal.tom_kha_soup, NPC.sandy, 7, {Forageable.coconut: 1, Fish.shrimp: 1, Forageable.common_mushroom: 1}) + +survival_burger = skill_recipe(Meal.survival_burger, Skill.foraging, 8, {Meal.bread: 1, Forageable.cave_carrot: 1, Vegetable.eggplant: 1}) +tom_kha_soup = friendship_recipe(Meal.tom_kha_soup, NPC.sandy, 7, {Forageable.coconut: 1, Fish.shrimp: 1, Mushroom.common: 1}) tortilla_ingredients = {Vegetable.corn: 1} tortilla_qos = queen_of_sauce_recipe(Meal.tortilla, 1, Season.fall, 7, tortilla_ingredients) tortilla_saloon = shop_recipe(Meal.tortilla, Region.saloon, 100, tortilla_ingredients) @@ -175,7 +178,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, trout_soup = queen_of_sauce_recipe(Meal.trout_soup, 1, Season.fall, 14, {Fish.rainbow_trout: 1, WaterItem.green_algae: 1}) vegetable_medley = friendship_recipe(Meal.vegetable_medley, NPC.caroline, 7, {Vegetable.tomato: 1, Vegetable.beet: 1}) -magic_elixir = shop_recipe(ModEdible.magic_elixir, Region.adventurer_guild, 3000, {Edible.life_elixir: 1, Forageable.purple_mushroom: 1}, ModNames.magic) +magic_elixir = shop_recipe(ModEdible.magic_elixir, Region.adventurer_guild, 3000, {Edible.life_elixir: 1, Mushroom.purple: 1}, ModNames.magic) baked_berry_oatmeal = shop_recipe(SVEMeal.baked_berry_oatmeal, SVERegion.bear_shop, 0, {Forageable.salmonberry: 15, Forageable.blackberry: 15, Ingredient.sugar: 1, Ingredient.wheat_flour: 2}, ModNames.sve) @@ -188,7 +191,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, glazed_butterfish = friendship_and_shop_recipe(SVEMeal.glazed_butterfish, NPC.gus, 10, Region.saloon, 4000, {SVEFish.butterfish: 1, Ingredient.wheat_flour: 1, Ingredient.oil: 1}, ModNames.sve) mixed_berry_pie = shop_recipe(SVEMeal.mixed_berry_pie, Region.saloon, 3500, {Fruit.strawberry: 6, SVEFruit.salal_berry: 6, Forageable.blackberry: 6, - SVEForage.bearberrys: 6, Ingredient.sugar: 1, Ingredient.wheat_flour: 1}, + SVEForage.bearberry: 6, Ingredient.sugar: 1, Ingredient.wheat_flour: 1}, ModNames.sve) mushroom_berry_rice = friendship_and_shop_recipe(SVEMeal.mushroom_berry_rice, ModNPC.marlon, 6, Region.adventurer_guild, 1500, {SVEForage.poison_mushroom: 3, SVEForage.red_baneberry: 10, Ingredient.rice: 1, Ingredient.sugar: 2}, ModNames.sve) @@ -198,8 +201,8 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, void_salmon_sushi = friendship_and_shop_recipe(SVEMeal.void_salmon_sushi, NPC.krobus, 10, Region.sewer, 5000, {Fish.void_salmon: 1, ArtisanGood.void_mayonnaise: 1, WaterItem.seaweed: 3}, ModNames.sve) -mushroom_kebab = friendship_recipe(DistantLandsMeal.mushroom_kebab, ModNPC.goblin, 2, {Forageable.chanterelle: 1, Forageable.common_mushroom: 1, - Forageable.red_mushroom: 1, Material.wood: 1}, ModNames.distant_lands) +mushroom_kebab = friendship_recipe(DistantLandsMeal.mushroom_kebab, ModNPC.goblin, 2, {Mushroom.chanterelle: 1, Mushroom.common: 1, + Mushroom.red: 1, Material.wood: 1}, ModNames.distant_lands) void_mint_tea = friendship_recipe(DistantLandsMeal.void_mint_tea, ModNPC.goblin, 4, {DistantLandsCrop.void_mint: 1}, ModNames.distant_lands) crayfish_soup = friendship_recipe(DistantLandsMeal.crayfish_soup, ModNPC.goblin, 6, {Forageable.cave_carrot: 1, Fish.crayfish: 1, DistantLandsFish.purple_algae: 1, WaterItem.white_algae: 1}, ModNames.distant_lands) @@ -208,6 +211,11 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, special_pumpkin_soup = friendship_recipe(BoardingHouseMeal.special_pumpkin_soup, ModNPC.joel, 6, {Vegetable.pumpkin: 2, AnimalProduct.large_goat_milk: 1, Vegetable.garlic: 1}, ModNames.boarding_house) +diggers_delight = skill_recipe(ArchaeologyMeal.diggers_delight, ModSkill.archaeology, 3, {Forageable.cave_carrot: 2, Ingredient.sugar: 1, AnimalProduct.milk: 1}, ModNames.archaeology) +rocky_root = skill_recipe(ArchaeologyMeal.rocky_root, ModSkill.archaeology, 7, {Forageable.cave_carrot: 3, Seed.coffee: 1, Material.stone: 1}, ModNames.archaeology) +ancient_jello = skill_recipe(ArchaeologyMeal.ancient_jello, ModSkill.archaeology, 9, {WaterItem.cave_jelly: 6, Ingredient.sugar: 5, AnimalProduct.egg: 1, AnimalProduct.milk: 1, Artifact.chipped_amphora: 1}, ModNames.archaeology) +grilled_cheese = skill_recipe(TrashyMeal.grilled_cheese, ModSkill.binning, 1, {Meal.bread: 1, ArtisanGood.cheese: 1}, ModNames.binning_skill) +fish_casserole = skill_recipe(TrashyMeal.fish_casserole, ModSkill.binning, 8, {Fish.any: 1, AnimalProduct.milk: 1, Vegetable.carrot: 1}, ModNames.binning_skill) all_cooking_recipes_by_name = {recipe.meal: recipe for recipe in all_cooking_recipes} \ No newline at end of file diff --git a/worlds/stardew_valley/data/recipe_source.py b/worlds/stardew_valley/data/recipe_source.py index 8dd622e926e7..24b03bf77bd4 100644 --- a/worlds/stardew_valley/data/recipe_source.py +++ b/worlds/stardew_valley/data/recipe_source.py @@ -94,6 +94,16 @@ def __repr__(self): return f"SkillSource at level {self.level} {self.skill}" +class MasterySource(RecipeSource): + skill: str + + def __init__(self, skill: str): + self.skill = skill + + def __repr__(self): + return f"MasterySource at level {self.level} {self.skill}" + + class ShopSource(RecipeSource): region: str price: int diff --git a/worlds/stardew_valley/data/requirement.py b/worlds/stardew_valley/data/requirement.py new file mode 100644 index 000000000000..7e9466630fc3 --- /dev/null +++ b/worlds/stardew_valley/data/requirement.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass + +from .game_item import Requirement +from ..strings.tool_names import ToolMaterial + + +@dataclass(frozen=True) +class BookRequirement(Requirement): + book: str + + +@dataclass(frozen=True) +class ToolRequirement(Requirement): + tool: str + tier: str = ToolMaterial.basic + + +@dataclass(frozen=True) +class SkillRequirement(Requirement): + skill: str + level: int + + +@dataclass(frozen=True) +class SeasonRequirement(Requirement): + season: str + + +@dataclass(frozen=True) +class YearRequirement(Requirement): + year: int diff --git a/worlds/stardew_valley/data/shop.py b/worlds/stardew_valley/data/shop.py new file mode 100644 index 000000000000..ca54d35e14f2 --- /dev/null +++ b/worlds/stardew_valley/data/shop.py @@ -0,0 +1,40 @@ +from dataclasses import dataclass +from typing import Tuple, Optional + +from .game_item import ItemSource, kw_only, Requirement +from ..strings.season_names import Season + +ItemPrice = Tuple[int, str] + + +@dataclass(frozen=True, **kw_only) +class ShopSource(ItemSource): + shop_region: str + money_price: Optional[int] = None + items_price: Optional[Tuple[ItemPrice, ...]] = None + seasons: Tuple[str, ...] = Season.all + other_requirements: Tuple[Requirement, ...] = () + + def __post_init__(self): + assert self.money_price or self.items_price, "At least money price or items price need to be defined." + assert self.items_price is None or all(type(p) == tuple for p in self.items_price), "Items price should be a tuple." + + +@dataclass(frozen=True, **kw_only) +class MysteryBoxSource(ItemSource): + amount: int + + +@dataclass(frozen=True, **kw_only) +class ArtifactTroveSource(ItemSource): + amount: int + + +@dataclass(frozen=True, **kw_only) +class PrizeMachineSource(ItemSource): + amount: int + + +@dataclass(frozen=True, **kw_only) +class FishingTreasureChestSource(ItemSource): + amount: int diff --git a/worlds/stardew_valley/data/skill.py b/worlds/stardew_valley/data/skill.py new file mode 100644 index 000000000000..d0674f34c0e1 --- /dev/null +++ b/worlds/stardew_valley/data/skill.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass, field + +from ..data.game_item import kw_only + + +@dataclass(frozen=True) +class Skill: + name: str + has_mastery: bool = field(**kw_only) diff --git a/worlds/stardew_valley/data/villagers_data.py b/worlds/stardew_valley/data/villagers_data.py index 718bce743b1c..70fb110ffbae 100644 --- a/worlds/stardew_valley/data/villagers_data.py +++ b/worlds/stardew_valley/data/villagers_data.py @@ -1,10 +1,10 @@ from dataclasses import dataclass -from typing import List, Tuple, Optional, Dict, Callable, Set +from typing import Tuple, Optional from ..mods.mod_data import ModNames from ..strings.food_names import Beverage from ..strings.generic_names import Generic -from ..strings.region_names import Region, SVERegion, AlectoRegion, BoardingHouseRegion, LaceyRegion +from ..strings.region_names import Region, SVERegion, AlectoRegion, BoardingHouseRegion, LaceyRegion, LogicRegion from ..strings.season_names import Season from ..strings.villager_names import NPC, ModNPC @@ -36,7 +36,7 @@ def __repr__(self): alex_house = (Region.alex_house,) elliott_house = (Region.elliott_house,) ranch = (Region.ranch,) -mines_dwarf_shop = (Region.mines_dwarf_shop,) +mines_dwarf_shop = (LogicRegion.mines_dwarf_shop,) desert = (Region.desert,) oasis = (Region.oasis,) sewers = (Region.sewer,) @@ -355,28 +355,10 @@ def __repr__(self): susan_loves = pancakes + chocolate_cake + pink_cake + ice_cream + cookie + pumpkin_pie + rhubarb_pie + \ blueberry_tart + blackberry_cobbler + cranberry_candy + red_plate -all_villagers: List[Villager] = [] -villager_modifications_by_mod: Dict[str, Dict[str, Callable[[str, Villager], Villager]]] = {} - def villager(name: str, bachelor: bool, locations: Tuple[str, ...], birthday: str, gifts: Tuple[str, ...], available: bool, mod_name: Optional[str] = None) -> Villager: - npc = Villager(name, bachelor, locations, birthday, gifts, available, mod_name) - all_villagers.append(npc) - return npc - - -def adapt_wizard_to_sve(mod_name: str, npc: Villager): - if npc.mod_name: - mod_name = npc.mod_name - # The wizard leaves his tower on sunday, for like 1 hour... Good enough to meet him! - return Villager(npc.name, True, npc.locations + forest, npc.birthday, npc.gifts, npc.available, mod_name) - - -def register_villager_modification(mod_name: str, npc: Villager, modification_function): - if mod_name not in villager_modifications_by_mod: - villager_modifications_by_mod[mod_name] = {} - villager_modifications_by_mod[mod_name][npc.name] = modification_function + return Villager(name, bachelor, locations, birthday, gifts, available, mod_name) josh = villager(NPC.alex, True, town + alex_house, Season.summer, universal_loves + complete_breakfast + salmon_dinner, True) @@ -385,18 +367,18 @@ def register_villager_modification(mod_name: str, npc: Villager, modification_fu sam = villager(NPC.sam, True, town, Season.summer, universal_loves + sam_loves, True) sebastian = villager(NPC.sebastian, True, carpenter, Season.winter, universal_loves + sebastian_loves, True) shane = villager(NPC.shane, True, ranch, Season.spring, universal_loves + shane_loves, True) -best_girl = villager(NPC.abigail, True, town, Season.fall, universal_loves + abigail_loves, True) +abigail = villager(NPC.abigail, True, town, Season.fall, universal_loves + abigail_loves, True) emily = villager(NPC.emily, True, town, Season.spring, universal_loves + emily_loves, True) -hoe = villager(NPC.haley, True, town, Season.spring, universal_loves_no_prismatic_shard + haley_loves, True) +haley = villager(NPC.haley, True, town, Season.spring, universal_loves_no_prismatic_shard + haley_loves, True) leah = villager(NPC.leah, True, forest, Season.winter, universal_loves + leah_loves, True) -nerd = villager(NPC.maru, True, carpenter + hospital + town, Season.summer, universal_loves + maru_loves, True) +maru = villager(NPC.maru, True, carpenter + hospital + town, Season.summer, universal_loves + maru_loves, True) penny = villager(NPC.penny, True, town, Season.fall, universal_loves_no_rabbit_foot + penny_loves, True) caroline = villager(NPC.caroline, False, town, Season.winter, universal_loves + caroline_loves, True) clint = villager(NPC.clint, False, town, Season.winter, universal_loves + clint_loves, True) demetrius = villager(NPC.demetrius, False, carpenter, Season.summer, universal_loves + demetrius_loves, True) dwarf = villager(NPC.dwarf, False, mines_dwarf_shop, Season.summer, universal_loves + dwarf_loves, False) -gilf = villager(NPC.evelyn, False, town, Season.winter, universal_loves + evelyn_loves, True) -boomer = villager(NPC.george, False, town, Season.fall, universal_loves + george_loves, True) +evelyn = villager(NPC.evelyn, False, town, Season.winter, universal_loves + evelyn_loves, True) +george = villager(NPC.george, False, town, Season.fall, universal_loves + george_loves, True) gus = villager(NPC.gus, False, town, Season.summer, universal_loves + gus_loves, True) jas = villager(NPC.jas, False, ranch, Season.summer, universal_loves + jas_loves, True) jodi = villager(NPC.jodi, False, town, Season.fall, universal_loves + jodi_loves, True) @@ -408,7 +390,7 @@ def register_villager_modification(mod_name: str, npc: Villager, modification_fu marnie = villager(NPC.marnie, False, ranch, Season.fall, universal_loves + marnie_loves, True) pam = villager(NPC.pam, False, town, Season.spring, universal_loves + pam_loves, True) pierre = villager(NPC.pierre, False, town, Season.spring, universal_loves + pierre_loves, True) -milf = villager(NPC.robin, False, carpenter, Season.fall, universal_loves + robin_loves, True) +robin = villager(NPC.robin, False, carpenter, Season.fall, universal_loves + robin_loves, True) sandy = villager(NPC.sandy, False, oasis, Season.fall, universal_loves + sandy_loves, False) vincent = villager(NPC.vincent, False, town, Season.spring, universal_loves + vincent_loves, True) willy = villager(NPC.willy, False, beach, Season.summer, universal_loves + willy_loves, True) @@ -443,54 +425,10 @@ def register_villager_modification(mod_name: str, npc: Villager, modification_fu victor = villager(ModNPC.victor, True, town, Season.summer, universal_loves + victor_loves, True, ModNames.sve) andy = villager(ModNPC.andy, False, forest, Season.spring, universal_loves + andy_loves, True, ModNames.sve) apples = villager(ModNPC.apples, False, aurora + junimo, Generic.any, starfruit, False, ModNames.sve) -gunther = villager(ModNPC.gunther, False, museum, Season.winter, universal_loves + gunther_loves, True, ModNames.jasper_sve) +gunther = villager(ModNPC.gunther, False, museum, Season.winter, universal_loves + gunther_loves, True, ModNames.sve) martin = villager(ModNPC.martin, False, town + jojamart, Season.summer, universal_loves + martin_loves, True, ModNames.sve) -marlon = villager(ModNPC.marlon, False, adventurer, Season.winter, universal_loves + marlon_loves, False, ModNames.jasper_sve) +marlon = villager(ModNPC.marlon, False, adventurer, Season.winter, universal_loves + marlon_loves, False, ModNames.sve) morgan = villager(ModNPC.morgan, False, forest, Season.fall, universal_loves_no_rabbit_foot + morgan_loves, False, ModNames.sve) scarlett = villager(ModNPC.scarlett, False, bluemoon, Season.summer, universal_loves + scarlett_loves, False, ModNames.sve) susan = villager(ModNPC.susan, False, railroad, Season.fall, universal_loves + susan_loves, False, ModNames.sve) morris = villager(ModNPC.morris, False, jojamart, Season.spring, universal_loves + morris_loves, True, ModNames.sve) - -# Modified villagers; not included in all villagers - -register_villager_modification(ModNames.sve, wizard, adapt_wizard_to_sve) - -all_villagers_by_name: Dict[str, Villager] = {villager.name: villager for villager in all_villagers} -all_villagers_by_mod: Dict[str, List[Villager]] = {} -all_villagers_by_mod_by_name: Dict[str, Dict[str, Villager]] = {} - -for npc in all_villagers: - mod = npc.mod_name - name = npc.name - if mod in all_villagers_by_mod: - all_villagers_by_mod[mod].append(npc) - all_villagers_by_mod_by_name[mod][name] = npc - else: - all_villagers_by_mod[mod] = [npc] - all_villagers_by_mod_by_name[mod] = {} - all_villagers_by_mod_by_name[mod][name] = npc - - -def villager_included_for_any_mod(npc: Villager, mods: Set[str]): - if not npc.mod_name: - return True - for mod in npc.mod_name.split(","): - if mod in mods: - return True - return False - - -def get_villagers_for_mods(mods: Set[str]) -> List[Villager]: - villagers_for_current_mods = [] - for npc in all_villagers: - if not villager_included_for_any_mod(npc, mods): - continue - modified_npc = npc - for active_mod in mods: - if (active_mod not in villager_modifications_by_mod or - npc.name not in villager_modifications_by_mod[active_mod]): - continue - modification = villager_modifications_by_mod[active_mod][npc.name] - modified_npc = modification(active_mod, modified_npc) - villagers_for_current_mods.append(modified_npc) - return villagers_for_current_mods diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md index c29ae859e095..0ed693031b82 100644 --- a/worlds/stardew_valley/docs/en_Stardew Valley.md +++ b/worlds/stardew_valley/docs/en_Stardew Valley.md @@ -11,59 +11,62 @@ A vast number of objectives in Stardew Valley can be shuffled around the multiwo player can customize their experience in their YAML file. For these objectives, if they have a vanilla reward, this reward will instead be an item in the multiworld. For the remaining -number of such objectives, there are a number of "Resource Pack" items, which are simply an item or a stack of items that +number of such objectives, there are a number of "Resource Pack" items, which are simply an item or a stack of items that may be useful to the player. ## What is the goal of Stardew Valley? The player can choose from a number of goals, using their YAML options. + - Complete the [Community Center](https://stardewvalleywiki.com/Bundles) - Succeed [Grandpa's Evaluation](https://stardewvalleywiki.com/Grandpa) with 4 lit candles - Reach the bottom of the [Pelican Town Mineshaft](https://stardewvalleywiki.com/The_Mines) -- Complete the [Cryptic Note](https://stardewvalleywiki.com/Secret_Notes#Secret_Note_.2310) quest, by meeting Mr Qi on -floor 100 of the Skull Cavern +- Complete the [Cryptic Note](https://stardewvalleywiki.com/Secret_Notes#Secret_Note_.2310) quest, by meeting Mr Qi on + floor 100 of the Skull Cavern - Become a [Master Angler](https://stardewvalleywiki.com/Fish), which requires catching every fish in your slot -- Restore [A Complete Collection](https://stardewvalleywiki.com/Museum), which requires donating all the artifacts and -minerals to the museum +- Restore [A Complete Collection](https://stardewvalleywiki.com/Museum), which requires donating all the artifacts and + minerals to the museum - Get the achievement [Full House](https://stardewvalleywiki.com/Children), which requires getting married and having two kids -- Get recognized as the [Greatest Walnut Hunter](https://stardewvalleywiki.com/Golden_Walnut) by Mr Qi, which requires -finding all 130 golden walnuts on ginger island -- Become the [Protector of the Valley](https://stardewvalleywiki.com/Adventurer%27s_Guild#Monster_Eradication_Goals) by -completing all the monster slayer goals at the Adventure Guild +- Get recognized as the [Greatest Walnut Hunter](https://stardewvalleywiki.com/Golden_Walnut) by Mr Qi, which requires + finding all 130 golden walnuts on ginger island +- Become the [Protector of the Valley](https://stardewvalleywiki.com/Adventurer%27s_Guild#Monster_Eradication_Goals) by + completing all the monster slayer goals at the Adventure Guild - Complete a [Full Shipment](https://stardewvalleywiki.com/Shipping#Collection) by shipping every item in your slot - Become a [Gourmet Chef](https://stardewvalleywiki.com/Cooking) by cooking every recipe in your slot - Become a [Craft Master](https://stardewvalleywiki.com/Crafting) by crafting every item -- Earn the title of [Legend](https://stardewvalleywiki.com/Gold) by earning 10 000 000g -- Solve the [Mystery of the Stardrops](https://stardewvalleywiki.com/Stardrop) by finding every stardrop +- Earn the title of [Legend](https://stardewvalleywiki.com/Gold) by earning 10 000 000g +- Solve the [Mystery of the Stardrops](https://stardewvalleywiki.com/Stardrop) by finding every stardrop - Finish 100% of your randomizer slot with Allsanity: Complete every check in your slot - Achieve [Perfection](https://stardewvalleywiki.com/Perfection) in your save file -The following goals [Community Center, Master Angler, Protector of the Valley, Full Shipment and Gourmet Chef] will adapt -to other options in your slots, and are therefore customizable in duration and difficulty. For example, if you set "Fishsanity" +The following goals [Community Center, Master Angler, Protector of the Valley, Full Shipment and Gourmet Chef] will adapt +to other options in your slots, and are therefore customizable in duration and difficulty. For example, if you set "Fishsanity" to "Exclude Legendaries", and pick the Master Angler goal, you will not need to catch the legendaries to complete the goal. ## What are location checks in Stardew Valley? Location checks in Stardew Valley always include: + - [Community Center Bundles](https://stardewvalleywiki.com/Bundles) - [Mineshaft Chest Rewards](https://stardewvalleywiki.com/The_Mines#Remixed_Rewards) - [Traveling Merchant Items](https://stardewvalleywiki.com/Traveling_Cart) -- Isolated objectives such as the [beach bridge](https://stardewvalleywiki.com/The_Beach#Tide_Pools), -[Old Master Cannoli](https://stardewvalleywiki.com/Secret_Woods#Old_Master_Cannoli), -[Grim Reaper Statue](https://stardewvalleywiki.com/Golden_Scythe), etc +- Isolated objectives such as the [beach bridge](https://stardewvalleywiki.com/The_Beach#Tide_Pools), + [Old Master Cannoli](https://stardewvalleywiki.com/Secret_Woods#Old_Master_Cannoli), + [Grim Reaper Statue](https://stardewvalleywiki.com/Golden_Scythe), etc There also are a number of location checks that are optional, and individual players choose to include them or not in their shuffling: + - [Tools and Fishing Rod Upgrades](https://stardewvalleywiki.com/Tools) - [Carpenter Buildings](https://stardewvalleywiki.com/Carpenter%27s_Shop#Farm_Buildings) - [Backpack Upgrades](https://stardewvalleywiki.com/Tools#Other_Tools) - [Mine Elevator Levels](https://stardewvalleywiki.com/The_Mines#Staircases) -- [Skill Levels](https://stardewvalleywiki.com/Skills) +- [Skill Levels](https://stardewvalleywiki.com/Skills) and [Masteries](https://stardewvalleywiki.com/Mastery_Cave#Masteries) - Arcade Machines - [Story Quests](https://stardewvalleywiki.com/Quests#List_of_Story_Quests) - [Help Wanted Quests](https://stardewvalleywiki.com/Quests#Help_Wanted_Quests) - Participating in [Festivals](https://stardewvalleywiki.com/Festivals) -- [Special Orders](https://stardewvalleywiki.com/Quests#List_of_Special_Orders) from the town board, or from -[Mr Qi](https://stardewvalleywiki.com/Quests#List_of_Mr._Qi.27s_Special_Orders) +- [Special Orders](https://stardewvalleywiki.com/Quests#List_of_Special_Orders) from the town board, or from + [Mr Qi](https://stardewvalleywiki.com/Quests#List_of_Mr._Qi.27s_Special_Orders) - [Cropsanity](https://stardewvalleywiki.com/Crops): Growing and Harvesting individual crop types - [Fishsanity](https://stardewvalleywiki.com/Fish): Catching individual fish - [Museumsanity](https://stardewvalleywiki.com/Museum): Donating individual items, or reaching milestones for museum donations @@ -73,6 +76,8 @@ There also are a number of location checks that are optional, and individual pla - [Chefsanity](https://stardewvalleywiki.com/Cooking#Recipes): Learning cooking recipes - [Craftsanity](https://stardewvalleywiki.com/Crafting): Crafting individual items - [Shipsanity](https://stardewvalleywiki.com/Shipping): Shipping individual items +- [Booksanity](https://stardewvalleywiki.com/Books): Reading individual books +- [Walnutsanity](https://stardewvalleywiki.com/Golden_Walnut): Collecting Walnuts on Ginger Island ## Which items can be in another player's world? @@ -80,49 +85,57 @@ Every normal reward from the above locations can be in another player's world. For the locations which do not include a normal reward, Resource Packs and traps are instead added to the pool. Traps are optional. A player can enable some options that will add some items to the pool that are relevant to progression + - Seasons Randomizer: - - All 4 seasons will be items, and one of them will be selected randomly and be added to the player's start inventory. - - At the end of each month, the player can choose the next season, instead of following the vanilla season order. On Seasons Randomizer, they can only choose from the seasons they have received. + - All 4 seasons will be items, and one of them will be selected randomly and be added to the player's start inventory. + - At the end of each month, the player can choose the next season, instead of following the vanilla season order. On Seasons Randomizer, they can only + choose from the seasons they have received. - Cropsanity: - - Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received as multiworld items. Growing each seed and harvesting the resulting crop sends a location check - - The way merchants sell seeds is considerably changed. Pierre sells fewer seeds at a high price, while Joja sells unlimited seeds but in huge discount packs, not individually. + - Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received as multiworld items. Growing each + seed and harvesting the resulting crop sends a location check + - The way merchants sell seeds is considerably changed. Pierre sells fewer seeds at a high price, while Joja sells unlimited seeds but in huge discount + packs, not individually. - Museumsanity: - - The items that are normally obtained from museum donation milestones are added to the item pool. Some items, like the magic rock candy, are duplicated for convenience. - - The Traveling Merchant now sells artifacts and minerals, with a bias towards undonated ones, to mitigate randomness. She will sell these items as the player receives "Traveling Merchant Metal Detector" items. + - The items that are normally obtained from museum donation milestones are added to the item pool. Some items, like the magic rock candy, are duplicated for + convenience. + - The Traveling Merchant now sells artifacts and minerals, with a bias towards undonated ones, to mitigate randomness. She will sell these items as the + player receives "Traveling Merchant Metal Detector" items. - TV Channels - Babies - Only if Friendsanity is enabled There are a few extra vanilla items, which are added to the pool for convenience, but do not have a matching location. These include + - [Wizard Buildings](https://stardewvalleywiki.com/Wizard%27s_Tower#Buildings) - [Return Scepter](https://stardewvalleywiki.com/Return_Scepter) - [Qi Walnut Room QoL items](https://stardewvalleywiki.com/Qi%27s_Walnut_Room#Stock) And lastly, some Archipelago-exclusive items exist in the pool, which are designed around game balance and QoL. These include: + - Arcade Machine buffs (Only if the arcade machines are randomized) - - Journey of the Prairie King has drop rate increases, extra lives, and equipment - - Junimo Kart has extra lives. + - Journey of the Prairie King has drop rate increases, extra lives, and equipment + - Junimo Kart has extra lives. - Permanent Movement Speed Bonuses (customizable) -- Permanent Luck Bonuses (customizable) -- Traveling Merchant buffs +- Various Permanent Player Buffs (customizable) +- Traveling Merchant modifiers ## When the player receives an item, what happens? -Since Pelican Town is a remote area, it takes one business day for every item to reach the player. If an item is received -while online, it will appear in the player's mailbox the next morning, with a message from the sender telling them where +Since Pelican Town is a remote area, it takes one business day for every item to reach the player. If an item is received +while online, it will appear in the player's mailbox the next morning, with a message from the sender telling them where it was found. If an item is received while offline, it will be in the mailbox as soon as the player logs in. -Some items will be directly attached to the letter, while some others will instead be a world-wide unlock, and the letter +Some items will be directly attached to the letter, while some others will instead be a world-wide unlock, and the letter only serves to tell the player about it. -In some cases, like receiving Carpenter and Wizard buildings, the player will still need to go ask Robin to construct the +In some cases, like receiving Carpenter and Wizard buildings, the player will still need to go ask Robin to construct the building that they have received, so they can choose its position. This construction will be completely free. ## Mods Some Stardew Valley mods unrelated to Archipelago are officially "supported". -This means that, for these specific mods, if you decide to include them in your yaml options, the multiworld will be generated -with the assumption that you will install and play with these mods. The multiworld will contain related items and locations +This means that, for these specific mods, if you decide to include them in your yaml options, the multiworld will be generated +with the assumption that you will install and play with these mods. The multiworld will contain related items and locations for these mods, the specifics will vary from mod to mod [Supported Mods Documentation](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md) @@ -131,17 +144,14 @@ List of supported mods: - General - [Stardew Valley Expanded](https://www.nexusmods.com/stardewvalley/mods/3753) - - [DeepWoods](https://www.nexusmods.com/stardewvalley/mods/2571) - [Skull Cavern Elevator](https://www.nexusmods.com/stardewvalley/mods/963) - [Bigger Backpack](https://www.nexusmods.com/stardewvalley/mods/1845) - [Tractor Mod](https://www.nexusmods.com/stardewvalley/mods/1401) - [Distant Lands - Witch Swamp Overhaul](https://www.nexusmods.com/stardewvalley/mods/18109) - Skills - - [Magic](https://www.nexusmods.com/stardewvalley/mods/2007) - [Luck Skill](https://www.nexusmods.com/stardewvalley/mods/521) - [Socializing Skill](https://www.nexusmods.com/stardewvalley/mods/14142) - - [Archaeology](https://www.nexusmods.com/stardewvalley/mods/15793) - - [Cooking Skill](https://www.nexusmods.com/stardewvalley/mods/522) + - [Archaeology](https://www.nexusmods.com/stardewvalley/mods/22199) - [Binning Skill](https://www.nexusmods.com/stardewvalley/mods/14073) - NPCs - [Ayeisha - The Postal Worker (Custom NPC)](https://www.nexusmods.com/stardewvalley/mods/6427) @@ -149,20 +159,15 @@ List of supported mods: - [Juna - Roommate NPC](https://www.nexusmods.com/stardewvalley/mods/8606) - [Professor Jasper Thomas](https://www.nexusmods.com/stardewvalley/mods/5599) - [Alec Revisited](https://www.nexusmods.com/stardewvalley/mods/10697) - - [Custom NPC - Yoba](https://www.nexusmods.com/stardewvalley/mods/14871) - [Custom NPC Eugene](https://www.nexusmods.com/stardewvalley/mods/9222) - - ['Prophet' Wellwick](https://www.nexusmods.com/stardewvalley/mods/6462) - - [Shiko - New Custom NPC](https://www.nexusmods.com/stardewvalley/mods/3732) - - [Delores - Custom NPC](https://www.nexusmods.com/stardewvalley/mods/5510) - - [Custom NPC - Riley](https://www.nexusmods.com/stardewvalley/mods/5811) - [Alecto the Witch](https://www.nexusmods.com/stardewvalley/mods/10671) - -Some of these mods might need a patch mod to tie the randomizer with the mod. These can be found + +Some of these mods might need a patch mod to tie the randomizer with the mod. These can be found [here](https://github.com/Witchybun/SDV-Randomizer-Content-Patcher/releases) ## Multiplayer You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-term plans to support that feature. -You can, however, send Stardew Valley objects as gifts from one Stardew Player to another Stardew player, using in-game -Joja Prime delivery, for a fee. This exclusive feature can be turned off if you don't want to send and receive gifts. +You can, however, send Stardew Valley objects as gifts from one Stardew Player to another Stardew , or a player in another game that supports gifting, using +in-game Joja Prime delivery, for a fee. This exclusive feature can be turned off if you don't want to send and receive gifts. diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md index 74caf9b7daba..c672152543cf 100644 --- a/worlds/stardew_valley/docs/setup_en.md +++ b/worlds/stardew_valley/docs/setup_en.md @@ -2,14 +2,10 @@ ## Required Software -- Stardew Valley on PC (Recommended: [Steam version](https://store.steampowered.com/app/413150/Stardew_Valley/)) - - You need version 1.5.6. It is available in a public beta branch on Steam ![image](https://i.imgur.com/uKAUmF0.png). - - If your Stardew is not on Steam, you are responsible for finding a way to downgrade it. - - This measure is temporary. We are working hard to bring the mod to Stardew 1.6 as soon as possible. -- SMAPI 3.x.x ([Mod loader for Stardew Valley](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files)) - - Same as Stardew Valley itself, SMAPI needs a slightly older version to be compatible with Stardew Valley 1.5.6 ![image](https://i.imgur.com/kzgObHy.png) -- [StardewArchipelago Mod Release 5.x.x](https://github.com/agilbert1412/StardewArchipelago/releases) - - It is important to use a mod release of version 5.x.x to play seeds that have been generated here. Later releases +- Stardew Valley 1.6 on PC (Recommended: [Steam version](https://store.steampowered.com/app/413150/Stardew_Valley/)) +- SMAPI ([Mod loader for Stardew Valley](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files)) +- [StardewArchipelago Mod Release 6.x.x](https://github.com/agilbert1412/StardewArchipelago/releases) + - It is important to use a mod release of version 6.x.x to play seeds that have been generated here. Later releases can only be used with later releases of the world generator, that are not hosted on archipelago.gg yet. ## Optional Software @@ -38,7 +34,7 @@ You can customize your options by visiting the [Stardew Valley Player Options Pa ### Installing the mod -- Install [SMAPI version 3.x.x](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files) by following the instructions on the mod page +- Install [SMAPI](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files) by following the instructions on the mod page - Download and extract the [StardewArchipelago](https://github.com/agilbert1412/StardewArchipelago/releases) mod into your Stardew Valley "Mods" folder - *OPTIONAL*: If you want to launch your game through Steam, add the following to your Stardew Valley launch options: `"[PATH TO STARDEW VALLEY]\Stardew Valley\StardewModdingAPI.exe" %command%` @@ -93,7 +89,7 @@ Stardew-exclusive commands. ### Playing with supported mods -See the [Supported mods documentation](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md) +See the [Supported mods documentation](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md) ### Multiplayer diff --git a/worlds/stardew_valley/early_items.py b/worlds/stardew_valley/early_items.py index 78170f29fee7..e1ad8cebfd4a 100644 --- a/worlds/stardew_valley/early_items.py +++ b/worlds/stardew_valley/early_items.py @@ -1,51 +1,69 @@ from random import Random -from .options import BuildingProgression, StardewValleyOptions, BackpackProgression, ExcludeGingerIsland, SeasonRandomization, SpecialOrderLocations, \ - Monstersanity, ToolProgression, SkillProgression, Cooksanity, Chefsanity +from . import options as stardew_options +from .strings.ap_names.ap_weapon_names import APWeapon +from .strings.ap_names.transport_names import Transportation +from .strings.building_names import Building +from .strings.region_names import Region +from .strings.season_names import Season +from .strings.tv_channel_names import Channel +from .strings.wallet_item_names import Wallet early_candidate_rate = 4 -always_early_candidates = ["Greenhouse", "Desert Obelisk", "Rusty Key"] -seasons = ["Spring", "Summer", "Fall", "Winter"] +always_early_candidates = [Region.greenhouse, Transportation.desert_obelisk, Wallet.rusty_key] +seasons = [Season.spring, Season.summer, Season.fall, Season.winter] -def setup_early_items(multiworld, options: StardewValleyOptions, player: int, random: Random): +def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions, player: int, random: Random): early_forced = [] early_candidates = [] early_candidates.extend(always_early_candidates) add_seasonal_candidates(early_candidates, options) - if options.building_progression & BuildingProgression.option_progressive: - early_forced.append("Shipping Bin") - early_candidates.append("Progressive Coop") + if options.building_progression & stardew_options.BuildingProgression.option_progressive: + early_forced.append(Building.shipping_bin) + if options.farm_type != stardew_options.FarmType.option_meadowlands: + early_candidates.append("Progressive Coop") early_candidates.append("Progressive Barn") - if options.backpack_progression == BackpackProgression.option_early_progressive: + if options.backpack_progression == stardew_options.BackpackProgression.option_early_progressive: early_forced.append("Progressive Backpack") - if options.tool_progression & ToolProgression.option_progressive: - early_forced.append("Progressive Fishing Rod") + if options.tool_progression & stardew_options.ToolProgression.option_progressive: + if options.fishsanity != stardew_options.Fishsanity.option_none: + early_candidates.append("Progressive Fishing Rod") early_forced.append("Progressive Pickaxe") - if options.skill_progression == SkillProgression.option_progressive: + if options.skill_progression == stardew_options.SkillProgression.option_progressive: early_forced.append("Fishing Level") if options.quest_locations >= 0: - early_candidates.append("Magnifying Glass") + early_candidates.append(Wallet.magnifying_glass) - if options.special_order_locations != SpecialOrderLocations.option_disabled: + if options.special_order_locations & stardew_options.SpecialOrderLocations.option_board: early_candidates.append("Special Order Board") - if options.cooksanity != Cooksanity.option_none | options.chefsanity & Chefsanity.option_queen_of_sauce: - early_candidates.append("The Queen of Sauce") + if options.cooksanity != stardew_options.Cooksanity.option_none or options.chefsanity & stardew_options.Chefsanity.option_queen_of_sauce: + early_candidates.append(Channel.queen_of_sauce) - if options.monstersanity == Monstersanity.option_none: - early_candidates.append("Progressive Weapon") + if options.craftsanity != stardew_options.Craftsanity.option_none: + early_candidates.append("Furnace Recipe") + + if options.monstersanity == stardew_options.Monstersanity.option_none: + early_candidates.append(APWeapon.weapon) else: - early_candidates.append("Progressive Sword") + early_candidates.append(APWeapon.sword) + + if options.exclude_ginger_island == stardew_options.ExcludeGingerIsland.option_false: + early_candidates.append(Transportation.island_obelisk) + + if options.walnutsanity.value: + early_candidates.append("Island North Turtle") + early_candidates.append("Island West Turtle") - if options.exclude_ginger_island == ExcludeGingerIsland.option_false: - early_candidates.append("Island Obelisk") + if options.museumsanity != stardew_options.Museumsanity.option_none or options.shipsanity >= stardew_options.Shipsanity.option_full_shipment: + early_candidates.append(Wallet.metal_detector) early_forced.extend(random.sample(early_candidates, len(early_candidates) // early_candidate_rate)) @@ -56,10 +74,10 @@ def setup_early_items(multiworld, options: StardewValleyOptions, player: int, ra def add_seasonal_candidates(early_candidates, options): - if options.season_randomization == SeasonRandomization.option_progressive: - early_candidates.extend(["Progressive Season"] * 3) + if options.season_randomization == stardew_options.SeasonRandomization.option_progressive: + early_candidates.extend([Season.progressive] * 3) return - if options.season_randomization == SeasonRandomization.option_disabled: + if options.season_randomization == stardew_options.SeasonRandomization.option_disabled: return early_candidates.extend(seasons) diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index d0cb09bd9953..cb6102016942 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -8,18 +8,20 @@ from BaseClasses import Item, ItemClassification from . import data -from .data.villagers_data import get_villagers_for_mods +from .content.feature import friendsanity +from .content.game_content import StardewContent +from .data.game_item import ItemTag +from .logic.logic_event import all_events from .mods.mod_data import ModNames -from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Cropsanity, \ - Friendsanity, Museumsanity, \ - Fishsanity, BuildingProgression, SkillProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \ - Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity +from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \ + BuildingProgression, SkillProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \ + Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs +from .strings.ap_names.ap_option_names import OptionName from .strings.ap_names.ap_weapon_names import APWeapon from .strings.ap_names.buff_names import Buff from .strings.ap_names.community_upgrade_names import CommunityUpgrade -from .strings.ap_names.event_names import Event from .strings.ap_names.mods.mod_items import SVEQuestItem -from .strings.villager_names import NPC, ModNPC +from .strings.currency_names import Currency from .strings.wallet_item_names import Wallet ITEM_CODE_OFFSET = 717000 @@ -44,6 +46,7 @@ class Group(enum.Enum): WEAPON_SLINGSHOT = enum.auto() PROGRESSIVE_TOOLS = enum.auto() SKILL_LEVEL_UP = enum.auto() + SKILL_MASTERY = enum.auto() BUILDING = enum.auto() WIZARD_BUILDING = enum.auto() ARCADE_MACHINE_BUFFS = enum.auto() @@ -62,6 +65,7 @@ class Group(enum.Enum): FESTIVAL = enum.auto() RARECROW = enum.auto() TRAP = enum.auto() + BONUS = enum.auto() MAXIMUM_ONE = enum.auto() EXACTLY_TWO = enum.auto() DEPRECATED = enum.auto() @@ -80,6 +84,9 @@ class Group(enum.Enum): CHEFSANITY_FRIENDSHIP = enum.auto() CHEFSANITY_SKILL = enum.auto() CRAFTSANITY = enum.auto() + BOOK_POWER = enum.auto() + LOST_BOOK = enum.auto() + PLAYER_BUFF = enum.auto() # Mods MAGIC_SPELL = enum.auto() MOD_WARP = enum.auto() @@ -135,11 +142,8 @@ def load_item_csv(): events = [ - ItemData(None, Event.victory, ItemClassification.progression), - ItemData(None, Event.can_construct_buildings, ItemClassification.progression), - ItemData(None, Event.start_dark_talisman_quest, ItemClassification.progression), - ItemData(None, Event.can_ship_items, ItemClassification.progression), - ItemData(None, Event.can_shop_at_pierre, ItemClassification.progression), + ItemData(None, e, ItemClassification.progression) + for e in sorted(all_events) ] all_items: List[ItemData] = load_item_csv() + events @@ -168,9 +172,9 @@ def get_too_many_items_error_message(locations_count: int, items_count: int) -> def create_items(item_factory: StardewItemFactory, item_deleter: StardewItemDeleter, locations_count: int, items_to_exclude: List[Item], - options: StardewValleyOptions, random: Random) -> List[Item]: + options: StardewValleyOptions, content: StardewContent, random: Random) -> List[Item]: items = [] - unique_items = create_unique_items(item_factory, options, random) + unique_items = create_unique_items(item_factory, options, content, random) remove_items(item_deleter, items_to_exclude, unique_items) @@ -213,11 +217,12 @@ def remove_items_if_no_room_for_them(item_deleter: StardewItemDeleter, unique_it remove_items(item_deleter, items_to_remove, unique_items) -def create_unique_items(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random) -> List[Item]: +def create_unique_items(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, random: Random) -> List[Item]: items = [] items.extend(item_factory(item) for item in items_by_group[Group.COMMUNITY_REWARD]) items.append(item_factory(CommunityUpgrade.movie_theater)) # It is a community reward, but we need two of them + create_raccoons(item_factory, options, items) items.append(item_factory(Wallet.metal_detector)) # Always offer at least one metal detector create_backpack_items(item_factory, options, items) @@ -233,25 +238,30 @@ def create_unique_items(item_factory: StardewItemFactory, options: StardewValley items.append(item_factory(CommunityUpgrade.mushroom_boxes)) items.append(item_factory("Beach Bridge")) create_tv_channels(item_factory, options, items) - create_special_quest_rewards(item_factory, options, items) - create_stardrops(item_factory, options, items) + create_quest_rewards(item_factory, options, items) + create_stardrops(item_factory, options, content, items) create_museum_items(item_factory, options, items) create_arcade_machine_items(item_factory, options, items) - create_player_buffs(item_factory, options, items) + create_movement_buffs(item_factory, options, items) create_traveling_merchant_items(item_factory, items) items.append(item_factory("Return Scepter")) create_seasons(item_factory, options, items) - create_seeds(item_factory, options, items) - create_friendsanity_items(item_factory, options, items, random) + create_seeds(item_factory, content, items) + create_friendsanity_items(item_factory, options, content, items, random) create_festival_rewards(item_factory, options, items) create_special_order_board_rewards(item_factory, options, items) create_special_order_qi_rewards(item_factory, options, items) + create_walnuts(item_factory, options, items) create_walnut_purchase_rewards(item_factory, options, items) create_crafting_recipes(item_factory, options, items) create_cooking_recipes(item_factory, options, items) create_shipsanity_items(item_factory, options, items) + create_booksanity_items(item_factory, content, items) create_goal_items(item_factory, options, items) items.append(item_factory("Golden Egg")) + items.append(item_factory(CommunityUpgrade.mr_qi_plane_ride)) + + create_sve_special_items(item_factory, options, items) create_magic_mod_spells(item_factory, options, items) create_deepwoods_pendants(item_factory, options, items) create_archaeology_items(item_factory, options, items) @@ -259,6 +269,14 @@ def create_unique_items(item_factory: StardewItemFactory, options: StardewValley return items +def create_raccoons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + number_progressive_raccoons = 9 + if options.quest_locations < 0: + number_progressive_raccoons = number_progressive_raccoons - 1 + + items.extend(item_factory(item) for item in [CommunityUpgrade.raccoon] * number_progressive_raccoons) + + def create_backpack_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): if (options.backpack_progression == BackpackProgression.option_progressive or options.backpack_progression == BackpackProgression.option_early_progressive): @@ -310,15 +328,28 @@ def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions items.append(item_factory(item_data, ItemClassification.useful)) else: items.extend([item_factory(item) for item in [item_data] * 4]) - items.append(item_factory("Golden Scythe")) + if options.skill_progression == SkillProgression.option_progressive_with_masteries: + items.append(item_factory("Progressive Scythe")) + items.append(item_factory("Progressive Fishing Rod")) + items.append(item_factory("Progressive Scythe")) def create_skills(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.skill_progression == SkillProgression.option_progressive: - for item in items_by_group[Group.SKILL_LEVEL_UP]: - if item.mod_name not in options.mods and item.mod_name is not None: - continue - items.extend(item_factory(item) for item in [item.name] * 10) + if options.skill_progression == SkillProgression.option_vanilla: + return + + for item in items_by_group[Group.SKILL_LEVEL_UP]: + if item.mod_name not in options.mods and item.mod_name is not None: + continue + items.extend(item_factory(item) for item in [item.name] * 10) + + if options.skill_progression != SkillProgression.option_progressive_with_masteries: + return + + for item in items_by_group[Group.SKILL_MASTERY]: + if item.mod_name not in options.mods and item.mod_name is not None: + continue + items.append(item_factory(item)) def create_wizard_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): @@ -360,6 +391,13 @@ def create_carpenter_buildings(item_factory: StardewItemFactory, options: Starde items.append(item_factory("Tractor Garage")) +def create_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + create_special_quest_rewards(item_factory, options, items) + create_help_wanted_quest_rewards(item_factory, options, items) + + create_quest_rewards_sve(item_factory, options, items) + + def create_special_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): if options.quest_locations < 0: return @@ -373,21 +411,28 @@ def create_special_quest_rewards(item_factory: StardewItemFactory, options: Star items.append(item_factory(Wallet.iridium_snake_milk)) items.append(item_factory("Fairy Dust Recipe")) items.append(item_factory("Dark Talisman")) - create_special_quest_rewards_sve(item_factory, options, items) - create_distant_lands_quest_rewards(item_factory, options, items) - create_boarding_house_quest_rewards(item_factory, options, items) -def create_stardrops(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): +def create_help_wanted_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if options.quest_locations <= 0: + return + + number_help_wanted = options.quest_locations.value + quest_per_prize_ticket = 3 + number_prize_tickets = number_help_wanted // quest_per_prize_ticket + items.extend(item_factory(item) for item in [Currency.prize_ticket] * number_prize_tickets) + + +def create_stardrops(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item]): stardrops_classification = get_stardrop_classification(options) items.append(item_factory("Stardrop", stardrops_classification)) # The Mines level 100 items.append(item_factory("Stardrop", stardrops_classification)) # Old Master Cannoli items.append(item_factory("Stardrop", stardrops_classification)) # Krobus Stardrop - if options.fishsanity != Fishsanity.option_none: + if content.features.fishsanity.is_enabled: items.append(item_factory("Stardrop", stardrops_classification)) # Master Angler Stardrop if ModNames.deepwoods in options.mods: items.append(item_factory("Stardrop", stardrops_classification)) # Petting the Unicorn - if options.friendsanity != Friendsanity.option_none: + if content.features.friendsanity.is_enabled: items.append(item_factory("Stardrop", stardrops_classification)) # Spouse Stardrop @@ -403,39 +448,23 @@ def create_museum_items(item_factory: StardewItemFactory, options: StardewValley items.append(item_factory(Wallet.metal_detector)) -def create_friendsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item], random: Random): - island_villagers = [NPC.leo, ModNPC.lance] - if options.friendsanity == Friendsanity.option_none: +def create_friendsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item], random: Random): + if not content.features.friendsanity.is_enabled: return + create_babies(item_factory, items, random) - exclude_non_bachelors = options.friendsanity == Friendsanity.option_bachelors - exclude_locked_villagers = options.friendsanity == Friendsanity.option_starting_npcs or \ - options.friendsanity == Friendsanity.option_bachelors - include_post_marriage_hearts = options.friendsanity == Friendsanity.option_all_with_marriage - exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true - mods = options.mods - heart_size = options.friendsanity_heart_size - for villager in get_villagers_for_mods(mods.value): - if not villager.available and exclude_locked_villagers: - continue - if not villager.bachelor and exclude_non_bachelors: - continue - if villager.name in island_villagers and exclude_ginger_island: - continue - heart_cap = 8 if villager.bachelor else 10 - if include_post_marriage_hearts and villager.bachelor: - heart_cap = 14 - classification = ItemClassification.progression - for heart in range(1, 15): - if heart > heart_cap: - break - if heart % heart_size == 0 or heart == heart_cap: - items.append(item_factory(f"{villager.name} <3", classification)) - if not exclude_non_bachelors: - need_pet = options.goal == Goal.option_grandpa_evaluation - for heart in range(1, 6): - if heart % heart_size == 0 or heart == 5: - items.append(item_factory(f"Pet <3", ItemClassification.progression_skip_balancing if need_pet else ItemClassification.useful)) + + for villager in content.villagers.values(): + item_name = friendsanity.to_item_name(villager.name) + + for _ in content.features.friendsanity.get_randomized_hearts(villager): + items.append(item_factory(item_name, ItemClassification.progression)) + + need_pet = options.goal == Goal.option_grandpa_evaluation + pet_item_classification = ItemClassification.progression_skip_balancing if need_pet else ItemClassification.useful + + for _ in content.features.friendsanity.get_pet_randomized_hearts(): + items.append(item_factory(friendsanity.pet_heart_item_name, pet_item_classification)) def create_babies(item_factory: StardewItemFactory, items: List[Item], random: Random): @@ -462,26 +491,14 @@ def create_arcade_machine_items(item_factory: StardewItemFactory, options: Stard items.extend(item_factory(item) for item in ["Junimo Kart: Extra Life"] * 8) -def create_player_buffs(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): +def create_movement_buffs(item_factory, options: StardewValleyOptions, items: List[Item]): movement_buffs: int = options.movement_buff_number.value - luck_buffs: int = options.luck_buff_number.value - need_all_buffs = options.special_order_locations == SpecialOrderLocations.option_board_qi - need_half_buffs = options.festival_locations == FestivalLocations.option_easy - create_player_buff(item_factory, Buff.movement, movement_buffs, need_all_buffs, need_half_buffs, items) - create_player_buff(item_factory, Buff.luck, luck_buffs, True, need_half_buffs, items) - - -def create_player_buff(item_factory, buff: str, amount: int, need_all_buffs: bool, need_half_buffs: bool, items: List[Item]): - progression_buffs = amount if need_all_buffs else (amount // 2 if need_half_buffs else 0) - useful_buffs = amount - progression_buffs - items.extend(item_factory(item) for item in [buff] * progression_buffs) - items.extend(item_factory(item, ItemClassification.useful) for item in [buff] * useful_buffs) + items.extend(item_factory(item) for item in [Buff.movement] * movement_buffs) def create_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]): items.extend([*(item_factory(item) for item in items_by_group[Group.TRAVELING_MERCHANT_DAY]), - *(item_factory(item) for item in ["Traveling Merchant Stock Size"] * 6), - *(item_factory(item) for item in ["Traveling Merchant Discount"] * 8)]) + *(item_factory(item) for item in ["Traveling Merchant Stock Size"] * 6)]) def create_seasons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): @@ -495,14 +512,11 @@ def create_seasons(item_factory: StardewItemFactory, options: StardewValleyOptio items.extend([item_factory(item) for item in items_by_group[Group.SEASON]]) -def create_seeds(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.cropsanity == Cropsanity.option_disabled: +def create_seeds(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]): + if not content.features.cropsanity.is_enabled: return - base_seed_items = [item for item in items_by_group[Group.CROPSANITY]] - filtered_seed_items = remove_excluded_items(base_seed_items, options) - seed_items = [item_factory(item) for item in filtered_seed_items] - items.extend(seed_items) + items.extend(item_factory(item_table[seed.name]) for seed in content.find_tagged_items(ItemTag.CROPSANITY_SEED)) def create_festival_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): @@ -514,6 +528,35 @@ def create_festival_rewards(item_factory: StardewItemFactory, options: StardewVa items.extend([*festival_rewards, item_factory("Stardrop", get_stardrop_classification(options))]) +def create_walnuts(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + walnutsanity = options.walnutsanity + if options.exclude_ginger_island == ExcludeGingerIsland.option_true or walnutsanity == Walnutsanity.preset_none: + return + + # Give baseline walnuts just to be nice + num_single_walnuts = 0 + num_triple_walnuts = 2 + num_penta_walnuts = 1 + # https://stardewvalleywiki.com/Golden_Walnut + # Totals should be accurate, but distribution is slightly offset to make room for baseline walnuts + if OptionName.walnutsanity_puzzles in walnutsanity: # 61 + num_single_walnuts += 6 # 6 + num_triple_walnuts += 5 # 15 + num_penta_walnuts += 8 # 40 + if OptionName.walnutsanity_bushes in walnutsanity: # 25 + num_single_walnuts += 16 # 16 + num_triple_walnuts += 3 # 9 + if OptionName.walnutsanity_dig_spots in walnutsanity: # 18 + num_single_walnuts += 18 # 18 + if OptionName.walnutsanity_repeatables in walnutsanity: # 33 + num_single_walnuts += 30 # 30 + num_triple_walnuts += 1 # 3 + + items.extend([item_factory(item) for item in ["Golden Walnut"] * num_single_walnuts]) + items.extend([item_factory(item) for item in ["3 Golden Walnuts"] * num_triple_walnuts]) + items.extend([item_factory(item) for item in ["5 Golden Walnuts"] * num_penta_walnuts]) + + def create_walnut_purchase_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): if options.exclude_ginger_island == ExcludeGingerIsland.option_true: return @@ -526,12 +569,9 @@ def create_walnut_purchase_rewards(item_factory: StardewItemFactory, options: St def create_special_order_board_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.special_order_locations == SpecialOrderLocations.option_disabled: - return - - special_order_board_items = [item for item in items_by_group[Group.SPECIAL_ORDER_BOARD]] - - items.extend([item_factory(item) for item in special_order_board_items]) + if options.special_order_locations & SpecialOrderLocations.option_board: + special_order_board_items = [item for item in items_by_group[Group.SPECIAL_ORDER_BOARD]] + items.extend([item_factory(item) for item in special_order_board_items]) def special_order_board_item_classification(item: ItemData, need_all_recipes: bool) -> ItemClassification: @@ -554,7 +594,7 @@ def create_special_order_qi_rewards(item_factory: StardewItemFactory, options: S qi_gem_rewards.append("15 Qi Gems") qi_gem_rewards.append("15 Qi Gems") - if options.special_order_locations == SpecialOrderLocations.option_board_qi: + if options.special_order_locations & SpecialOrderLocations.value_qi: qi_gem_rewards.extend(["100 Qi Gems", "10 Qi Gems", "40 Qi Gems", "25 Qi Gems", "25 Qi Gems", "40 Qi Gems", "20 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems"]) @@ -607,6 +647,16 @@ def create_shipsanity_items(item_factory: StardewItemFactory, options: StardewVa items.append(item_factory(Wallet.metal_detector)) +def create_booksanity_items(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]): + booksanity = content.features.booksanity + if not booksanity.is_enabled: + return + + items.extend(item_factory(item_table[booksanity.to_item_name(book.name)]) for book in content.find_tagged_items(ItemTag.BOOK_POWER)) + progressive_lost_book = item_table[booksanity.progressive_lost_book] + items.extend(item_factory(progressive_lost_book) for _ in content.features.booksanity.get_randomized_lost_books()) + + def create_goal_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): goal = options.goal if goal != Goal.option_perfection and goal != Goal.option_complete_collection: @@ -643,35 +693,29 @@ def create_deepwoods_pendants(item_factory: StardewItemFactory, options: Stardew items.extend([item_factory(item) for item in ["Pendant of Elders", "Pendant of Community", "Pendant of Depths"]]) -def create_special_quest_rewards_sve(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): +def create_sve_special_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): if ModNames.sve not in options.mods: return items.extend([item_factory(item) for item in items_by_group[Group.MOD_WARP] if item.mod_name == ModNames.sve]) - if options.quest_locations < 0: - return - exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true - items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items]) - if exclude_ginger_island: +def create_quest_rewards_sve(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if ModNames.sve not in options.mods: return - items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items_ginger_island]) + exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true + items.extend([item_factory(item) for item in SVEQuestItem.sve_always_quest_items]) + if not exclude_ginger_island: + items.extend([item_factory(item) for item in SVEQuestItem.sve_always_quest_items_ginger_island]) -def create_distant_lands_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.quest_locations < 0 or ModNames.distant_lands not in options.mods: - return - items.append(item_factory("Crayfish Soup Recipe")) - if options.exclude_ginger_island == ExcludeGingerIsland.option_true: + if options.quest_locations < 0: return - items.append(item_factory("Ginger Tincture Recipe")) - -def create_boarding_house_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.quest_locations < 0 or ModNames.boarding_house not in options.mods: + items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items]) + if exclude_ginger_island: return - items.append(item_factory("Special Pumpkin Soup Recipe")) + items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items_ginger_island]) def create_unique_filler_items(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random, @@ -699,18 +743,21 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options items_already_added_names = [item.name for item in items_already_added] useful_resource_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK_USEFUL] if pack.name not in items_already_added_names] - trap_items = [pack for pack in items_by_group[Group.TRAP] - if pack.name not in items_already_added_names and - (pack.mod_name is None or pack.mod_name in options.mods)] + trap_items = [trap for trap in items_by_group[Group.TRAP] + if trap.name not in items_already_added_names and + (trap.mod_name is None or trap.mod_name in options.mods)] + player_buffs = get_allowed_player_buffs(options.enabled_filler_buffs) priority_filler_items = [] priority_filler_items.extend(useful_resource_packs) + priority_filler_items.extend(player_buffs) if include_traps: priority_filler_items.extend(trap_items) exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true all_filler_packs = remove_excluded_items(get_all_filler_items(include_traps, exclude_ginger_island), options) + all_filler_packs.extend(player_buffs) priority_filler_items = remove_excluded_items(priority_filler_items, options) number_priority_items = len(priority_filler_items) @@ -776,7 +823,7 @@ def remove_limited_amount_packs(packs): return [pack for pack in packs if Group.MAXIMUM_ONE not in pack.groups and Group.EXACTLY_TWO not in pack.groups] -def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool): +def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool) -> List[ItemData]: all_filler_items = [pack for pack in items_by_group[Group.RESOURCE_PACK]] all_filler_items.extend(items_by_group[Group.TRASH]) if include_traps: @@ -785,6 +832,33 @@ def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool): return all_filler_items +def get_allowed_player_buffs(buff_option: EnabledFillerBuffs) -> List[ItemData]: + allowed_buffs = [] + if OptionName.buff_luck in buff_option: + allowed_buffs.append(item_table[Buff.luck]) + if OptionName.buff_damage in buff_option: + allowed_buffs.append(item_table[Buff.damage]) + if OptionName.buff_defense in buff_option: + allowed_buffs.append(item_table[Buff.defense]) + if OptionName.buff_immunity in buff_option: + allowed_buffs.append(item_table[Buff.immunity]) + if OptionName.buff_health in buff_option: + allowed_buffs.append(item_table[Buff.health]) + if OptionName.buff_energy in buff_option: + allowed_buffs.append(item_table[Buff.energy]) + if OptionName.buff_bite in buff_option: + allowed_buffs.append(item_table[Buff.bite_rate]) + if OptionName.buff_fish_trap in buff_option: + allowed_buffs.append(item_table[Buff.fish_trap]) + if OptionName.buff_fishing_bar in buff_option: + allowed_buffs.append(item_table[Buff.fishing_bar]) + if OptionName.buff_quality in buff_option: + allowed_buffs.append(item_table[Buff.quality]) + if OptionName.buff_glow in buff_option: + allowed_buffs.append(item_table[Buff.glow]) + return allowed_buffs + + def get_stardrop_classification(options) -> ItemClassification: return ItemClassification.progression_skip_balancing if world_is_perfection(options) or world_is_stardrops(options) else ItemClassification.useful diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py index 103b3bd96081..43246a94a356 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -6,17 +6,17 @@ from . import data from .bundles.bundle_room import BundleRoom -from .data.fish_data import special_fish, get_fish_for_mods +from .content.game_content import StardewContent +from .data.game_item import ItemTag from .data.museum_data import all_museum_items -from .data.villagers_data import get_villagers_for_mods from .mods.mod_data import ModNames -from .options import ExcludeGingerIsland, Friendsanity, ArcadeMachineLocations, SpecialOrderLocations, Cropsanity, Fishsanity, Museumsanity, FestivalLocations, \ - SkillProgression, BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression +from .options import ExcludeGingerIsland, ArcadeMachineLocations, SpecialOrderLocations, Museumsanity, \ + FestivalLocations, SkillProgression, BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression, FarmType from .options import StardewValleyOptions, Craftsanity, Chefsanity, Cooksanity, Shipsanity, Monstersanity from .strings.goal_names import Goal -from .strings.quest_names import ModQuest -from .strings.region_names import Region -from .strings.villager_names import NPC, ModNPC +from .strings.quest_names import ModQuest, Quest +from .strings.region_names import Region, LogicRegion +from .strings.villager_names import NPC LOCATION_CODE_OFFSET = 717000 @@ -32,6 +32,7 @@ class LocationTags(enum.Enum): BULLETIN_BOARD_BUNDLE = enum.auto() VAULT_BUNDLE = enum.auto() COMMUNITY_CENTER_ROOM = enum.auto() + RACCOON_BUNDLES = enum.auto() BACKPACK = enum.auto() TOOL_UPGRADE = enum.auto() HOE_UPGRADE = enum.auto() @@ -40,6 +41,7 @@ class LocationTags(enum.Enum): WATERING_CAN_UPGRADE = enum.auto() TRASH_CAN_UPGRADE = enum.auto() FISHING_ROD_UPGRADE = enum.auto() + PAN_UPGRADE = enum.auto() THE_MINES_TREASURE = enum.auto() CROPSANITY = enum.auto() ELEVATOR = enum.auto() @@ -49,6 +51,7 @@ class LocationTags(enum.Enum): FORAGING_LEVEL = enum.auto() COMBAT_LEVEL = enum.auto() MINING_LEVEL = enum.auto() + MASTERY_LEVEL = enum.auto() BUILDING_BLUEPRINT = enum.auto() STORY_QUEST = enum.auto() ARCADE_MACHINE = enum.auto() @@ -63,11 +66,18 @@ class LocationTags(enum.Enum): FRIENDSANITY = enum.auto() FESTIVAL = enum.auto() FESTIVAL_HARD = enum.auto() + DESERT_FESTIVAL_CHEF = enum.auto() SPECIAL_ORDER_BOARD = enum.auto() SPECIAL_ORDER_QI = enum.auto() REQUIRES_QI_ORDERS = enum.auto() + REQUIRES_MASTERIES = enum.auto() GINGER_ISLAND = enum.auto() WALNUT_PURCHASE = enum.auto() + WALNUTSANITY = enum.auto() + WALNUTSANITY_PUZZLE = enum.auto() + WALNUTSANITY_BUSH = enum.auto() + WALNUTSANITY_DIG = enum.auto() + WALNUTSANITY_REPEATABLE = enum.auto() BABY = enum.auto() MONSTERSANITY = enum.auto() @@ -87,6 +97,10 @@ class LocationTags(enum.Enum): CHEFSANITY_SKILL = enum.auto() CHEFSANITY_STARTER = enum.auto() CRAFTSANITY = enum.auto() + BOOKSANITY = enum.auto() + BOOKSANITY_POWER = enum.auto() + BOOKSANITY_SKILL = enum.auto() + BOOKSANITY_LOST = enum.auto() # Mods # Skill Mods LUCK_LEVEL = enum.auto() @@ -143,10 +157,10 @@ def load_location_csv() -> List[LocationData]: LocationData(None, Region.farm_house, Goal.full_house), LocationData(None, Region.island_west, Goal.greatest_walnut_hunter), LocationData(None, Region.adventurer_guild, Goal.protector_of_the_valley), - LocationData(None, Region.shipping, Goal.full_shipment), - LocationData(None, Region.kitchen, Goal.gourmet_chef), + LocationData(None, LogicRegion.shipping, Goal.full_shipment), + LocationData(None, LogicRegion.kitchen, Goal.gourmet_chef), LocationData(None, Region.farm, Goal.craft_master), - LocationData(None, Region.shipping, Goal.legend), + LocationData(None, LogicRegion.shipping, Goal.legend), LocationData(None, Region.farm, Goal.mystery_of_the_stardrops), LocationData(None, Region.farm, Goal.allsanity), LocationData(None, Region.qi_walnut_room, Goal.perfection), @@ -168,13 +182,13 @@ def initialize_groups(): initialize_groups() -def extend_cropsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): - if options.cropsanity == Cropsanity.option_disabled: +def extend_cropsanity_locations(randomized_locations: List[LocationData], content: StardewContent): + cropsanity = content.features.cropsanity + if not cropsanity.is_enabled: return - cropsanity_locations = [item for item in locations_by_tag[LocationTags.CROPSANITY] if not item.mod_name or item.mod_name in options.mods] - cropsanity_locations = filter_ginger_island(options, cropsanity_locations) - randomized_locations.extend(cropsanity_locations) + randomized_locations.extend(location_table[cropsanity.to_location_name(item.name)] + for item in content.find_tagged_items(ItemTag.CROPSANITY)) def extend_quests_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): @@ -199,32 +213,19 @@ def extend_quests_locations(randomized_locations: List[LocationData], options: S randomized_locations.append(location_table[f"Help Wanted: Gathering {batch + 1}"]) -def extend_fishsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random): - prefix = "Fishsanity: " - fishsanity = options.fishsanity - active_fish = get_fish_for_mods(options.mods.value) - if fishsanity == Fishsanity.option_none: +def extend_fishsanity_locations(randomized_locations: List[LocationData], content: StardewContent, random: Random): + fishsanity = content.features.fishsanity + if not fishsanity.is_enabled: return - elif fishsanity == Fishsanity.option_legendaries: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if fish.legendary] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) - elif fishsanity == Fishsanity.option_special: - randomized_locations.extend(location_table[f"{prefix}{special.name}"] for special in special_fish) - elif fishsanity == Fishsanity.option_randomized: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if random.random() < 0.4] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) - elif fishsanity == Fishsanity.option_all: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) - elif fishsanity == Fishsanity.option_exclude_legendaries: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if not fish.legendary] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) - elif fishsanity == Fishsanity.option_exclude_hard_fish: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if fish.difficulty < 80] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) - elif options.fishsanity == Fishsanity.option_only_easy_fish: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if fish.difficulty < 50] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) + + for fish in content.fishes.values(): + if not fishsanity.is_included(fish): + continue + + if fishsanity.is_randomized and random.random() >= fishsanity.randomization_ratio: + continue + + randomized_locations.append(location_table[fishsanity.to_location_name(fish.name)]) def extend_museumsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random): @@ -240,38 +241,20 @@ def extend_museumsanity_locations(randomized_locations: List[LocationData], opti randomized_locations.extend(location_table[f"{prefix}{museum_item.item_name}"] for museum_item in all_museum_items) -def extend_friendsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): - island_villagers = [NPC.leo, ModNPC.lance] - if options.friendsanity == Friendsanity.option_none: +def extend_friendsanity_locations(randomized_locations: List[LocationData], content: StardewContent): + friendsanity = content.features.friendsanity + if not friendsanity.is_enabled: return randomized_locations.append(location_table[f"Spouse Stardrop"]) extend_baby_locations(randomized_locations) - exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true - exclude_non_bachelors = options.friendsanity == Friendsanity.option_bachelors - exclude_locked_villagers = options.friendsanity == Friendsanity.option_starting_npcs or \ - options.friendsanity == Friendsanity.option_bachelors - include_post_marriage_hearts = options.friendsanity == Friendsanity.option_all_with_marriage - heart_size = options.friendsanity_heart_size - for villager in get_villagers_for_mods(options.mods.value): - if not villager.available and exclude_locked_villagers: - continue - if not villager.bachelor and exclude_non_bachelors: - continue - if villager.name in island_villagers and exclude_ginger_island: - continue - heart_cap = 8 if villager.bachelor else 10 - if include_post_marriage_hearts and villager.bachelor: - heart_cap = 14 - for heart in range(1, 15): - if heart > heart_cap: - break - if heart % heart_size == 0 or heart == heart_cap: - randomized_locations.append(location_table[f"Friendsanity: {villager.name} {heart} <3"]) - if not exclude_non_bachelors: - for heart in range(1, 6): - if heart % heart_size == 0 or heart == 5: - randomized_locations.append(location_table[f"Friendsanity: Pet {heart} <3"]) + + for villager in content.villagers.values(): + for heart in friendsanity.get_randomized_hearts(villager): + randomized_locations.append(location_table[friendsanity.to_location_name(villager.name, heart)]) + + for heart in friendsanity.get_pet_randomized_hearts(): + randomized_locations.append(location_table[friendsanity.to_location_name(NPC.pet, heart)]) def extend_baby_locations(randomized_locations: List[LocationData]): @@ -279,16 +262,17 @@ def extend_baby_locations(randomized_locations: List[LocationData]): randomized_locations.extend(baby_locations) -def extend_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random): if options.festival_locations == FestivalLocations.option_disabled: return festival_locations = locations_by_tag[LocationTags.FESTIVAL] randomized_locations.extend(festival_locations) extend_hard_festival_locations(randomized_locations, options) + extend_desert_festival_chef_locations(randomized_locations, options, random) -def extend_hard_festival_locations(randomized_locations, options: StardewValleyOptions): +def extend_hard_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): if options.festival_locations != FestivalLocations.option_hard: return @@ -296,14 +280,20 @@ def extend_hard_festival_locations(randomized_locations, options: StardewValleyO randomized_locations.extend(hard_festival_locations) +def extend_desert_festival_chef_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random): + festival_chef_locations = locations_by_tag[LocationTags.DESERT_FESTIVAL_CHEF] + number_to_add = 5 if options.festival_locations == FestivalLocations.option_easy else 10 + locations_to_add = random.sample(festival_chef_locations, number_to_add) + randomized_locations.extend(locations_to_add) + + def extend_special_order_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): - if options.special_order_locations == SpecialOrderLocations.option_disabled: - return + if options.special_order_locations & SpecialOrderLocations.option_board: + board_locations = filter_disabled_locations(options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]) + randomized_locations.extend(board_locations) include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false - board_locations = filter_disabled_locations(options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]) - randomized_locations.extend(board_locations) - if options.special_order_locations == SpecialOrderLocations.option_board_qi and include_island: + if options.special_order_locations & SpecialOrderLocations.value_qi and include_island: include_arcade = options.arcade_machine_locations != ArcadeMachineLocations.option_disabled qi_orders = [location for location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI] if include_arcade or LocationTags.JUNIMO_KART not in location.tags] @@ -440,13 +430,43 @@ def extend_craftsanity_locations(randomized_locations: List[LocationData], optio return craftsanity_locations = [craft for craft in locations_by_tag[LocationTags.CRAFTSANITY]] - filtered_chefsanity_locations = filter_disabled_locations(options, craftsanity_locations) - randomized_locations.extend(filtered_chefsanity_locations) + filtered_craftsanity_locations = filter_disabled_locations(options, craftsanity_locations) + randomized_locations.extend(filtered_craftsanity_locations) + + +def extend_book_locations(randomized_locations: List[LocationData], content: StardewContent): + booksanity = content.features.booksanity + if not booksanity.is_enabled: + return + + book_locations = [] + for book in content.find_tagged_items(ItemTag.BOOK): + if booksanity.is_included(book): + book_locations.append(location_table[booksanity.to_location_name(book.name)]) + + book_locations.extend(location_table[booksanity.to_location_name(book)] for book in booksanity.get_randomized_lost_books()) + + randomized_locations.extend(book_locations) + + +def extend_walnutsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): + if not options.walnutsanity: + return + + if "Puzzles" in options.walnutsanity: + randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_PUZZLE]) + if "Bushes" in options.walnutsanity: + randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_BUSH]) + if "Dig Spots" in options.walnutsanity: + randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_DIG]) + if "Repeatables" in options.walnutsanity: + randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_REPEATABLE]) def create_locations(location_collector: StardewLocationCollector, bundle_rooms: List[BundleRoom], options: StardewValleyOptions, + content: StardewContent, random: Random): randomized_locations = [] @@ -461,8 +481,11 @@ def create_locations(location_collector: StardewLocationCollector, if not options.skill_progression == SkillProgression.option_vanilla: for location in locations_by_tag[LocationTags.SKILL_LEVEL]: - if location.mod_name is None or location.mod_name in options.mods: - randomized_locations.append(location_table[location.name]) + if location.mod_name is not None and location.mod_name not in options.mods: + continue + if LocationTags.MASTERY_LEVEL in location.tags and options.skill_progression != SkillProgression.option_progressive_with_masteries: + continue + randomized_locations.append(location_table[location.name]) if options.building_progression & BuildingProgression.option_progressive: for location in locations_by_tag[LocationTags.BUILDING_BLUEPRINT]: @@ -475,12 +498,12 @@ def create_locations(location_collector: StardewLocationCollector, if options.arcade_machine_locations == ArcadeMachineLocations.option_full_shuffling: randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE]) - extend_cropsanity_locations(randomized_locations, options) - extend_fishsanity_locations(randomized_locations, options, random) + extend_cropsanity_locations(randomized_locations, content) + extend_fishsanity_locations(randomized_locations, content, random) extend_museumsanity_locations(randomized_locations, options, random) - extend_friendsanity_locations(randomized_locations, options) + extend_friendsanity_locations(randomized_locations, content) - extend_festival_locations(randomized_locations, options) + extend_festival_locations(randomized_locations, options, random) extend_special_order_locations(randomized_locations, options) extend_walnut_purchase_locations(randomized_locations, options) @@ -490,28 +513,47 @@ def create_locations(location_collector: StardewLocationCollector, extend_chefsanity_locations(randomized_locations, options) extend_craftsanity_locations(randomized_locations, options) extend_quests_locations(randomized_locations, options) + extend_book_locations(randomized_locations, content) + extend_walnutsanity_locations(randomized_locations, options) + + # Mods extend_situational_quest_locations(randomized_locations, options) for location_data in randomized_locations: location_collector(location_data.name, location_data.code, location_data.region) +def filter_farm_type(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: + # On Meadowlands, "Feeding Animals" replaces "Raising Animals" + if options.farm_type == FarmType.option_meadowlands: + return (location for location in locations if location.name != Quest.raising_animals) + else: + return (location for location in locations if location.name != Quest.feeding_animals) + + def filter_ginger_island(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false return (location for location in locations if include_island or LocationTags.GINGER_ISLAND not in location.tags) def filter_qi_order_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: - include_qi_orders = options.special_order_locations == SpecialOrderLocations.option_board_qi + include_qi_orders = options.special_order_locations & SpecialOrderLocations.value_qi return (location for location in locations if include_qi_orders or LocationTags.REQUIRES_QI_ORDERS not in location.tags) +def filter_masteries_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: + include_masteries = options.skill_progression == SkillProgression.option_progressive_with_masteries + return (location for location in locations if include_masteries or LocationTags.REQUIRES_MASTERIES not in location.tags) + + def filter_modded_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: return (location for location in locations if location.mod_name is None or location.mod_name in options.mods) def filter_disabled_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: - locations_island_filter = filter_ginger_island(options, locations) + locations_farm_filter = filter_farm_type(options, locations) + locations_island_filter = filter_ginger_island(options, locations_farm_filter) locations_qi_filter = filter_qi_order_locations(options, locations_island_filter) - locations_mod_filter = filter_modded_locations(options, locations_qi_filter) + locations_masteries_filter = filter_masteries_locations(options, locations_qi_filter) + locations_mod_filter = filter_modded_locations(options, locations_masteries_filter) return locations_mod_filter diff --git a/worlds/stardew_valley/logic/ability_logic.py b/worlds/stardew_valley/logic/ability_logic.py index ae12ffee4742..add99a2c2e7e 100644 --- a/worlds/stardew_valley/logic/ability_logic.py +++ b/worlds/stardew_valley/logic/ability_logic.py @@ -1,6 +1,7 @@ from typing import Union from .base_logic import BaseLogicMixin, BaseLogic +from .cooking_logic import CookingLogicMixin from .mine_logic import MineLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin diff --git a/worlds/stardew_valley/logic/action_logic.py b/worlds/stardew_valley/logic/action_logic.py index 820ae4ead429..dc5deda427f3 100644 --- a/worlds/stardew_valley/logic/action_logic.py +++ b/worlds/stardew_valley/logic/action_logic.py @@ -5,10 +5,13 @@ from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin -from ..stardew_rule import StardewRule, True_, Or +from .tool_logic import ToolLogicMixin +from ..options import ToolProgression +from ..stardew_rule import StardewRule, True_ from ..strings.generic_names import Generic from ..strings.geode_names import Geode from ..strings.region_names import Region +from ..strings.tool_names import Tool class ActionLogicMixin(BaseLogicMixin): @@ -17,7 +20,7 @@ def __init__(self, *args, **kwargs): self.action = ActionLogic(*args, **kwargs) -class ActionLogic(BaseLogic[Union[ActionLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin]]): +class ActionLogic(BaseLogic[Union[ActionLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, ToolLogicMixin]]): def can_watch(self, channel: str = None): tv_rule = True_() @@ -25,16 +28,13 @@ def can_watch(self, channel: str = None): return tv_rule return self.logic.received(channel) & tv_rule - def can_pan(self) -> StardewRule: - return self.logic.received("Glittering Boulder Removed") & self.logic.region.can_reach(Region.mountain) - - def can_pan_at(self, region: str) -> StardewRule: - return self.logic.region.can_reach(region) & self.logic.action.can_pan() + def can_pan_at(self, region: str, material: str) -> StardewRule: + return self.logic.region.can_reach(region) & self.logic.tool.has_tool(Tool.pan, material) @cache_self1 def can_open_geode(self, geode: str) -> StardewRule: blacksmith_access = self.logic.region.can_reach(Region.blacksmith) geodes = [Geode.geode, Geode.frozen, Geode.magma, Geode.omni] if geode == Generic.any: - return blacksmith_access & Or(*(self.logic.has(geode_type) for geode_type in geodes)) + return blacksmith_access & self.logic.or_(*(self.logic.has(geode_type) for geode_type in geodes)) return blacksmith_access & self.logic.has(geode) diff --git a/worlds/stardew_valley/logic/artisan_logic.py b/worlds/stardew_valley/logic/artisan_logic.py index cdc2186d807a..23f0ae03b790 100644 --- a/worlds/stardew_valley/logic/artisan_logic.py +++ b/worlds/stardew_valley/logic/artisan_logic.py @@ -3,8 +3,13 @@ from .base_logic import BaseLogic, BaseLogicMixin from .has_logic import HasLogicMixin from .time_logic import TimeLogicMixin +from ..data.artisan import MachineSource +from ..data.game_item import ItemTag from ..stardew_rule import StardewRule -from ..strings.crop_names import all_vegetables, all_fruits, Vegetable, Fruit +from ..strings.artisan_good_names import ArtisanGood +from ..strings.crop_names import Vegetable, Fruit +from ..strings.fish_names import Fish, all_fish +from ..strings.forageable_names import Mushroom from ..strings.generic_names import Generic from ..strings.machine_names import Machine @@ -16,6 +21,10 @@ def __init__(self, *args, **kwargs): class ArtisanLogic(BaseLogic[Union[ArtisanLogicMixin, TimeLogicMixin, HasLogicMixin]]): + def initialize_rules(self): + # TODO remove this one too once fish are converted to sources + self.registry.artisan_good_rules.update({ArtisanGood.specific_smoked_fish(fish): self.can_smoke(fish) for fish in all_fish}) + self.registry.artisan_good_rules.update({ArtisanGood.specific_bait(fish): self.can_bait(fish) for fish in all_fish}) def has_jelly(self) -> StardewRule: return self.logic.artisan.can_preserves_jar(Fruit.any) @@ -23,31 +32,62 @@ def has_jelly(self) -> StardewRule: def has_pickle(self) -> StardewRule: return self.logic.artisan.can_preserves_jar(Vegetable.any) + def has_smoked_fish(self) -> StardewRule: + return self.logic.artisan.can_smoke(Fish.any) + + def has_targeted_bait(self) -> StardewRule: + return self.logic.artisan.can_bait(Fish.any) + + def has_dried_fruits(self) -> StardewRule: + return self.logic.artisan.can_dehydrate(Fruit.any) + + def has_dried_mushrooms(self) -> StardewRule: + return self.logic.artisan.can_dehydrate(Mushroom.any_edible) + + def has_raisins(self) -> StardewRule: + return self.logic.artisan.can_dehydrate(Fruit.grape) + + def can_produce_from(self, source: MachineSource) -> StardewRule: + return self.logic.has(source.item) & self.logic.has(source.machine) + def can_preserves_jar(self, item: str) -> StardewRule: machine_rule = self.logic.has(Machine.preserves_jar) if item == Generic.any: return machine_rule if item == Fruit.any: - return machine_rule & self.logic.has_any(*all_fruits) + return machine_rule & self.logic.has_any(*(fruit.name for fruit in self.content.find_tagged_items(ItemTag.FRUIT))) if item == Vegetable.any: - return machine_rule & self.logic.has_any(*all_vegetables) + return machine_rule & self.logic.has_any(*(vege.name for vege in self.content.find_tagged_items(ItemTag.VEGETABLE))) return machine_rule & self.logic.has(item) - def has_wine(self) -> StardewRule: - return self.logic.artisan.can_keg(Fruit.any) - - def has_juice(self) -> StardewRule: - return self.logic.artisan.can_keg(Vegetable.any) - def can_keg(self, item: str) -> StardewRule: machine_rule = self.logic.has(Machine.keg) if item == Generic.any: return machine_rule if item == Fruit.any: - return machine_rule & self.logic.has_any(*all_fruits) + return machine_rule & self.logic.has_any(*(fruit.name for fruit in self.content.find_tagged_items(ItemTag.FRUIT))) if item == Vegetable.any: - return machine_rule & self.logic.has_any(*all_vegetables) + return machine_rule & self.logic.has_any(*(vege.name for vege in self.content.find_tagged_items(ItemTag.VEGETABLE))) return machine_rule & self.logic.has(item) def can_mayonnaise(self, item: str) -> StardewRule: return self.logic.has(Machine.mayonnaise_machine) & self.logic.has(item) + + def can_smoke(self, item: str) -> StardewRule: + machine_rule = self.logic.has(Machine.fish_smoker) + return machine_rule & self.logic.has(item) + + def can_bait(self, item: str) -> StardewRule: + machine_rule = self.logic.has(Machine.bait_maker) + return machine_rule & self.logic.has(item) + + def can_dehydrate(self, item: str) -> StardewRule: + machine_rule = self.logic.has(Machine.dehydrator) + if item == Generic.any: + return machine_rule + if item == Fruit.any: + # Grapes make raisins + return machine_rule & self.logic.has_any(*(fruit.name for fruit in self.content.find_tagged_items(ItemTag.FRUIT) if fruit.name != Fruit.grape)) + if item == Mushroom.any_edible: + return machine_rule & self.logic.has_any(*(mushroom.name for mushroom in self.content.find_tagged_items(ItemTag.EDIBLE_MUSHROOM))) + return machine_rule & self.logic.has(item) diff --git a/worlds/stardew_valley/logic/base_logic.py b/worlds/stardew_valley/logic/base_logic.py index 9cfd089ea4f6..7b377fce1fcc 100644 --- a/worlds/stardew_valley/logic/base_logic.py +++ b/worlds/stardew_valley/logic/base_logic.py @@ -2,6 +2,7 @@ from typing import TypeVar, Generic, Dict, Collection +from ..content.game_content import StardewContent from ..options import StardewValleyOptions from ..stardew_rule import StardewRule @@ -10,12 +11,11 @@ class LogicRegistry: def __init__(self): self.item_rules: Dict[str, StardewRule] = {} - self.sapling_rules: Dict[str, StardewRule] = {} - self.tree_fruit_rules: Dict[str, StardewRule] = {} self.seed_rules: Dict[str, StardewRule] = {} self.cooking_rules: Dict[str, StardewRule] = {} self.crafting_rules: Dict[str, StardewRule] = {} self.crop_rules: Dict[str, StardewRule] = {} + self.artisan_good_rules: Dict[str, StardewRule] = {} self.fish_rules: Dict[str, StardewRule] = {} self.museum_rules: Dict[str, StardewRule] = {} self.festival_rules: Dict[str, StardewRule] = {} @@ -38,13 +38,15 @@ class BaseLogic(BaseLogicMixin, Generic[T]): player: int registry: LogicRegistry options: StardewValleyOptions + content: StardewContent regions: Collection[str] logic: T - def __init__(self, player: int, registry: LogicRegistry, options: StardewValleyOptions, regions: Collection[str], logic: T): - super().__init__(player, registry, options, regions, logic) + def __init__(self, player: int, registry: LogicRegistry, options: StardewValleyOptions, content: StardewContent, regions: Collection[str], logic: T): + super().__init__(player, registry, options, content, regions, logic) self.player = player self.registry = registry self.options = options + self.content = content self.regions = regions self.logic = logic diff --git a/worlds/stardew_valley/logic/book_logic.py b/worlds/stardew_valley/logic/book_logic.py new file mode 100644 index 000000000000..464056ee06ba --- /dev/null +++ b/worlds/stardew_valley/logic/book_logic.py @@ -0,0 +1,24 @@ +from typing import Union + +from Utils import cache_self1 +from .base_logic import BaseLogicMixin, BaseLogic +from .has_logic import HasLogicMixin +from .received_logic import ReceivedLogicMixin +from ..stardew_rule import StardewRule + + +class BookLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.book = BookLogic(*args, **kwargs) + + +class BookLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin]]): + + @cache_self1 + def has_book_power(self, book: str) -> StardewRule: + booksanity = self.content.features.booksanity + if booksanity.is_included(self.content.game_items[book]): + return self.logic.received(booksanity.to_item_name(book)) + else: + return self.logic.has(book) diff --git a/worlds/stardew_valley/logic/buff_logic.py b/worlds/stardew_valley/logic/buff_logic.py deleted file mode 100644 index fee9c9fc4d25..000000000000 --- a/worlds/stardew_valley/logic/buff_logic.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Union - -from .base_logic import BaseLogicMixin, BaseLogic -from .received_logic import ReceivedLogicMixin -from ..stardew_rule import StardewRule -from ..strings.ap_names.buff_names import Buff - - -class BuffLogicMixin(BaseLogicMixin): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.buff = BuffLogic(*args, **kwargs) - - -class BuffLogic(BaseLogic[Union[ReceivedLogicMixin]]): - def has_max_buffs(self) -> StardewRule: - return self.has_max_speed() & self.has_max_luck() - - def has_max_speed(self) -> StardewRule: - return self.logic.received(Buff.movement, self.options.movement_buff_number.value) - - def has_max_luck(self) -> StardewRule: - return self.logic.received(Buff.luck, self.options.luck_buff_number.value) diff --git a/worlds/stardew_valley/logic/building_logic.py b/worlds/stardew_valley/logic/building_logic.py index 7be3d19ec33b..4611eba37d64 100644 --- a/worlds/stardew_valley/logic/building_logic.py +++ b/worlds/stardew_valley/logic/building_logic.py @@ -15,6 +15,8 @@ from ..strings.material_names import Material from ..strings.metal_names import MetalBar +has_group = "building" + class BuildingLogicMixin(BaseLogicMixin): def __init__(self, *args, **kwargs): @@ -42,7 +44,7 @@ def initialize_rules(self): 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(50000) & self.logic.has(Material.hardwood) & self.logic.building.has_house(1), + 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 }) @@ -60,7 +62,7 @@ def has_building(self, building: str) -> StardewRule: carpenter_rule = self.logic.received(Event.can_construct_buildings) if not self.options.building_progression & BuildingProgression.option_progressive: - return Has(building, self.registry.building_rules) & carpenter_rule + return Has(building, self.registry.building_rules, has_group) & carpenter_rule count = 1 if building in [Building.coop, Building.barn, Building.shed]: @@ -86,10 +88,10 @@ def has_house(self, upgrade_level: int) -> StardewRule: 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) + 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) + 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) + return carpenter_rule & Has(Building.cellar, self.registry.building_rules, has_group) diff --git a/worlds/stardew_valley/logic/bundle_logic.py b/worlds/stardew_valley/logic/bundle_logic.py index 1ae07cf2ed82..4ca5fd81fc76 100644 --- a/worlds/stardew_valley/logic/bundle_logic.py +++ b/worlds/stardew_valley/logic/bundle_logic.py @@ -2,17 +2,22 @@ from typing import Union, List from .base_logic import BaseLogicMixin, BaseLogic -from .farming_logic import FarmingLogicMixin from .fishing_logic import FishingLogicMixin from .has_logic import HasLogicMixin from .money_logic import MoneyLogicMixin +from .quality_logic import QualityLogicMixin +from .quest_logic import QuestLogicMixin +from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .skill_logic import SkillLogicMixin +from .time_logic import TimeLogicMixin from ..bundles.bundle import Bundle -from ..stardew_rule import StardewRule, And, True_ +from ..stardew_rule import StardewRule, True_ +from ..strings.ap_names.community_upgrade_names import CommunityUpgrade from ..strings.currency_names import Currency from ..strings.machine_names import Machine from ..strings.quality_names import CropQuality, ForageQuality, FishQuality, ArtisanQuality +from ..strings.quest_names import Quest from ..strings.region_names import Region @@ -22,21 +27,26 @@ def __init__(self, *args, **kwargs): self.bundle = BundleLogic(*args, **kwargs) -class BundleLogic(BaseLogic[Union[HasLogicMixin, RegionLogicMixin, MoneyLogicMixin, FarmingLogicMixin, FishingLogicMixin, SkillLogicMixin]]): +class BundleLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, MoneyLogicMixin, QualityLogicMixin, FishingLogicMixin, SkillLogicMixin, +QuestLogicMixin]]): # Should be cached def can_complete_bundle(self, bundle: Bundle) -> StardewRule: item_rules = [] qualities = [] + time_to_grind = 0 can_speak_junimo = self.logic.region.can_reach(Region.wizard_tower) for bundle_item in bundle.items: - if Currency.is_currency(bundle_item.item_name): - return can_speak_junimo & self.logic.money.can_trade(bundle_item.item_name, bundle_item.amount) + if Currency.is_currency(bundle_item.get_item()): + return can_speak_junimo & self.logic.money.can_trade(bundle_item.get_item(), bundle_item.amount) - item_rules.append(bundle_item.item_name) + item_rules.append(bundle_item.get_item()) + if bundle_item.amount > 50: + time_to_grind = bundle_item.amount // 50 qualities.append(bundle_item.quality) quality_rules = self.get_quality_rules(qualities) item_rules = self.logic.has_n(*item_rules, count=bundle.number_required) - return can_speak_junimo & item_rules & quality_rules + time_rule = True_() if time_to_grind <= 0 else self.logic.time.has_lived_months(time_to_grind) + return can_speak_junimo & item_rules & quality_rules & time_rule def get_quality_rules(self, qualities: List[str]) -> StardewRule: crop_quality = CropQuality.get_highest(qualities) @@ -45,7 +55,7 @@ def get_quality_rules(self, qualities: List[str]) -> StardewRule: artisan_quality = ArtisanQuality.get_highest(qualities) quality_rules = [] if crop_quality != CropQuality.basic: - quality_rules.append(self.logic.farming.can_grow_crop_quality(crop_quality)) + quality_rules.append(self.logic.quality.can_grow_crop_quality(crop_quality)) if fish_quality != FishQuality.basic: quality_rules.append(self.logic.fishing.can_catch_quality_fish(fish_quality)) if forage_quality != ForageQuality.basic: @@ -54,7 +64,7 @@ def get_quality_rules(self, qualities: List[str]) -> StardewRule: quality_rules.append(self.logic.has(Machine.cask)) if not quality_rules: return True_() - return And(*quality_rules) + return self.logic.and_(*quality_rules) @cached_property def can_complete_community_center(self) -> StardewRule: @@ -64,3 +74,11 @@ def can_complete_community_center(self) -> StardewRule: self.logic.region.can_reach_location("Complete Bulletin Board") & self.logic.region.can_reach_location("Complete Vault") & self.logic.region.can_reach_location("Complete Boiler Room")) + + def can_access_raccoon_bundles(self) -> StardewRule: + if self.options.quest_locations < 0: + return self.logic.received(CommunityUpgrade.raccoon, 1) & self.logic.quest.can_complete_quest(Quest.giant_stump) + + # 1 - Break the tree + # 2 - Build the house, which summons the bundle racoon. This one is done manually if quests are turned off + return self.logic.received(CommunityUpgrade.raccoon, 2) diff --git a/worlds/stardew_valley/logic/combat_logic.py b/worlds/stardew_valley/logic/combat_logic.py index ba825192a99e..849bf14b2203 100644 --- a/worlds/stardew_valley/logic/combat_logic.py +++ b/worlds/stardew_valley/logic/combat_logic.py @@ -3,10 +3,11 @@ from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic +from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from ..mods.logic.magic_logic import MagicLogicMixin -from ..stardew_rule import StardewRule, Or, False_ +from ..stardew_rule import StardewRule, False_ from ..strings.ap_names.ap_weapon_names import APWeapon from ..strings.performance_names import Performance @@ -19,7 +20,7 @@ def __init__(self, *args, **kwargs): self.combat = CombatLogic(*args, **kwargs) -class CombatLogic(BaseLogic[Union[CombatLogicMixin, RegionLogicMixin, ReceivedLogicMixin, MagicLogicMixin]]): +class CombatLogic(BaseLogic[Union[HasLogicMixin, CombatLogicMixin, RegionLogicMixin, ReceivedLogicMixin, MagicLogicMixin]]): @cache_self1 def can_fight_at_level(self, level: str) -> StardewRule: if level == Performance.basic: @@ -42,16 +43,20 @@ def has_any_weapon(self) -> StardewRule: @cached_property def has_decent_weapon(self) -> StardewRule: - return Or(*(self.logic.received(weapon, 2) for weapon in valid_weapons)) + return self.logic.or_(*(self.logic.received(weapon, 2) for weapon in valid_weapons)) @cached_property def has_good_weapon(self) -> StardewRule: - return Or(*(self.logic.received(weapon, 3) for weapon in valid_weapons)) + return self.logic.or_(*(self.logic.received(weapon, 3) for weapon in valid_weapons)) @cached_property def has_great_weapon(self) -> StardewRule: - return Or(*(self.logic.received(weapon, 4) for weapon in valid_weapons)) + return self.logic.or_(*(self.logic.received(weapon, 4) for weapon in valid_weapons)) @cached_property def has_galaxy_weapon(self) -> StardewRule: - return Or(*(self.logic.received(weapon, 5) for weapon in valid_weapons)) + return self.logic.or_(*(self.logic.received(weapon, 5) for weapon in valid_weapons)) + + @cached_property + def has_slingshot(self) -> StardewRule: + return self.logic.received(APWeapon.slingshot) diff --git a/worlds/stardew_valley/logic/cooking_logic.py b/worlds/stardew_valley/logic/cooking_logic.py index 51cc74d0517a..46f3bdc93f2f 100644 --- a/worlds/stardew_valley/logic/cooking_logic.py +++ b/worlds/stardew_valley/logic/cooking_logic.py @@ -19,8 +19,8 @@ from ..locations import locations_by_tag, LocationTags from ..options import Chefsanity from ..options import ExcludeGingerIsland -from ..stardew_rule import StardewRule, True_, False_, And -from ..strings.region_names import Region +from ..stardew_rule import StardewRule, True_, False_ +from ..strings.region_names import LogicRegion from ..strings.skill_names import Skill from ..strings.tv_channel_names import Channel @@ -39,7 +39,7 @@ def can_cook_in_kitchen(self) -> StardewRule: # Should be cached def can_cook(self, recipe: CookingRecipe = None) -> StardewRule: - cook_rule = self.logic.region.can_reach(Region.kitchen) + cook_rule = self.logic.region.can_reach(LogicRegion.kitchen) if recipe is None: return cook_rule @@ -65,7 +65,7 @@ def knows_recipe(self, source: RecipeSource, meal_name: str) -> StardewRule: return self.logic.cooking.received_recipe(meal_name) if isinstance(source, QueenOfSauceSource) and self.options.chefsanity & Chefsanity.option_queen_of_sauce: return self.logic.cooking.received_recipe(meal_name) - if isinstance(source, ShopFriendshipSource) and self.options.chefsanity & Chefsanity.option_friendship: + if isinstance(source, ShopFriendshipSource) and self.options.chefsanity & Chefsanity.option_purchases: return self.logic.cooking.received_recipe(meal_name) return self.logic.cooking.can_learn_recipe(source) @@ -105,4 +105,4 @@ def can_cook_everything(self) -> StardewRule: continue all_recipes_names.append(location.name[len(cooksanity_prefix):]) all_recipes = [all_cooking_recipes_by_name[recipe_name] for recipe_name in all_recipes_names] - return And(*(self.logic.cooking.can_cook(recipe) for recipe in all_recipes)) + return self.logic.and_(*(self.logic.cooking.can_cook(recipe) for recipe in all_recipes)) diff --git a/worlds/stardew_valley/logic/crafting_logic.py b/worlds/stardew_valley/logic/crafting_logic.py index 8c267b7d1090..e346e4ba238b 100644 --- a/worlds/stardew_valley/logic/crafting_logic.py +++ b/worlds/stardew_valley/logic/crafting_logic.py @@ -13,12 +13,11 @@ from .special_order_logic import SpecialOrderLogicMixin from .. import options from ..data.craftable_data import CraftingRecipe, all_crafting_recipes_by_name -from ..data.recipe_data import StarterSource, ShopSource, SkillSource, FriendshipSource from ..data.recipe_source import CutsceneSource, ShopTradeSource, ArchipelagoSource, LogicSource, SpecialOrderSource, \ - FestivalShopSource, QuestSource + FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource from ..locations import locations_by_tag, LocationTags -from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland -from ..stardew_rule import StardewRule, True_, False_, And +from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland, SkillProgression +from ..stardew_rule import StardewRule, True_, False_ from ..strings.region_names import Region @@ -58,7 +57,7 @@ def knows_recipe(self, recipe: CraftingRecipe) -> StardewRule: if isinstance(recipe.source, StarterSource) or isinstance(recipe.source, ShopTradeSource) or isinstance( recipe.source, ShopSource): return self.logic.crafting.received_recipe(recipe.item) - if isinstance(recipe.source, SpecialOrderSource) and self.options.special_order_locations != SpecialOrderLocations.option_disabled: + if isinstance(recipe.source, SpecialOrderSource) and self.options.special_order_locations & SpecialOrderLocations.option_board: return self.logic.crafting.received_recipe(recipe.item) return self.logic.crafting.can_learn_recipe(recipe) @@ -74,6 +73,8 @@ def can_learn_recipe(self, recipe: CraftingRecipe) -> StardewRule: return self.logic.money.can_spend_at(recipe.source.region, recipe.source.price) if isinstance(recipe.source, SkillSource): return self.logic.skill.has_level(recipe.source.skill, recipe.source.level) + if isinstance(recipe.source, MasterySource): + return self.logic.skill.has_mastery(recipe.source.skill) if isinstance(recipe.source, CutsceneSource): return self.logic.region.can_reach(recipe.source.region) & self.logic.relationship.has_hearts(recipe.source.friend, recipe.source.hearts) if isinstance(recipe.source, FriendshipSource): @@ -81,9 +82,9 @@ def can_learn_recipe(self, recipe: CraftingRecipe) -> StardewRule: if isinstance(recipe.source, QuestSource): return self.logic.quest.can_complete_quest(recipe.source.quest) if isinstance(recipe.source, SpecialOrderSource): - if self.options.special_order_locations == SpecialOrderLocations.option_disabled: - return self.logic.special_order.can_complete_special_order(recipe.source.special_order) - return self.logic.crafting.received_recipe(recipe.item) + if self.options.special_order_locations & SpecialOrderLocations.option_board: + return self.logic.crafting.received_recipe(recipe.item) + return self.logic.special_order.can_complete_special_order(recipe.source.special_order) if isinstance(recipe.source, LogicSource): if recipe.source.logic_rule == "Cellar": return self.logic.region.can_reach(Region.cellar) @@ -99,13 +100,16 @@ def can_craft_everything(self) -> StardewRule: craftsanity_prefix = "Craft " all_recipes_names = [] exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true + exclude_masteries = self.options.skill_progression != SkillProgression.option_progressive_with_masteries for location in locations_by_tag[LocationTags.CRAFTSANITY]: if not location.name.startswith(craftsanity_prefix): continue if exclude_island and LocationTags.GINGER_ISLAND in location.tags: continue + if exclude_masteries and LocationTags.REQUIRES_MASTERIES in location.tags: + continue if location.mod_name and location.mod_name not in self.options.mods: continue all_recipes_names.append(location.name[len(craftsanity_prefix):]) all_recipes = [all_crafting_recipes_by_name[recipe_name] for recipe_name in all_recipes_names] - return And(*(self.logic.crafting.can_craft(recipe) for recipe in all_recipes)) + return self.logic.and_(*(self.logic.crafting.can_craft(recipe) for recipe in all_recipes)) diff --git a/worlds/stardew_valley/logic/crop_logic.py b/worlds/stardew_valley/logic/crop_logic.py deleted file mode 100644 index 8c107ba6a5df..000000000000 --- a/worlds/stardew_valley/logic/crop_logic.py +++ /dev/null @@ -1,72 +0,0 @@ -from typing import Union, Iterable - -from Utils import cache_self1 -from .base_logic import BaseLogicMixin, BaseLogic -from .has_logic import HasLogicMixin -from .money_logic import MoneyLogicMixin -from .received_logic import ReceivedLogicMixin -from .region_logic import RegionLogicMixin -from .season_logic import SeasonLogicMixin -from .tool_logic import ToolLogicMixin -from .traveling_merchant_logic import TravelingMerchantLogicMixin -from ..data import CropItem, SeedItem -from ..options import Cropsanity, ExcludeGingerIsland -from ..stardew_rule import StardewRule, True_, False_ -from ..strings.craftable_names import Craftable -from ..strings.forageable_names import Forageable -from ..strings.machine_names import Machine -from ..strings.metal_names import Fossil -from ..strings.region_names import Region -from ..strings.seed_names import Seed -from ..strings.tool_names import Tool - - -class CropLogicMixin(BaseLogicMixin): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.crop = CropLogic(*args, **kwargs) - - -class CropLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, TravelingMerchantLogicMixin, SeasonLogicMixin, MoneyLogicMixin, - ToolLogicMixin, CropLogicMixin]]): - @cache_self1 - def can_grow(self, crop: CropItem) -> StardewRule: - season_rule = self.logic.season.has_any(crop.farm_growth_seasons) - seed_rule = self.logic.has(crop.seed.name) - farm_rule = self.logic.region.can_reach(Region.farm) & season_rule - tool_rule = self.logic.tool.has_tool(Tool.hoe) & self.logic.tool.has_tool(Tool.watering_can) - region_rule = farm_rule | self.logic.region.can_reach(Region.greenhouse) | self.logic.crop.has_island_farm() - if crop.name == Forageable.cactus_fruit: - region_rule = self.logic.region.can_reach(Region.greenhouse) | self.logic.has(Craftable.garden_pot) - return seed_rule & region_rule & tool_rule - - def can_plant_and_grow_item(self, seasons: Union[str, Iterable[str]]) -> StardewRule: - if isinstance(seasons, str): - seasons = [seasons] - season_rule = self.logic.season.has_any(seasons) | self.logic.region.can_reach(Region.greenhouse) | self.logic.crop.has_island_farm() - farm_rule = self.logic.region.can_reach(Region.farm) | self.logic.region.can_reach(Region.greenhouse) | self.logic.crop.has_island_farm() - return season_rule & farm_rule - - def has_island_farm(self) -> StardewRule: - if self.options.exclude_ginger_island == ExcludeGingerIsland.option_false: - return self.logic.region.can_reach(Region.island_west) - return False_() - - @cache_self1 - def can_buy_seed(self, seed: SeedItem) -> StardewRule: - if seed.requires_island and self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: - return False_() - if self.options.cropsanity == Cropsanity.option_disabled or seed.name == Seed.qi_bean: - item_rule = True_() - else: - item_rule = self.logic.received(seed.name) - if seed.name == Seed.coffee: - item_rule = item_rule & self.logic.traveling_merchant.has_days(3) - season_rule = self.logic.season.has_any(seed.seasons) - region_rule = self.logic.region.can_reach_all(seed.regions) - currency_rule = self.logic.money.can_spend(1000) - if seed.name == Seed.pineapple: - currency_rule = self.logic.has(Forageable.magma_cap) - if seed.name == Seed.taro: - currency_rule = self.logic.has(Fossil.bone_fragment) - return season_rule & region_rule & item_rule & currency_rule diff --git a/worlds/stardew_valley/logic/farming_logic.py b/worlds/stardew_valley/logic/farming_logic.py index b255aa27f785..88523bb85d8e 100644 --- a/worlds/stardew_valley/logic/farming_logic.py +++ b/worlds/stardew_valley/logic/farming_logic.py @@ -1,11 +1,27 @@ -from typing import Union +from functools import cached_property +from typing import Union, Tuple +from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic from .has_logic import HasLogicMixin -from .skill_logic import SkillLogicMixin -from ..stardew_rule import StardewRule, True_, False_ +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from .season_logic import SeasonLogicMixin +from .tool_logic import ToolLogicMixin +from .. import options +from ..stardew_rule import StardewRule, True_, false_ +from ..strings.ap_names.event_names import Event from ..strings.fertilizer_names import Fertilizer -from ..strings.quality_names import CropQuality +from ..strings.region_names import Region +from ..strings.season_names import Season +from ..strings.tool_names import Tool + +farming_event_by_season = { + Season.spring: Event.spring_farming, + Season.summer: Event.summer_farming, + Season.fall: Event.fall_farming, + Season.winter: Event.winter_farming, +} class FarmingLogicMixin(BaseLogicMixin): @@ -14,7 +30,12 @@ def __init__(self, *args, **kwargs): self.farming = FarmingLogic(*args, **kwargs) -class FarmingLogic(BaseLogic[Union[HasLogicMixin, SkillLogicMixin, FarmingLogicMixin]]): +class FarmingLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin, FarmingLogicMixin]]): + + @cached_property + def has_farming_tools(self) -> StardewRule: + return self.logic.tool.has_tool(Tool.hoe) & self.logic.tool.can_water(0) + def has_fertilizer(self, tier: int) -> StardewRule: if tier <= 0: return True_() @@ -25,17 +46,17 @@ def has_fertilizer(self, tier: int) -> StardewRule: if tier >= 3: return self.logic.has(Fertilizer.deluxe) - def can_grow_crop_quality(self, quality: str) -> StardewRule: - if quality == CropQuality.basic: - return True_() - if quality == CropQuality.silver: - return self.logic.skill.has_farming_level(5) | (self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(2)) | ( - self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(1)) | self.logic.farming.has_fertilizer(3) - if quality == CropQuality.gold: - return self.logic.skill.has_farming_level(10) | ( - self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(5)) | ( - self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(3)) | ( - self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(2)) - if quality == CropQuality.iridium: - return self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(4) - return False_() + @cache_self1 + def can_plant_and_grow_item(self, seasons: Union[str, Tuple[str]]) -> StardewRule: + if seasons == (): # indoor farming + return (self.logic.region.can_reach(Region.greenhouse) | self.logic.farming.has_island_farm()) & self.logic.farming.has_farming_tools + + if isinstance(seasons, str): + seasons = (seasons,) + + return self.logic.or_(*(self.logic.received(farming_event_by_season[season]) for season in seasons)) + + def has_island_farm(self) -> StardewRule: + if self.options.exclude_ginger_island == options.ExcludeGingerIsland.option_false: + return self.logic.region.can_reach(Region.island_west) + return false_ diff --git a/worlds/stardew_valley/logic/fishing_logic.py b/worlds/stardew_valley/logic/fishing_logic.py index a7399a65d99c..539385232fd2 100644 --- a/worlds/stardew_valley/logic/fishing_logic.py +++ b/worlds/stardew_valley/logic/fishing_logic.py @@ -1,18 +1,21 @@ -from typing import Union, List +from typing import Union from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic +from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .season_logic import SeasonLogicMixin from .skill_logic import SkillLogicMixin from .tool_logic import ToolLogicMixin -from ..data import FishItem, fish_data -from ..locations import LocationTags, locations_by_tag -from ..options import ExcludeGingerIsland, Fishsanity +from ..data import fish_data +from ..data.fish_data import FishItem +from ..options import ExcludeGingerIsland from ..options import SpecialOrderLocations -from ..stardew_rule import StardewRule, True_, False_, And +from ..stardew_rule import StardewRule, True_, False_ +from ..strings.ap_names.mods.mod_items import SVEQuestItem from ..strings.fish_names import SVEFish +from ..strings.machine_names import Machine from ..strings.quality_names import FishQuality from ..strings.region_names import Region from ..strings.skill_names import Skill @@ -24,17 +27,16 @@ def __init__(self, *args, **kwargs): self.fishing = FishingLogic(*args, **kwargs) -class FishingLogic(BaseLogic[Union[FishingLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin, SkillLogicMixin]]): +class FishingLogic(BaseLogic[Union[HasLogicMixin, FishingLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin, + SkillLogicMixin]]): def can_fish_in_freshwater(self) -> StardewRule: return self.logic.skill.can_fish() & self.logic.region.can_reach_any((Region.forest, Region.town, Region.mountain)) def has_max_fishing(self) -> StardewRule: - skill_rule = self.logic.skill.has_level(Skill.fishing, 10) - return self.logic.tool.has_fishing_rod(4) & skill_rule + return self.logic.tool.has_fishing_rod(4) & self.logic.skill.has_level(Skill.fishing, 10) def can_fish_chests(self) -> StardewRule: - skill_rule = self.logic.skill.has_level(Skill.fishing, 6) - return self.logic.tool.has_fishing_rod(4) & skill_rule + return self.logic.tool.has_fishing_rod(4) & self.logic.skill.has_level(Skill.fishing, 6) def can_fish_at(self, region: str) -> StardewRule: return self.logic.skill.can_fish() & self.logic.region.can_reach(region) @@ -51,17 +53,23 @@ def can_catch_fish(self, fish: FishItem) -> StardewRule: else: difficulty_rule = self.logic.skill.can_fish(difficulty=(120 if fish.legendary else fish.difficulty)) if fish.name == SVEFish.kittyfish: - item_rule = self.logic.received("Kittyfish Spell") + item_rule = self.logic.received(SVEQuestItem.kittyfish_spell) else: item_rule = True_() return quest_rule & region_rule & season_rule & difficulty_rule & item_rule + def can_catch_fish_for_fishsanity(self, fish: FishItem) -> StardewRule: + """ Rule could be different from the basic `can_catch_fish`. Imagine a fishsanity setting where you need to catch every fish with gold quality. + """ + return self.logic.fishing.can_catch_fish(fish) + def can_start_extended_family_quest(self) -> StardewRule: if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: return False_() - if self.options.special_order_locations != SpecialOrderLocations.option_board_qi: + if not self.options.special_order_locations & SpecialOrderLocations.value_qi: return False_() - return self.logic.region.can_reach(Region.qi_walnut_room) & And(*(self.logic.fishing.can_catch_fish(fish) for fish in fish_data.legendary_fish)) + return (self.logic.region.can_reach(Region.qi_walnut_room) & + self.logic.and_(*(self.logic.fishing.can_catch_fish(fish) for fish in fish_data.vanilla_legendary_fish))) def can_catch_quality_fish(self, fish_quality: str) -> StardewRule: if fish_quality == FishQuality.basic: @@ -78,24 +86,27 @@ def can_catch_quality_fish(self, fish_quality: str) -> StardewRule: def can_catch_every_fish(self) -> StardewRule: rules = [self.has_max_fishing()] - exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true - exclude_extended_family = self.options.special_order_locations != SpecialOrderLocations.option_board_qi - for fish in fish_data.get_fish_for_mods(self.options.mods.value): - if exclude_island and fish in fish_data.island_fish: - continue - if exclude_extended_family and fish in fish_data.extended_family: - continue - rules.append(self.logic.fishing.can_catch_fish(fish)) - return And(*rules) - - def can_catch_every_fish_in_slot(self, all_location_names_in_slot: List[str]) -> StardewRule: - if self.options.fishsanity == Fishsanity.option_none: + + rules.extend( + self.logic.fishing.can_catch_fish(fish) + for fish in self.content.fishes.values() + ) + + return self.logic.and_(*rules) + + def can_catch_every_fish_for_fishsanity(self) -> StardewRule: + if not self.content.features.fishsanity.is_enabled: return self.can_catch_every_fish() rules = [self.has_max_fishing()] - for fishsanity_location in locations_by_tag[LocationTags.FISHSANITY]: - if fishsanity_location.name not in all_location_names_in_slot: - continue - rules.append(self.logic.region.can_reach_location(fishsanity_location.name)) - return And(*rules) + rules.extend( + self.logic.fishing.can_catch_fish_for_fishsanity(fish) + for fish in self.content.fishes.values() + if self.content.features.fishsanity.is_included(fish) + ) + + return self.logic.and_(*rules) + + def has_specific_bait(self, fish: FishItem) -> StardewRule: + return self.can_catch_fish(fish) & self.logic.has(Machine.bait_maker) diff --git a/worlds/stardew_valley/logic/grind_logic.py b/worlds/stardew_valley/logic/grind_logic.py new file mode 100644 index 000000000000..ccd8c5daccfb --- /dev/null +++ b/worlds/stardew_valley/logic/grind_logic.py @@ -0,0 +1,74 @@ +from typing import Union, TYPE_CHECKING + +from Utils import cache_self1 +from .base_logic import BaseLogic, BaseLogicMixin +from .book_logic import BookLogicMixin +from .has_logic import HasLogicMixin +from .received_logic import ReceivedLogicMixin +from .time_logic import TimeLogicMixin +from ..options import Booksanity +from ..stardew_rule import StardewRule, HasProgressionPercent +from ..strings.book_names import Book +from ..strings.craftable_names import Consumable +from ..strings.currency_names import Currency +from ..strings.fish_names import WaterChest +from ..strings.geode_names import Geode +from ..strings.tool_names import Tool + +if TYPE_CHECKING: + from .tool_logic import ToolLogicMixin +else: + ToolLogicMixin = object + +MIN_ITEMS = 10 +MAX_ITEMS = 999 +PERCENT_REQUIRED_FOR_MAX_ITEM = 24 + + +class GrindLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.grind = GrindLogic(*args, **kwargs) + + +class GrindLogic(BaseLogic[Union[GrindLogicMixin, HasLogicMixin, ReceivedLogicMixin, BookLogicMixin, TimeLogicMixin, ToolLogicMixin]]): + + def can_grind_mystery_boxes(self, quantity: int) -> StardewRule: + mystery_box_rule = self.logic.has(Consumable.mystery_box) + book_of_mysteries_rule = self.logic.true_ \ + if self.options.booksanity == Booksanity.option_none \ + else self.logic.book.has_book_power(Book.book_of_mysteries) + # Assuming one box per day, but halved because we don't know how many months have passed before Mr. Qi's Plane Ride. + time_rule = self.logic.time.has_lived_months(quantity // 14) + return self.logic.and_(mystery_box_rule, + book_of_mysteries_rule, + time_rule) + + def can_grind_artifact_troves(self, quantity: int) -> StardewRule: + return self.logic.and_(self.logic.has(Geode.artifact_trove), + # Assuming one per month if the player does not grind it. + self.logic.time.has_lived_months(quantity)) + + def can_grind_prize_tickets(self, quantity: int) -> StardewRule: + return self.logic.and_(self.logic.has(Currency.prize_ticket), + # Assuming two per month if the player does not grind it. + self.logic.time.has_lived_months(quantity // 2)) + + def can_grind_fishing_treasure_chests(self, quantity: int) -> StardewRule: + return self.logic.and_(self.logic.has(WaterChest.fishing_chest), + # Assuming one per week if the player does not grind it. + self.logic.time.has_lived_months(quantity // 4)) + + def can_grind_artifact_spots(self, quantity: int) -> StardewRule: + return self.logic.and_(self.logic.tool.has_tool(Tool.hoe), + # Assuming twelve per month if the player does not grind it. + self.logic.time.has_lived_months(quantity // 12)) + + @cache_self1 + def can_grind_item(self, quantity: int) -> StardewRule: + if quantity <= MIN_ITEMS: + return self.logic.true_ + + quantity = min(quantity, MAX_ITEMS) + price = max(1, quantity * PERCENT_REQUIRED_FOR_MAX_ITEM // MAX_ITEMS) + return HasProgressionPercent(self.player, price) diff --git a/worlds/stardew_valley/logic/harvesting_logic.py b/worlds/stardew_valley/logic/harvesting_logic.py new file mode 100644 index 000000000000..3b4d41953ccd --- /dev/null +++ b/worlds/stardew_valley/logic/harvesting_logic.py @@ -0,0 +1,56 @@ +from functools import cached_property +from typing import Union + +from Utils import cache_self1 +from .base_logic import BaseLogicMixin, BaseLogic +from .farming_logic import FarmingLogicMixin +from .has_logic import HasLogicMixin +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from .season_logic import SeasonLogicMixin +from .time_logic import TimeLogicMixin +from .tool_logic import ToolLogicMixin +from ..data.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource +from ..stardew_rule import StardewRule +from ..strings.ap_names.community_upgrade_names import CommunityUpgrade +from ..strings.region_names import Region + + +class HarvestingLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.harvesting = HarvestingLogic(*args, **kwargs) + + +class HarvestingLogic(BaseLogic[Union[HarvestingLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin, +FarmingLogicMixin, TimeLogicMixin]]): + + @cached_property + def can_harvest_from_fruit_bats(self) -> StardewRule: + return self.logic.region.can_reach(Region.farm_cave) & self.logic.received(CommunityUpgrade.fruit_bats) + + @cached_property + def can_harvest_from_mushroom_cave(self) -> StardewRule: + return self.logic.region.can_reach(Region.farm_cave) & self.logic.received(CommunityUpgrade.mushroom_boxes) + + @cache_self1 + def can_forage_from(self, source: ForagingSource) -> StardewRule: + seasons_rule = self.logic.season.has_any(source.seasons) + regions_rule = self.logic.region.can_reach_any(source.regions) + return seasons_rule & regions_rule + + @cache_self1 + def can_harvest_tree_from(self, source: HarvestFruitTreeSource) -> StardewRule: + # FIXME tool not required for this + region_to_grow_rule = self.logic.farming.can_plant_and_grow_item(source.seasons) + sapling_rule = self.logic.has(source.sapling) + # Because it takes 1 month to grow the sapling + time_rule = self.logic.time.has_lived_months(1) + + return region_to_grow_rule & sapling_rule & time_rule + + @cache_self1 + def can_harvest_crop_from(self, source: HarvestCropSource) -> StardewRule: + region_to_grow_rule = self.logic.farming.can_plant_and_grow_item(source.seasons) + seed_rule = self.logic.has(source.seed) + return region_to_grow_rule & seed_rule diff --git a/worlds/stardew_valley/logic/has_logic.py b/worlds/stardew_valley/logic/has_logic.py index d92d4224d7d2..4331780dc01d 100644 --- a/worlds/stardew_valley/logic/has_logic.py +++ b/worlds/stardew_valley/logic/has_logic.py @@ -1,8 +1,11 @@ from .base_logic import BaseLogic -from ..stardew_rule import StardewRule, And, Or, Has, Count +from ..stardew_rule import StardewRule, And, Or, Has, Count, true_, false_ class HasLogicMixin(BaseLogic[None]): + true_ = true_ + false_ = false_ + # Should be cached def has(self, item: str) -> StardewRule: return Has(item, self.registry.item_rules) @@ -10,12 +13,12 @@ def has(self, item: str) -> StardewRule: def has_all(self, *items: str): assert items, "Can't have all of no items." - return And(*(self.has(item) for item in items)) + return self.logic.and_(*(self.has(item) for item in items)) def has_any(self, *items: str): assert items, "Can't have any of no items." - return Or(*(self.has(item) for item in items)) + return self.logic.or_(*(self.has(item) for item in items)) def has_n(self, *items: str, count: int): return self.count(count, *(self.has(item) for item in items)) @@ -24,6 +27,16 @@ def has_n(self, *items: str, count: int): def count(count: int, *rules: StardewRule) -> StardewRule: assert rules, "Can't create a Count conditions without rules" assert len(rules) >= count, "Count need at least as many rules as the count" + assert count > 0, "Count can't be negative" + + count -= sum(r is true_ for r in rules) + rules = list(r for r in rules if r is not true_) + + if count <= 0: + return true_ + + if len(rules) == 1: + return rules[0] if count == 1: return Or(*rules) @@ -31,4 +44,22 @@ def count(count: int, *rules: StardewRule) -> StardewRule: if count == len(rules): return And(*rules) - return Count(list(rules), count) + return Count(rules, count) + + @staticmethod + def and_(*rules: StardewRule) -> StardewRule: + assert rules, "Can't create a And conditions without rules" + + if len(rules) == 1: + return rules[0] + + return And(*rules) + + @staticmethod + def or_(*rules: StardewRule) -> StardewRule: + assert rules, "Can't create a Or conditions without rules" + + if len(rules) == 1: + return rules[0] + + return Or(*rules) diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py index a7fcec922838..74cdaf2374e1 100644 --- a/worlds/stardew_valley/logic/logic.py +++ b/worlds/stardew_valley/logic/logic.py @@ -1,7 +1,8 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import Collection +import logging +from functools import cached_property +from typing import Collection, Callable from .ability_logic import AbilityLogicMixin from .action_logic import ActionLogicMixin @@ -9,50 +10,53 @@ from .arcade_logic import ArcadeLogicMixin from .artisan_logic import ArtisanLogicMixin from .base_logic import LogicRegistry -from .buff_logic import BuffLogicMixin +from .book_logic import BookLogicMixin from .building_logic import BuildingLogicMixin from .bundle_logic import BundleLogicMixin from .combat_logic import CombatLogicMixin from .cooking_logic import CookingLogicMixin from .crafting_logic import CraftingLogicMixin -from .crop_logic import CropLogicMixin from .farming_logic import FarmingLogicMixin from .fishing_logic import FishingLogicMixin from .gift_logic import GiftLogicMixin +from .grind_logic import GrindLogicMixin +from .harvesting_logic import HarvestingLogicMixin from .has_logic import HasLogicMixin +from .logic_event import all_logic_events from .mine_logic import MineLogicMixin from .money_logic import MoneyLogicMixin from .monster_logic import MonsterLogicMixin from .museum_logic import MuseumLogicMixin from .pet_logic import PetLogicMixin +from .quality_logic import QualityLogicMixin from .quest_logic import QuestLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .relationship_logic import RelationshipLogicMixin +from .requirement_logic import RequirementLogicMixin from .season_logic import SeasonLogicMixin from .shipping_logic import ShippingLogicMixin from .skill_logic import SkillLogicMixin +from .source_logic import SourceLogicMixin from .special_order_logic import SpecialOrderLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin from .traveling_merchant_logic import TravelingMerchantLogicMixin from .wallet_logic import WalletLogicMixin -from ..data import all_purchasable_seeds, all_crops +from ..content.game_content import StardewContent from ..data.craftable_data import all_crafting_recipes -from ..data.crops_data import crops_by_name -from ..data.fish_data import get_fish_for_mods from ..data.museum_data import all_museum_items from ..data.recipe_data import all_cooking_recipes from ..mods.logic.magic_logic import MagicLogicMixin from ..mods.logic.mod_logic import ModLogicMixin from ..mods.mod_data import ModNames -from ..options import Cropsanity, SpecialOrderLocations, ExcludeGingerIsland, FestivalLocations, Fishsanity, Friendsanity, StardewValleyOptions -from ..stardew_rule import False_, Or, True_, And, StardewRule +from ..options import SpecialOrderLocations, ExcludeGingerIsland, FestivalLocations, StardewValleyOptions, Walnutsanity +from ..stardew_rule import False_, True_, StardewRule from ..strings.animal_names import Animal from ..strings.animal_product_names import AnimalProduct -from ..strings.ap_names.ap_weapon_names import APWeapon -from ..strings.ap_names.buff_names import Buff +from ..strings.ap_names.ap_option_names import OptionName from ..strings.ap_names.community_upgrade_names import CommunityUpgrade +from ..strings.ap_names.event_names import Event from ..strings.artisan_good_names import ArtisanGood from ..strings.building_names import Building from ..strings.craftable_names import Consumable, Furniture, Ring, Fishing, Lighting, WildSeeds @@ -72,10 +76,10 @@ from ..strings.ingredient_names import Ingredient from ..strings.machine_names import Machine from ..strings.material_names import Material -from ..strings.metal_names import Ore, MetalBar, Mineral, Fossil +from ..strings.metal_names import Ore, MetalBar, Mineral, Fossil, Artifact from ..strings.monster_drop_names import Loot from ..strings.monster_names import Monster -from ..strings.region_names import Region +from ..strings.region_names import Region, LogicRegion from ..strings.season_names import Season from ..strings.seed_names import Seed, TreeSeed from ..strings.skill_names import Skill @@ -83,23 +87,26 @@ from ..strings.villager_names import NPC from ..strings.wallet_item_names import Wallet +logger = logging.getLogger(__name__) -@dataclass(frozen=False, repr=False) -class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogicMixin, TravelingMerchantLogicMixin, TimeLogicMixin, + +class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, TravelingMerchantLogicMixin, TimeLogicMixin, SeasonLogicMixin, MoneyLogicMixin, ActionLogicMixin, ArcadeLogicMixin, ArtisanLogicMixin, GiftLogicMixin, BuildingLogicMixin, ShippingLogicMixin, RelationshipLogicMixin, MuseumLogicMixin, WalletLogicMixin, AnimalLogicMixin, - CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, CropLogicMixin, + CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin, SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin, - SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin): + SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin, + RequirementLogicMixin, BookLogicMixin, GrindLogicMixin): player: int options: StardewValleyOptions + content: StardewContent regions: Collection[str] - def __init__(self, player: int, options: StardewValleyOptions, regions: Collection[str]): + def __init__(self, player: int, options: StardewValleyOptions, content: StardewContent, regions: Collection[str]): self.registry = LogicRegistry() - super().__init__(player, self.registry, options, regions, self) + super().__init__(player, self.registry, options, content, regions, self) - self.registry.fish_rules.update({fish.name: self.fishing.can_catch_fish(fish) for fish in get_fish_for_mods(self.options.mods.value)}) + self.registry.fish_rules.update({fish.name: self.fishing.can_catch_fish(fish) for fish in content.fishes.values()}) self.registry.museum_rules.update({donation.item_name: self.museum.can_find_museum_item(donation) for donation in all_museum_items}) for recipe in all_cooking_recipes: @@ -118,37 +125,7 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti can_craft_rule = can_craft_rule | self.registry.crafting_rules[recipe.item] self.registry.crafting_rules[recipe.item] = can_craft_rule - self.registry.sapling_rules.update({ - Sapling.apple: self.can_buy_sapling(Fruit.apple), - Sapling.apricot: self.can_buy_sapling(Fruit.apricot), - Sapling.cherry: self.can_buy_sapling(Fruit.cherry), - Sapling.orange: self.can_buy_sapling(Fruit.orange), - Sapling.peach: self.can_buy_sapling(Fruit.peach), - Sapling.pomegranate: self.can_buy_sapling(Fruit.pomegranate), - Sapling.banana: self.can_buy_sapling(Fruit.banana), - Sapling.mango: self.can_buy_sapling(Fruit.mango), - }) - - self.registry.tree_fruit_rules.update({ - Fruit.apple: self.crop.can_plant_and_grow_item(Season.fall), - Fruit.apricot: self.crop.can_plant_and_grow_item(Season.spring), - Fruit.cherry: self.crop.can_plant_and_grow_item(Season.spring), - Fruit.orange: self.crop.can_plant_and_grow_item(Season.summer), - Fruit.peach: self.crop.can_plant_and_grow_item(Season.summer), - Fruit.pomegranate: self.crop.can_plant_and_grow_item(Season.fall), - Fruit.banana: self.crop.can_plant_and_grow_item(Season.summer), - Fruit.mango: self.crop.can_plant_and_grow_item(Season.summer), - }) - - for tree_fruit in self.registry.tree_fruit_rules: - existing_rules = self.registry.tree_fruit_rules[tree_fruit] - sapling = f"{tree_fruit} Sapling" - self.registry.tree_fruit_rules[tree_fruit] = existing_rules & self.has(sapling) & self.time.has_lived_months(1) - - self.registry.seed_rules.update({seed.name: self.crop.can_buy_seed(seed) for seed in all_purchasable_seeds}) - self.registry.crop_rules.update({crop.name: self.crop.can_grow(crop) for crop in all_crops}) self.registry.crop_rules.update({ - Seed.coffee: (self.season.has(Season.spring) | self.season.has(Season.summer)) & self.crop.can_buy_seed(crops_by_name[Seed.coffee].seed), Fruit.ancient_fruit: (self.received("Ancient Seeds") | self.received("Ancient Seeds Recipe")) & self.region.can_reach(Region.greenhouse) & self.has(Machine.seed_maker), }) @@ -157,6 +134,7 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti self.registry.item_rules.update({ "Energy Tonic": self.money.can_spend_at(Region.hospital, 1000), WaterChest.fishing_chest: self.fishing.can_fish_chests(), + WaterChest.golden_fishing_chest: self.fishing.can_fish_chests() & self.skill.has_mastery(Skill.fishing), WaterChest.treasure: self.fishing.can_fish_chests(), Ring.hot_java_ring: self.region.can_reach(Region.volcano_floor_10), "Galaxy Soul": self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 40), @@ -197,7 +175,7 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti AnimalProduct.large_goat_milk: self.animal.has_happy_animal(Animal.goat), AnimalProduct.large_milk: self.animal.has_happy_animal(Animal.cow), AnimalProduct.milk: self.animal.has_animal(Animal.cow), - AnimalProduct.ostrich_egg: self.tool.can_forage(Generic.any, Region.island_north, True), + AnimalProduct.ostrich_egg: self.tool.can_forage(Generic.any, Region.island_north, True) & self.has(Forageable.journal_scrap) & self.region.can_reach(Region.volcano_floor_5), AnimalProduct.rabbit_foot: self.animal.has_happy_animal(Animal.rabbit), AnimalProduct.roe: self.skill.can_fish() & self.building.has_building(Building.fish_pond), AnimalProduct.squid_ink: self.mine.can_mine_in_the_mines_floor_81_120() | (self.building.has_building(Building.fish_pond) & self.has(Fish.squid)), @@ -218,29 +196,35 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti ArtisanGood.dinosaur_mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.dinosaur_egg), ArtisanGood.duck_mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.duck_egg), ArtisanGood.goat_cheese: self.has(AnimalProduct.goat_milk) & self.has(Machine.cheese_press), - ArtisanGood.green_tea: self.artisan.can_keg(Vegetable.tea_leaves), ArtisanGood.honey: self.money.can_spend_at(Region.oasis, 200) | (self.has(Machine.bee_house) & self.season.has_any_not_winter()), - ArtisanGood.jelly: self.artisan.has_jelly(), - ArtisanGood.juice: self.artisan.has_juice(), ArtisanGood.maple_syrup: self.has(Machine.tapper), ArtisanGood.mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.chicken_egg), - ArtisanGood.mead: self.artisan.can_keg(ArtisanGood.honey), + ArtisanGood.mystic_syrup: self.has(Machine.tapper) & self.has(TreeSeed.mystic), ArtisanGood.oak_resin: self.has(Machine.tapper), - ArtisanGood.pale_ale: self.artisan.can_keg(Vegetable.hops), - ArtisanGood.pickles: self.artisan.has_pickle(), ArtisanGood.pine_tar: self.has(Machine.tapper), + ArtisanGood.smoked_fish: self.artisan.has_smoked_fish(), + ArtisanGood.targeted_bait: self.artisan.has_targeted_bait(), + ArtisanGood.stardrop_tea: self.has(WaterChest.golden_fishing_chest), ArtisanGood.truffle_oil: self.has(AnimalProduct.truffle) & self.has(Machine.oil_maker), ArtisanGood.void_mayonnaise: (self.skill.can_fish(Region.witch_swamp)) | (self.artisan.can_mayonnaise(AnimalProduct.void_egg)), - ArtisanGood.wine: self.artisan.has_wine(), - Beverage.beer: self.artisan.can_keg(Vegetable.wheat) | self.money.can_spend_at(Region.saloon, 400), - Beverage.coffee: self.artisan.can_keg(Seed.coffee) | self.has(Machine.coffee_maker) | (self.money.can_spend_at(Region.saloon, 300)) | self.has("Hot Java Ring"), Beverage.pina_colada: self.money.can_spend_at(Region.island_resort, 600), Beverage.triple_shot_espresso: self.has("Hot Java Ring"), + Consumable.butterfly_powder: self.money.can_spend_at(Region.sewer, 20000), + Consumable.far_away_stone: self.region.can_reach(Region.mines_floor_100) & self.has(Artifact.ancient_doll), + Consumable.fireworks_red: self.region.can_reach(Region.casino), + Consumable.fireworks_purple: self.region.can_reach(Region.casino), + Consumable.fireworks_green: self.region.can_reach(Region.casino), + Consumable.golden_animal_cracker: self.skill.has_mastery(Skill.farming), + Consumable.mystery_box: self.received(CommunityUpgrade.mr_qi_plane_ride), + Consumable.gold_mystery_box: self.received(CommunityUpgrade.mr_qi_plane_ride) & self.skill.has_mastery(Skill.foraging), + Currency.calico_egg: self.region.can_reach(LogicRegion.desert_festival), + Currency.golden_tag: self.region.can_reach(LogicRegion.trout_derby), + Currency.prize_ticket: self.time.has_lived_months(2), # Time to do a few help wanted quests Decoration.rotten_plant: self.has(Lighting.jack_o_lantern) & self.season.has(Season.winter), Fertilizer.basic: self.money.can_spend_at(Region.pierre_store, 100), Fertilizer.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150), Fertilizer.tree: self.skill.has_level(Skill.foraging, 7) & self.has(Material.fiber) & self.has(Material.stone), - Fish.any: Or(*(self.fishing.can_catch_fish(fish) for fish in get_fish_for_mods(self.options.mods.value))), + Fish.any: self.logic.or_(*(self.fishing.can_catch_fish(fish) for fish in content.fishes.values())), Fish.crab: self.skill.can_crab_pot_at(Region.beach), Fish.crayfish: self.skill.can_crab_pot_at(Region.town), Fish.lobster: self.skill.can_crab_pot_at(Region.beach), @@ -252,44 +236,15 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti Fish.snail: self.skill.can_crab_pot_at(Region.town), Fishing.curiosity_lure: self.monster.can_kill(self.monster.all_monsters_by_name[Monster.mummy]), Fishing.lead_bobber: self.skill.has_level(Skill.fishing, 6) & self.money.can_spend_at(Region.fish_shop, 200), - Forageable.blackberry: self.tool.can_forage(Season.fall) | self.has_fruit_bats(), - Forageable.cactus_fruit: self.tool.can_forage(Generic.any, Region.desert), - Forageable.cave_carrot: self.tool.can_forage(Generic.any, Region.mines_floor_10, True), - Forageable.chanterelle: self.tool.can_forage(Season.fall, Region.secret_woods) | self.has_mushroom_cave(), - Forageable.coconut: self.tool.can_forage(Generic.any, Region.desert), - Forageable.common_mushroom: self.tool.can_forage(Season.fall) | (self.tool.can_forage(Season.spring, Region.secret_woods)) | self.has_mushroom_cave(), - Forageable.crocus: self.tool.can_forage(Season.winter), - Forageable.crystal_fruit: self.tool.can_forage(Season.winter), - Forageable.daffodil: self.tool.can_forage(Season.spring), - Forageable.dandelion: self.tool.can_forage(Season.spring), - Forageable.dragon_tooth: self.tool.can_forage(Generic.any, Region.volcano_floor_10), - Forageable.fiddlehead_fern: self.tool.can_forage(Season.summer, Region.secret_woods), - Forageable.ginger: self.tool.can_forage(Generic.any, Region.island_west, True), - Forageable.hay: self.building.has_building(Building.silo) & self.tool.has_tool(Tool.scythe), - Forageable.hazelnut: self.tool.can_forage(Season.fall), - Forageable.holly: self.tool.can_forage(Season.winter), - Forageable.journal_scrap: self.region.can_reach_all((Region.island_west, Region.island_north, Region.island_south, Region.volcano_floor_10)) & self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()), - Forageable.leek: self.tool.can_forage(Season.spring), - Forageable.magma_cap: self.tool.can_forage(Generic.any, Region.volcano_floor_5), - Forageable.morel: self.tool.can_forage(Season.spring, Region.secret_woods) | self.has_mushroom_cave(), - Forageable.purple_mushroom: self.tool.can_forage(Generic.any, Region.mines_floor_95) | self.tool.can_forage(Generic.any, Region.skull_cavern_25) | self.has_mushroom_cave(), - Forageable.rainbow_shell: self.tool.can_forage(Season.summer, Region.beach), - Forageable.red_mushroom: self.tool.can_forage(Season.summer, Region.secret_woods) | self.tool.can_forage(Season.fall, Region.secret_woods) | self.has_mushroom_cave(), - Forageable.salmonberry: self.tool.can_forage(Season.spring) | self.has_fruit_bats(), - Forageable.secret_note: self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()), - Forageable.snow_yam: self.tool.can_forage(Season.winter, Region.beach, True), - Forageable.spice_berry: self.tool.can_forage(Season.summer) | self.has_fruit_bats(), - Forageable.spring_onion: self.tool.can_forage(Season.spring), - Forageable.sweet_pea: self.tool.can_forage(Season.summer), - Forageable.wild_horseradish: self.tool.can_forage(Season.spring), - Forageable.wild_plum: self.tool.can_forage(Season.fall) | self.has_fruit_bats(), - Forageable.winter_root: self.tool.can_forage(Season.winter, Region.forest, True), + Forageable.hay: self.building.has_building(Building.silo) & self.tool.has_tool(Tool.scythe), # + Forageable.journal_scrap: self.region.can_reach_all((Region.island_west, Region.island_north, Region.island_south, Region.volcano_floor_10)) & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()),# + Forageable.secret_note: self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()), # Fossil.bone_fragment: (self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe)) | self.monster.can_kill(Monster.skeleton), Fossil.fossilized_leg: self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe), Fossil.fossilized_ribs: self.region.can_reach(Region.island_south) & self.tool.has_tool(Tool.hoe), Fossil.fossilized_skull: self.action.can_open_geode(Geode.golden_coconut), Fossil.fossilized_spine: self.skill.can_fish(Region.dig_site), - Fossil.fossilized_tail: self.action.can_pan_at(Region.dig_site), + Fossil.fossilized_tail: self.action.can_pan_at(Region.dig_site, ToolMaterial.copper), Fossil.mummified_bat: self.region.can_reach(Region.volcano_floor_10), Fossil.mummified_frog: self.region.can_reach(Region.island_east) & self.tool.has_tool(Tool.scythe), Fossil.snake_skull: self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.hoe), @@ -299,10 +254,10 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti Geode.geode: self.mine.can_mine_in_the_mines_floor_1_40(), Geode.golden_coconut: self.region.can_reach(Region.island_north), Geode.magma: self.mine.can_mine_in_the_mines_floor_81_120() | (self.has(Fish.lava_eel) & self.building.has_building(Building.fish_pond)), - Geode.omni: self.mine.can_mine_in_the_mines_floor_41_80() | self.region.can_reach(Region.desert) | self.action.can_pan() | 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(Generic.bachelor, 8) & self.money.can_spend_at(Region.pierre_store, 100), + Geode.omni: self.mine.can_mine_in_the_mines_floor_41_80() | self.region.can_reach(Region.desert) | self.tool.has_tool(Tool.pan, ToolMaterial.iron) | self.received(Wallet.rusty_key) | (self.has(Fish.octopus) & self.building.has_building(Building.fish_pond)) | self.region.can_reach(Region.volcano_floor_10), + Gift.bouquet: self.relationship.has_hearts_with_any_bachelor(8) & self.money.can_spend_at(Region.pierre_store, 100), Gift.golden_pumpkin: self.season.has(Season.fall) | self.action.can_open_geode(Geode.artifact_trove), - Gift.mermaid_pendant: self.region.can_reach(Region.tide_pools) & self.relationship.has_hearts(Generic.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_house(1) & self.has(Consumable.rain_totem), Gift.movie_ticket: self.money.can_spend_at(Region.movie_ticket_stand, 1000), Gift.pearl: (self.has(Fish.blobfish) & self.building.has_building(Building.fish_pond)) | self.action.can_open_geode(Geode.artifact_trove), Gift.tea_set: self.season.has(Season.winter) & self.time.has_lived_max_months, @@ -312,45 +267,27 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti Ingredient.qi_seasoning: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 10), Ingredient.rice: self.money.can_spend_at(Region.pierre_store, 200) | (self.building.has_building(Building.mill) & self.has(Vegetable.unmilled_rice)), Ingredient.sugar: self.money.can_spend_at(Region.pierre_store, 100) | (self.building.has_building(Building.mill) & self.has(Vegetable.beet)), - Ingredient.vinegar: self.money.can_spend_at(Region.pierre_store, 200), + Ingredient.vinegar: self.money.can_spend_at(Region.pierre_store, 200) | self.artisan.can_keg(Ingredient.rice), Ingredient.wheat_flour: self.money.can_spend_at(Region.pierre_store, 100) | (self.building.has_building(Building.mill) & self.has(Vegetable.wheat)), Loot.bat_wing: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern(), Loot.bug_meat: self.mine.can_mine_in_the_mines_floor_1_40(), Loot.slime: self.mine.can_mine_in_the_mines_floor_1_40(), Loot.solar_essence: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern(), Loot.void_essence: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern(), - Machine.bee_house: self.skill.has_farming_level(3) & self.has(MetalBar.iron) & self.has(ArtisanGood.maple_syrup) & self.has(Material.coal) & self.has(Material.wood), - Machine.cask: self.building.has_house(3) & self.region.can_reach(Region.cellar) & self.has(Material.wood) & self.has(Material.hardwood), - Machine.cheese_press: self.skill.has_farming_level(6) & self.has(Material.wood) & self.has(Material.stone) & self.has(Material.hardwood) & self.has(MetalBar.copper), Machine.coffee_maker: self.received(Machine.coffee_maker), - Machine.crab_pot: self.skill.has_level(Skill.fishing, 3) & (self.money.can_spend_at(Region.fish_shop, 1500) | (self.has(MetalBar.iron) & self.has(Material.wood))), - Machine.furnace: self.has(Material.stone) & self.has(Ore.copper), - Machine.keg: self.skill.has_farming_level(8) & self.has(Material.wood) & self.has(MetalBar.iron) & self.has(MetalBar.copper) & self.has(ArtisanGood.oak_resin), - Machine.lightning_rod: self.skill.has_level(Skill.foraging, 6) & self.has(MetalBar.iron) & self.has(MetalBar.quartz) & self.has(Loot.bat_wing), - Machine.loom: self.skill.has_farming_level(7) & self.has(Material.wood) & self.has(Material.fiber) & self.has(ArtisanGood.pine_tar), - Machine.mayonnaise_machine: self.skill.has_farming_level(2) & self.has(Material.wood) & self.has(Material.stone) & self.has("Earth Crystal") & self.has(MetalBar.copper), - Machine.ostrich_incubator: self.received("Ostrich Incubator Recipe") & self.has(Fossil.bone_fragment) & self.has(Material.hardwood) & self.has(Material.cinder_shard), - Machine.preserves_jar: self.skill.has_farming_level(4) & self.has(Material.wood) & self.has(Material.stone) & self.has(Material.coal), - Machine.recycling_machine: self.skill.has_level(Skill.fishing, 4) & self.has(Material.wood) & self.has(Material.stone) & self.has(MetalBar.iron), - Machine.seed_maker: self.skill.has_farming_level(9) & self.has(Material.wood) & self.has(MetalBar.gold) & self.has(Material.coal), - Machine.solar_panel: self.received("Solar Panel Recipe") & self.has(MetalBar.quartz) & self.has(MetalBar.iron) & self.has(MetalBar.gold), - Machine.tapper: self.skill.has_level(Skill.foraging, 3) & self.has(Material.wood) & self.has(MetalBar.copper), - Machine.worm_bin: self.skill.has_level(Skill.fishing, 8) & self.has(Material.hardwood) & self.has(MetalBar.gold) & self.has(MetalBar.iron) & self.has(Material.fiber), + Machine.crab_pot: self.skill.has_level(Skill.fishing, 3) & self.money.can_spend_at(Region.fish_shop, 1500), Machine.enricher: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 20), Machine.pressure_nozzle: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 20), Material.cinder_shard: self.region.can_reach(Region.volcano_floor_5), Material.clay: self.region.can_reach_any((Region.farm, Region.beach, Region.quarry)) & self.tool.has_tool(Tool.hoe), - Material.coal: self.mine.can_mine_in_the_mines_floor_41_80() | self.action.can_pan(), + Material.coal: self.mine.can_mine_in_the_mines_floor_41_80() | self.tool.has_tool(Tool.pan), Material.fiber: True_(), Material.hardwood: self.tool.has_tool(Tool.axe, ToolMaterial.copper) & (self.region.can_reach(Region.secret_woods) | self.region.can_reach(Region.island_west)), + Material.moss: True_(), Material.sap: self.ability.can_chop_trees(), Material.stone: self.tool.has_tool(Tool.pickaxe), Material.wood: self.tool.has_tool(Tool.axe), - Meal.bread: self.money.can_spend_at(Region.saloon, 120), Meal.ice_cream: (self.season.has(Season.summer) & self.money.can_spend_at(Region.town, 250)) | self.money.can_spend_at(Region.oasis, 240), - Meal.pizza: self.money.can_spend_at(Region.saloon, 600), - Meal.salad: self.money.can_spend_at(Region.saloon, 220), - Meal.spaghetti: self.money.can_spend_at(Region.saloon, 240), Meal.strange_bun: self.relationship.has_hearts(NPC.shane, 7) & self.has(Ingredient.wheat_flour) & self.has(Fish.periwinkle) & self.has(ArtisanGood.void_mayonnaise), MetalBar.copper: self.can_smelt(Ore.copper), MetalBar.gold: self.can_smelt(Ore.gold), @@ -358,15 +295,14 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti MetalBar.iron: self.can_smelt(Ore.iron), MetalBar.quartz: self.can_smelt(Mineral.quartz) | self.can_smelt("Fire Quartz") | (self.has(Machine.recycling_machine) & (self.has(Trash.broken_cd) | self.has(Trash.broken_glasses))), MetalBar.radioactive: self.can_smelt(Ore.radioactive), - Ore.copper: self.mine.can_mine_in_the_mines_floor_1_40() | self.mine.can_mine_in_the_skull_cavern() | self.action.can_pan(), - Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.action.can_pan(), - Ore.iridium: self.mine.can_mine_in_the_skull_cavern() | self.can_fish_pond(Fish.super_cucumber), - Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.action.can_pan(), + Ore.copper: self.mine.can_mine_in_the_mines_floor_1_40() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper), + Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.iron), + Ore.iridium: self.mine.can_mine_in_the_skull_cavern() | self.can_fish_pond(Fish.super_cucumber) | self.tool.has_tool(Tool.pan, ToolMaterial.gold), + Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper), Ore.radioactive: self.ability.can_mine_perfectly() & self.region.can_reach(Region.qi_walnut_room), RetainingSoil.basic: self.money.can_spend_at(Region.pierre_store, 100), RetainingSoil.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150), Sapling.tea: self.relationship.has_hearts(NPC.caroline, 2) & self.has(Material.fiber) & self.has(Material.wood), - Seed.mixed: self.tool.has_tool(Tool.scythe) & self.region.can_reach_all((Region.farm, Region.forest, Region.town)), SpeedGro.basic: self.money.can_spend_at(Region.pierre_store, 100), SpeedGro.deluxe: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150), Trash.broken_cd: self.skill.can_crab_pot, @@ -380,24 +316,33 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti TreeSeed.maple: self.skill.has_level(Skill.foraging, 1) & self.ability.can_chop_trees(), TreeSeed.mushroom: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 5), TreeSeed.pine: self.skill.has_level(Skill.foraging, 1) & self.ability.can_chop_trees(), - Vegetable.tea_leaves: self.has(Sapling.tea) & self.time.has_lived_months(2) & self.season.has_any_not_winter(), + TreeSeed.mossy: self.ability.can_chop_trees() & self.season.has(Season.summer), Fish.clam: self.tool.can_forage(Generic.any, Region.beach), Fish.cockle: self.tool.can_forage(Generic.any, Region.beach), - WaterItem.coral: self.tool.can_forage(Generic.any, Region.tide_pools) | self.tool.can_forage(Season.summer, Region.beach), WaterItem.green_algae: self.fishing.can_fish_in_freshwater(), - WaterItem.nautilus_shell: self.tool.can_forage(Season.winter, Region.beach), - WaterItem.sea_urchin: self.tool.can_forage(Generic.any, Region.tide_pools), + WaterItem.cave_jelly: self.fishing.can_fish_at(Region.mines_floor_100) & self.tool.has_fishing_rod(2), + WaterItem.river_jelly: self.fishing.can_fish_at(Region.town) & self.tool.has_fishing_rod(2), + WaterItem.sea_jelly: self.fishing.can_fish_at(Region.beach) & self.tool.has_fishing_rod(2), WaterItem.seaweed: self.skill.can_fish(Region.tide_pools), WaterItem.white_algae: self.skill.can_fish(Region.mines_floor_20), WildSeeds.grass_starter: self.money.can_spend_at(Region.pierre_store, 100), }) # @formatter:on + + content_rules = { + item_name: self.source.has_access_to_item(game_item) + for item_name, game_item in self.content.game_items.items() + } + + for item in set(content_rules.keys()).intersection(self.registry.item_rules.keys()): + logger.warning(f"Rule for {item} already exists in the registry, overwriting it.") + + self.registry.item_rules.update(content_rules) self.registry.item_rules.update(self.registry.fish_rules) self.registry.item_rules.update(self.registry.museum_rules) - self.registry.item_rules.update(self.registry.sapling_rules) - self.registry.item_rules.update(self.registry.tree_fruit_rules) - self.registry.item_rules.update(self.registry.seed_rules) self.registry.item_rules.update(self.registry.crop_rules) + self.artisan.initialize_rules() + self.registry.item_rules.update(self.registry.artisan_good_rules) self.registry.item_rules.update(self.mod.item.get_modded_item_rules()) self.mod.item.modify_vanilla_item_rules_with_mod_additions(self.registry.item_rules) # New regions and content means new ways to obtain old items @@ -423,7 +368,7 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti self.registry.festival_rules.update({ FestivalCheck.egg_hunt: self.can_win_egg_hunt(), FestivalCheck.strawberry_seeds: self.money.can_spend(1000), - FestivalCheck.dance: self.relationship.has_hearts(Generic.bachelor, 4), + FestivalCheck.dance: self.relationship.has_hearts_with_any_bachelor(4), FestivalCheck.tub_o_flowers: self.money.can_spend(2000), FestivalCheck.rarecrow_5: self.money.can_spend(2500), FestivalCheck.luau_soup: self.can_succeed_luau_soup(), @@ -457,43 +402,90 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti FestivalCheck.legend_of_the_winter_star: True_(), FestivalCheck.rarecrow_3: True_(), FestivalCheck.all_rarecrows: self.region.can_reach(Region.farm) & self.has_all_rarecrows(), + FestivalCheck.calico_race: True_(), + FestivalCheck.mummy_mask: True_(), + FestivalCheck.calico_statue: True_(), + FestivalCheck.emily_outfit_service: True_(), + FestivalCheck.earthy_mousse: True_(), + FestivalCheck.sweet_bean_cake: True_(), + FestivalCheck.skull_cave_casserole: True_(), + FestivalCheck.spicy_tacos: True_(), + FestivalCheck.mountain_chili: True_(), + FestivalCheck.crystal_cake: True_(), + FestivalCheck.cave_kebab: True_(), + FestivalCheck.hot_log: True_(), + FestivalCheck.sour_salad: True_(), + FestivalCheck.superfood_cake: True_(), + FestivalCheck.warrior_smoothie: True_(), + FestivalCheck.rumpled_fruit_skin: True_(), + FestivalCheck.calico_pizza: True_(), + FestivalCheck.stuffed_mushrooms: True_(), + FestivalCheck.elf_quesadilla: True_(), + FestivalCheck.nachos_of_the_desert: True_(), + FestivalCheck.cloppino: True_(), + FestivalCheck.rainforest_shrimp: True_(), + FestivalCheck.shrimp_donut: True_(), + FestivalCheck.smell_of_the_sea: True_(), + FestivalCheck.desert_gumbo: True_(), + FestivalCheck.free_cactis: True_(), + FestivalCheck.monster_hunt: self.monster.can_kill(Monster.serpent), + FestivalCheck.deep_dive: self.region.can_reach(Region.skull_cavern_50), + FestivalCheck.treasure_hunt: self.region.can_reach(Region.skull_cavern_25), + FestivalCheck.touch_calico_statue: self.region.can_reach(Region.skull_cavern_25), + FestivalCheck.real_calico_egg_hunter: self.region.can_reach(Region.skull_cavern_100), + FestivalCheck.willy_challenge: self.fishing.can_catch_fish(content.fishes[Fish.scorpion_carp]), + FestivalCheck.desert_scholar: True_(), + FestivalCheck.squidfest_day_1_copper: self.fishing.can_catch_fish(content.fishes[Fish.squid]), + FestivalCheck.squidfest_day_1_iron: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.bait), + FestivalCheck.squidfest_day_1_gold: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.deluxe_bait), + FestivalCheck.squidfest_day_1_iridium: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & + self.fishing.has_specific_bait(content.fishes[Fish.squid]), + FestivalCheck.squidfest_day_2_copper: self.fishing.can_catch_fish(content.fishes[Fish.squid]), + FestivalCheck.squidfest_day_2_iron: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.bait), + FestivalCheck.squidfest_day_2_gold: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.deluxe_bait), + FestivalCheck.squidfest_day_2_iridium: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & + self.fishing.has_specific_bait(content.fishes[Fish.squid]), }) + for i in range(1, 11): + self.registry.festival_rules[f"{FestivalCheck.trout_derby_reward_pattern}{i}"] = self.fishing.can_catch_fish(content.fishes[Fish.rainbow_trout]) self.special_order.initialize_rules() self.special_order.update_rules(self.mod.special_order.get_modded_special_orders_rules()) - def can_buy_sapling(self, fruit: str) -> StardewRule: - sapling_prices = {Fruit.apple: 4000, Fruit.apricot: 2000, Fruit.cherry: 3400, Fruit.orange: 4000, - Fruit.peach: 6000, - Fruit.pomegranate: 6000, Fruit.banana: 0, Fruit.mango: 0} - received_sapling = self.received(f"{fruit} Sapling") - if self.options.cropsanity == Cropsanity.option_disabled: - allowed_buy_sapling = True_() - else: - allowed_buy_sapling = received_sapling - can_buy_sapling = self.money.can_spend_at(Region.pierre_store, sapling_prices[fruit]) - if fruit == Fruit.banana: - can_buy_sapling = self.has_island_trader() & self.has(Forageable.dragon_tooth) - elif fruit == Fruit.mango: - can_buy_sapling = self.has_island_trader() & self.has(Fish.mussel_node) - - return allowed_buy_sapling & can_buy_sapling + def setup_events(self, register_event: Callable[[str, str, StardewRule], None]) -> None: + for logic_event in all_logic_events: + rule = self.registry.item_rules[logic_event.item] + register_event(logic_event.name, logic_event.region, rule) + self.registry.item_rules[logic_event.item] = self.received(logic_event.name) def can_smelt(self, item: str) -> StardewRule: return self.has(Machine.furnace) & self.has(item) - def can_complete_field_office(self) -> StardewRule: + @cached_property + def can_start_field_office(self) -> StardewRule: field_office = self.region.can_reach(Region.field_office) professor_snail = self.received("Open Professor Snail Cave") - tools = self.tool.has_tool(Tool.pickaxe) & self.tool.has_tool(Tool.hoe) & self.tool.has_tool(Tool.scythe) - leg_and_snake_skull = self.has_all(Fossil.fossilized_leg, Fossil.snake_skull) - ribs_and_spine = self.has_all(Fossil.fossilized_ribs, Fossil.fossilized_spine) - skull = self.has(Fossil.fossilized_skull) - tail = self.has(Fossil.fossilized_tail) - frog = self.has(Fossil.mummified_frog) - bat = self.has(Fossil.mummified_bat) - snake_vertebrae = self.has(Fossil.snake_vertebrae) - return field_office & professor_snail & tools & leg_and_snake_skull & ribs_and_spine & skull & tail & frog & bat & snake_vertebrae + return field_office & professor_snail + + def can_complete_large_animal_collection(self) -> StardewRule: + fossils = self.has_all(Fossil.fossilized_leg, Fossil.fossilized_ribs, Fossil.fossilized_skull, Fossil.fossilized_spine, Fossil.fossilized_tail) + return self.can_start_field_office & fossils + + def can_complete_snake_collection(self) -> StardewRule: + fossils = self.has_all(Fossil.snake_skull, Fossil.snake_vertebrae) + return self.can_start_field_office & fossils + + def can_complete_frog_collection(self) -> StardewRule: + fossils = self.has_all(Fossil.mummified_frog) + return self.can_start_field_office & fossils + + def can_complete_bat_collection(self) -> StardewRule: + fossils = self.has_all(Fossil.mummified_bat) + return self.can_start_field_office & fossils + + def can_complete_field_office(self) -> StardewRule: + return self.can_complete_large_animal_collection() & self.can_complete_snake_collection() & \ + self.can_complete_frog_collection() & self.can_complete_bat_collection() def can_finish_grandpa_evaluation(self) -> StardewRule: # https://stardewvalleywiki.com/Grandpa @@ -511,9 +503,9 @@ def can_finish_grandpa_evaluation(self) -> StardewRule: # Catching every fish not expected # Shipping every item not expected self.relationship.can_get_married() & self.building.has_house(2), - self.relationship.has_hearts("5", 8), # 5 Friends - self.relationship.has_hearts("10", 8), # 10 friends - self.pet.has_hearts(5), # Max Pet + self.relationship.has_hearts_with_n(5, 8), # 5 Friends + self.relationship.has_hearts_with_n(10, 8), # 10 friends + self.pet.has_pet_hearts(5), # Max Pet self.bundle.can_complete_community_center, # Community Center Completion self.bundle.can_complete_community_center, # CC Ceremony first point self.bundle.can_complete_community_center, # CC Ceremony second point @@ -523,41 +515,44 @@ def can_finish_grandpa_evaluation(self) -> StardewRule: return self.count(12, *rules_worth_a_point) def can_win_egg_hunt(self) -> StardewRule: - number_of_movement_buffs = self.options.movement_buff_number - if self.options.festival_locations == FestivalLocations.option_hard or number_of_movement_buffs < 2: - return True_() - return self.received(Buff.movement, number_of_movement_buffs // 2) + return True_() def can_succeed_luau_soup(self) -> StardewRule: if self.options.festival_locations != FestivalLocations.option_hard: return True_() - eligible_fish = [Fish.blobfish, Fish.crimsonfish, "Ice Pip", Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish, Fish.mutant_carp, - Fish.spookfish, Fish.stingray, Fish.sturgeon, "Super Cucumber"] - fish_rule = self.has_any(*eligible_fish) - eligible_kegables = [Fruit.ancient_fruit, Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.melon, + eligible_fish = (Fish.blobfish, Fish.crimsonfish, Fish.ice_pip, Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish, + Fish.mutant_carp, Fish.spookfish, Fish.stingray, Fish.sturgeon, Fish.super_cucumber) + fish_rule = self.has_any(*(f for f in eligible_fish if f in self.content.fishes)) # To filter stingray + eligible_kegables = (Fruit.ancient_fruit, Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.melon, Fruit.orange, Fruit.peach, Fruit.pineapple, Fruit.pomegranate, Fruit.rhubarb, Fruit.starfruit, Fruit.strawberry, Forageable.cactus_fruit, Fruit.cherry, Fruit.cranberries, Fruit.grape, Forageable.spice_berry, Forageable.wild_plum, - Vegetable.hops, Vegetable.wheat] - keg_rules = [self.artisan.can_keg(kegable) for kegable in eligible_kegables] - aged_rule = self.has(Machine.cask) & Or(*keg_rules) + Vegetable.hops, Vegetable.wheat) + keg_rules = [self.artisan.can_keg(kegable) for kegable in eligible_kegables if kegable in self.content.game_items] + aged_rule = self.has(Machine.cask) & self.logic.or_(*keg_rules) # There are a few other valid items, but I don't feel like coding them all return fish_rule | aged_rule def can_succeed_grange_display(self) -> StardewRule: if self.options.festival_locations != FestivalLocations.option_hard: return True_() - + animal_rule = self.animal.has_animal(Generic.any) artisan_rule = self.artisan.can_keg(Generic.any) | self.artisan.can_preserves_jar(Generic.any) cooking_rule = self.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough fish_rule = self.skill.can_fish(difficulty=50) forage_rule = self.region.can_reach_any((Region.forest, Region.backwoods)) # Hazelnut always available since the grange display is in fall mineral_rule = self.action.can_open_geode(Generic.any) # More than half the minerals are good enough - good_fruits = [Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate, - Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit, ] + good_fruits = (fruit + for fruit in + (Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate, + Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit) + if fruit in self.content.game_items) fruit_rule = self.has_any(*good_fruits) - good_vegetables = [Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale, - Vegetable.radish, Vegetable.taro_root, Vegetable.yam, Vegetable.red_cabbage, Vegetable.pumpkin] + good_vegetables = (vegeteable + for vegeteable in + (Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale, + Vegetable.radish, Vegetable.taro_root, Vegetable.yam, Vegetable.red_cabbage, Vegetable.pumpkin) + if vegeteable in self.content.game_items) vegetable_rule = self.has_any(*good_vegetables) return animal_rule & artisan_rule & cooking_rule & fish_rule & \ @@ -576,6 +571,44 @@ def has_walnut(self, number: int) -> StardewRule: return False_() if number <= 0: return True_() + + if self.options.walnutsanity == Walnutsanity.preset_none: + return self.can_get_walnuts(number) + if self.options.walnutsanity == Walnutsanity.preset_all: + return self.has_received_walnuts(number) + puzzle_walnuts = 61 + bush_walnuts = 25 + dig_walnuts = 18 + repeatable_walnuts = 33 + total_walnuts = puzzle_walnuts + bush_walnuts + dig_walnuts + repeatable_walnuts + walnuts_to_receive = 0 + walnuts_to_collect = number + if OptionName.walnutsanity_puzzles in self.options.walnutsanity: + puzzle_walnut_rate = puzzle_walnuts / total_walnuts + puzzle_walnuts_required = round(puzzle_walnut_rate * number) + walnuts_to_receive += puzzle_walnuts_required + walnuts_to_collect -= puzzle_walnuts_required + if OptionName.walnutsanity_bushes in self.options.walnutsanity: + bush_walnuts_rate = bush_walnuts / total_walnuts + bush_walnuts_required = round(bush_walnuts_rate * number) + walnuts_to_receive += bush_walnuts_required + walnuts_to_collect -= bush_walnuts_required + if OptionName.walnutsanity_dig_spots in self.options.walnutsanity: + dig_walnuts_rate = dig_walnuts / total_walnuts + dig_walnuts_required = round(dig_walnuts_rate * number) + walnuts_to_receive += dig_walnuts_required + walnuts_to_collect -= dig_walnuts_required + if OptionName.walnutsanity_repeatables in self.options.walnutsanity: + repeatable_walnuts_rate = repeatable_walnuts / total_walnuts + repeatable_walnuts_required = round(repeatable_walnuts_rate * number) + walnuts_to_receive += repeatable_walnuts_required + walnuts_to_collect -= repeatable_walnuts_required + return self.has_received_walnuts(walnuts_to_receive) & self.can_get_walnuts(walnuts_to_collect) + + def has_received_walnuts(self, number: int) -> StardewRule: + return self.received(Event.received_walnuts, number) + + def can_get_walnuts(self, number: int) -> StardewRule: # https://stardewcommunitywiki.com/Golden_Walnut#Walnut_Locations reach_south = self.region.can_reach(Region.island_south) reach_north = self.region.can_reach(Region.island_north) @@ -584,28 +617,28 @@ def has_walnut(self, number: int) -> StardewRule: reach_southeast = self.region.can_reach(Region.island_south_east) reach_field_office = self.region.can_reach(Region.field_office) reach_pirate_cove = self.region.can_reach(Region.pirate_cove) - reach_outside_areas = And(reach_south, reach_north, reach_west, reach_hut) + reach_outside_areas = self.logic.and_(reach_south, reach_north, reach_west, reach_hut) reach_volcano_regions = [self.region.can_reach(Region.volcano), self.region.can_reach(Region.volcano_secret_beach), self.region.can_reach(Region.volcano_floor_5), self.region.can_reach(Region.volcano_floor_10)] - reach_volcano = Or(*reach_volcano_regions) - reach_all_volcano = And(*reach_volcano_regions) + reach_volcano = self.logic.or_(*reach_volcano_regions) + reach_all_volcano = self.logic.and_(*reach_volcano_regions) reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano, reach_field_office] - reach_caves = And(self.region.can_reach(Region.qi_walnut_room), self.region.can_reach(Region.dig_site), - self.region.can_reach(Region.gourmand_frog_cave), - self.region.can_reach(Region.colored_crystals_cave), - self.region.can_reach(Region.shipwreck), self.received(APWeapon.slingshot)) - reach_entire_island = And(reach_outside_areas, reach_all_volcano, - reach_caves, reach_southeast, reach_field_office, reach_pirate_cove) + reach_caves = self.logic.and_(self.region.can_reach(Region.qi_walnut_room), self.region.can_reach(Region.dig_site), + self.region.can_reach(Region.gourmand_frog_cave), + self.region.can_reach(Region.colored_crystals_cave), + self.region.can_reach(Region.shipwreck), self.combat.has_slingshot) + reach_entire_island = self.logic.and_(reach_outside_areas, reach_all_volcano, + reach_caves, reach_southeast, reach_field_office, reach_pirate_cove) if number <= 5: - return Or(reach_south, reach_north, reach_west, reach_volcano) + return self.logic.or_(reach_south, reach_north, reach_west, reach_volcano) if number <= 10: return self.count(2, *reach_walnut_regions) if number <= 15: return self.count(3, *reach_walnut_regions) if number <= 20: - return And(*reach_walnut_regions) + return self.logic.and_(*reach_walnut_regions) if number <= 50: return reach_entire_island gems = (Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz) @@ -621,20 +654,22 @@ def has_all_stardrops(self) -> StardewRule: number_of_stardrops_to_receive += 1 # Museum Stardrop number_of_stardrops_to_receive += 1 # Krobus Stardrop - if self.options.fishsanity == Fishsanity.option_none: # Master Angler Stardrop - other_rules.append(self.fishing.can_catch_every_fish()) - else: + # Master Angler Stardrop + if self.content.features.fishsanity.is_enabled: number_of_stardrops_to_receive += 1 + else: + other_rules.append(self.fishing.can_catch_every_fish()) if self.options.festival_locations == FestivalLocations.option_disabled: # Fair Stardrop other_rules.append(self.season.has(Season.fall)) else: number_of_stardrops_to_receive += 1 - if self.options.friendsanity == Friendsanity.option_none: # Spouse Stardrop - other_rules.append(self.relationship.has_hearts(Generic.bachelor, 13)) - else: + # Spouse Stardrop + if self.content.features.friendsanity.is_enabled: number_of_stardrops_to_receive += 1 + else: + other_rules.append(self.relationship.has_hearts_with_any_bachelor(13)) if ModNames.deepwoods in self.options.mods: # Petting the Unicorn number_of_stardrops_to_receive += 1 @@ -642,18 +677,13 @@ def has_all_stardrops(self) -> StardewRule: if not other_rules: return self.received("Stardrop", number_of_stardrops_to_receive) - return self.received("Stardrop", number_of_stardrops_to_receive) & And(*other_rules) - - def has_prismatic_jelly_reward_access(self) -> StardewRule: - if self.options.special_order_locations == SpecialOrderLocations.option_disabled: - return self.special_order.can_complete_special_order("Prismatic Jelly") - return self.received("Monster Musk Recipe") + return self.received("Stardrop", number_of_stardrops_to_receive) & self.logic.and_(*other_rules) def has_all_rarecrows(self) -> StardewRule: rules = [] for rarecrow_number in range(1, 9): rules.append(self.received(f"Rarecrow #{rarecrow_number}")) - return And(*rules) + return self.logic.and_(*rules) def has_abandoned_jojamart(self) -> StardewRule: return self.received(CommunityUpgrade.movie_theater, 1) @@ -664,11 +694,5 @@ def has_movie_theater(self) -> StardewRule: def can_use_obelisk(self, obelisk: str) -> StardewRule: return self.region.can_reach(Region.farm) & self.received(obelisk) - def has_fruit_bats(self) -> StardewRule: - return self.region.can_reach(Region.farm_cave) & self.received(CommunityUpgrade.fruit_bats) - - def has_mushroom_cave(self) -> StardewRule: - return self.region.can_reach(Region.farm_cave) & self.received(CommunityUpgrade.mushroom_boxes) - def can_fish_pond(self, fish: str) -> StardewRule: return self.building.has_building(Building.fish_pond) & self.has(fish) diff --git a/worlds/stardew_valley/logic/logic_and_mods_design.md b/worlds/stardew_valley/logic/logic_and_mods_design.md index 14716e1af0e1..87631175b391 100644 --- a/worlds/stardew_valley/logic/logic_and_mods_design.md +++ b/worlds/stardew_valley/logic/logic_and_mods_design.md @@ -55,4 +55,21 @@ dependencies. Vanilla would always be first, then anything that depends only on 3. In `create_items`, AP items are unpacked, and randomized. 4. In `set_rules`, the rules are applied to the AP entrances and locations. Each content pack have to apply the proper rules for their entrances and locations. - (idea) To begin this step, sphere 0 could be simplified instantly as sphere 0 regions and items are already known. -5. Nothing to do in `generate_basic`. +5. Nothing to do in `generate_basic`. + +## Item Sources + +Instead of containing rules directly, items would contain sources that would then be transformed into rules. Using a single dispatch mechanism, the sources will +be associated to their actual logic. + +This system is extensible and easily maintainable in the ways that it decouple the rule and the actual items. Any "type" of item could be used with any "type" +of source (Monster drop and fish can have foraging sources). + +- Mods requiring special rules can remove sources from vanilla content or wrap them to add their own logic (Magic add sources for some items), or change the + rules for monster drop sources. +- (idea) A certain difficulty level (or maybe tags) could be added to the source, to enable or disable them given settings chosen by the player. Someone with a + high grinding tolerance can enable "hard" or "grindy" sources. Some source that are pushed back in further spheres can be replaced by less forgiving sources + if easy logic is disabled. For instance, anything that requires money could be accessible as soon as you can sell something to someone (even wood). + +Items are classified by their source. An item with a fishing or a crab pot source is considered a fish, an item dropping from a monster is a monster drop. An +item with a foraging source is a forageable. Items can fit in multiple categories. diff --git a/worlds/stardew_valley/logic/logic_event.py b/worlds/stardew_valley/logic/logic_event.py new file mode 100644 index 000000000000..9af1d622578f --- /dev/null +++ b/worlds/stardew_valley/logic/logic_event.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass + +from ..strings.ap_names import event_names +from ..strings.metal_names import MetalBar, Ore +from ..strings.region_names import Region + +all_events = event_names.all_events.copy() +all_logic_events = list() + + +@dataclass(frozen=True) +class LogicEvent: + name: str + region: str + + +@dataclass(frozen=True) +class LogicItemEvent(LogicEvent): + item: str + + def __init__(self, item: str, region: str): + super().__init__(f"{item} (Logic event)", region) + super().__setattr__("item", item) + + +def register_item_event(item: str, region: str = Region.farm): + event = LogicItemEvent(item, region) + all_logic_events.append(event) + all_events.add(event.name) + + +for i in (MetalBar.copper, MetalBar.iron, MetalBar.gold, MetalBar.iridium, Ore.copper, Ore.iron, Ore.gold, Ore.iridium): + register_item_event(i) diff --git a/worlds/stardew_valley/logic/mine_logic.py b/worlds/stardew_valley/logic/mine_logic.py index 2c2eaabfd8ee..61eba41ffe07 100644 --- a/worlds/stardew_valley/logic/mine_logic.py +++ b/worlds/stardew_valley/logic/mine_logic.py @@ -3,13 +3,15 @@ from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic from .combat_logic import CombatLogicMixin +from .cooking_logic import CookingLogicMixin +from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .skill_logic import SkillLogicMixin from .tool_logic import ToolLogicMixin from .. import options from ..options import ToolProgression -from ..stardew_rule import StardewRule, And, True_ +from ..stardew_rule import StardewRule, True_ from ..strings.performance_names import Performance from ..strings.region_names import Region from ..strings.skill_names import Skill @@ -22,7 +24,8 @@ def __init__(self, *args, **kwargs): self.mine = MineLogic(*args, **kwargs) -class MineLogic(BaseLogic[Union[MineLogicMixin, RegionLogicMixin, ReceivedLogicMixin, CombatLogicMixin, ToolLogicMixin, SkillLogicMixin]]): +class MineLogic(BaseLogic[Union[HasLogicMixin, MineLogicMixin, RegionLogicMixin, ReceivedLogicMixin, CombatLogicMixin, ToolLogicMixin, +SkillLogicMixin, CookingLogicMixin]]): # Regions def can_mine_in_the_mines_floor_1_40(self) -> StardewRule: return self.logic.region.can_reach(Region.mines_floor_5) @@ -57,11 +60,13 @@ def can_progress_in_the_mines_from_floor(self, floor: int) -> StardewRule: rules.append(weapon_rule) if self.options.tool_progression & ToolProgression.option_progressive: rules.append(self.logic.tool.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier])) - if self.options.skill_progression == options.SkillProgression.option_progressive: + if self.options.skill_progression >= options.SkillProgression.option_progressive: skill_tier = min(10, max(0, tier * 2)) rules.append(self.logic.skill.has_level(Skill.combat, skill_tier)) rules.append(self.logic.skill.has_level(Skill.mining, skill_tier)) - return And(*rules) + if tier >= 4: + rules.append(self.logic.cooking.can_cook()) + return self.logic.and_(*rules) @cache_self1 def has_mine_elevator_to_floor(self, floor: int) -> StardewRule: @@ -79,8 +84,8 @@ def can_progress_in_the_skull_cavern_from_floor(self, floor: int) -> StardewRule rules.append(weapon_rule) if self.options.tool_progression & ToolProgression.option_progressive: rules.append(self.logic.received("Progressive Pickaxe", min(4, max(0, tier + 2)))) - if self.options.skill_progression == options.SkillProgression.option_progressive: + if self.options.skill_progression >= options.SkillProgression.option_progressive: skill_tier = min(10, max(0, tier * 2 + 6)) rules.extend({self.logic.skill.has_level(Skill.combat, skill_tier), self.logic.skill.has_level(Skill.mining, skill_tier)}) - return And(*rules) + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/money_logic.py b/worlds/stardew_valley/logic/money_logic.py index 92945a3636a8..73c5291af082 100644 --- a/worlds/stardew_valley/logic/money_logic.py +++ b/worlds/stardew_valley/logic/money_logic.py @@ -2,16 +2,18 @@ from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic -from .buff_logic import BuffLogicMixin +from .grind_logic import GrindLogicMixin from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin +from .season_logic import SeasonLogicMixin from .time_logic import TimeLogicMixin +from ..data.shop import ShopSource from ..options import SpecialOrderLocations -from ..stardew_rule import StardewRule, True_, HasProgressionPercent, False_ +from ..stardew_rule import StardewRule, True_, HasProgressionPercent, False_, true_ from ..strings.ap_names.event_names import Event from ..strings.currency_names import Currency -from ..strings.region_names import Region +from ..strings.region_names import Region, LogicRegion 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") @@ -23,7 +25,8 @@ def __init__(self, *args, **kwargs): self.money = MoneyLogic(*args, **kwargs) -class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, BuffLogicMixin]]): +class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SeasonLogicMixin, +GrindLogicMixin]]): @cache_self1 def can_have_earned_total(self, amount: int) -> StardewRule: @@ -31,7 +34,7 @@ def can_have_earned_total(self, amount: int) -> StardewRule: return True_() pierre_rule = self.logic.region.can_reach_all((Region.pierre_store, Region.forest)) - willy_rule = self.logic.region.can_reach_all((Region.fish_shop, Region.fishing)) + willy_rule = self.logic.region.can_reach_all((Region.fish_shop, LogicRegion.fishing)) clint_rule = self.logic.region.can_reach_all((Region.blacksmith, Region.mines_floor_5)) robin_rule = self.logic.region.can_reach_all((Region.carpenter, Region.secret_woods)) shipping_rule = self.logic.received(Event.can_ship_items) @@ -64,6 +67,20 @@ def can_spend(self, amount: int) -> StardewRule: def can_spend_at(self, region: str, amount: int) -> StardewRule: return self.logic.region.can_reach(region) & self.logic.money.can_spend(amount) + @cache_self1 + def can_shop_from(self, source: ShopSource) -> StardewRule: + season_rule = self.logic.season.has_any(source.seasons) + money_rule = self.logic.money.can_spend(source.money_price) if source.money_price is not None else true_ + + item_rules = [] + if source.items_price is not None: + for price, item in source.items_price: + item_rules.append(self.logic.has(item) & self.logic.grind.can_grind_item(price)) + + region_rule = self.logic.region.can_reach(source.shop_region) + + return self.logic.and_(season_rule, money_rule, *item_rules, region_rule) + # Should be cached def can_trade(self, currency: str, amount: int) -> StardewRule: if amount == 0: @@ -71,11 +88,11 @@ def can_trade(self, currency: str, amount: int) -> StardewRule: if currency == Currency.money: return self.can_spend(amount) if currency == Currency.star_token: - return self.logic.region.can_reach(Region.fair) + return self.logic.region.can_reach(LogicRegion.fair) if currency == Currency.qi_coin: - return self.logic.region.can_reach(Region.casino) & self.logic.buff.has_max_luck() + return self.logic.region.can_reach(Region.casino) & self.logic.time.has_lived_months(amount // 1000) if currency == Currency.qi_gem: - if self.options.special_order_locations == SpecialOrderLocations.option_board_qi: + if self.options.special_order_locations & SpecialOrderLocations.value_qi: number_rewards = min(len(qi_gem_rewards), max(1, (amount // 10))) return self.logic.received_n(*qi_gem_rewards, count=number_rewards) number_rewards = 2 @@ -84,7 +101,7 @@ def can_trade(self, currency: str, amount: int) -> StardewRule: if currency == Currency.golden_walnut: return self.can_spend_walnut(amount) - return self.logic.has(currency) & self.logic.time.has_lived_months(amount) + return self.logic.has(currency) & self.logic.grind.can_grind_item(amount) # Should be cached def can_trade_at(self, region: str, currency: str, amount: int) -> StardewRule: diff --git a/worlds/stardew_valley/logic/monster_logic.py b/worlds/stardew_valley/logic/monster_logic.py index 790f492347e6..7e6d786972ac 100644 --- a/worlds/stardew_valley/logic/monster_logic.py +++ b/worlds/stardew_valley/logic/monster_logic.py @@ -4,11 +4,13 @@ from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic from .combat_logic import CombatLogicMixin +from .has_logic import HasLogicMixin from .region_logic import RegionLogicMixin from .time_logic import TimeLogicMixin, MAX_MONTHS from .. import options from ..data import monster_data -from ..stardew_rule import StardewRule, Or, And +from ..stardew_rule import StardewRule +from ..strings.generic_names import Generic from ..strings.region_names import Region @@ -18,7 +20,7 @@ def __init__(self, *args, **kwargs): self.monster = MonsterLogic(*args, **kwargs) -class MonsterLogic(BaseLogic[Union[MonsterLogicMixin, RegionLogicMixin, CombatLogicMixin, TimeLogicMixin]]): +class MonsterLogic(BaseLogic[Union[HasLogicMixin, MonsterLogicMixin, RegionLogicMixin, CombatLogicMixin, TimeLogicMixin]]): @cached_property def all_monsters_by_name(self): @@ -29,13 +31,18 @@ def all_monsters_by_category(self): return monster_data.all_monsters_by_category_given_mods(self.options.mods.value) def can_kill(self, monster: Union[str, monster_data.StardewMonster], amount_tier: int = 0) -> StardewRule: + if amount_tier <= 0: + amount_tier = 0 + time_rule = self.logic.time.has_lived_months(amount_tier) + if isinstance(monster, str): + if monster == Generic.any: + return self.logic.monster.can_kill_any(self.all_monsters_by_name.values()) & time_rule + monster = self.all_monsters_by_name[monster] region_rule = self.logic.region.can_reach_any(monster.locations) combat_rule = self.logic.combat.can_fight_at_level(monster.difficulty) - if amount_tier <= 0: - amount_tier = 0 - time_rule = self.logic.time.has_lived_months(amount_tier) + return region_rule & combat_rule & time_rule @cache_self1 @@ -48,13 +55,11 @@ def can_kill_max(self, monster: monster_data.StardewMonster) -> StardewRule: # Should be cached def can_kill_any(self, monsters: (Iterable[monster_data.StardewMonster], Hashable), amount_tier: int = 0) -> StardewRule: - rules = [self.logic.monster.can_kill(monster, amount_tier) for monster in monsters] - return Or(*rules) + return self.logic.or_(*(self.logic.monster.can_kill(monster, amount_tier) for monster in monsters)) # Should be cached def can_kill_all(self, monsters: (Iterable[monster_data.StardewMonster], Hashable), amount_tier: int = 0) -> StardewRule: - rules = [self.logic.monster.can_kill(monster, amount_tier) for monster in monsters] - return And(*rules) + return self.logic.and_(*(self.logic.monster.can_kill(monster, amount_tier) for monster in monsters)) def can_complete_all_monster_slaying_goals(self) -> StardewRule: rules = [self.logic.time.has_lived_max_months] @@ -66,4 +71,4 @@ def can_complete_all_monster_slaying_goals(self) -> StardewRule: continue rules.append(self.logic.monster.can_kill_any(self.all_monsters_by_category[category])) - return And(*rules) + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/museum_logic.py b/worlds/stardew_valley/logic/museum_logic.py index 59ef0f6499c1..4ba5364f5524 100644 --- a/worlds/stardew_valley/logic/museum_logic.py +++ b/worlds/stardew_valley/logic/museum_logic.py @@ -6,10 +6,14 @@ from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin +from .time_logic import TimeLogicMixin +from .tool_logic import ToolLogicMixin from .. import options from ..data.museum_data import MuseumItem, all_museum_items, all_museum_artifacts, all_museum_minerals -from ..stardew_rule import StardewRule, And, False_ +from ..stardew_rule import StardewRule, False_ +from ..strings.metal_names import Mineral from ..strings.region_names import Region +from ..strings.tool_names import Tool, ToolMaterial class MuseumLogicMixin(BaseLogicMixin): @@ -18,7 +22,7 @@ def __init__(self, *args, **kwargs): self.museum = MuseumLogic(*args, **kwargs) -class MuseumLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, ActionLogicMixin, MuseumLogicMixin]]): +class MuseumLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, ActionLogicMixin, ToolLogicMixin, MuseumLogicMixin]]): def can_donate_museum_items(self, number: int) -> StardewRule: return self.logic.region.can_reach(Region.museum) & self.logic.museum.can_find_museum_items(number) @@ -33,15 +37,16 @@ def can_find_museum_item(self, item: MuseumItem) -> StardewRule: else: region_rule = False_() if item.geodes: - geodes_rule = And(*(self.logic.action.can_open_geode(geode) for geode in item.geodes)) + geodes_rule = self.logic.and_(*(self.logic.action.can_open_geode(geode) for geode in item.geodes)) else: geodes_rule = False_() # monster_rule = self.can_farm_monster(item.monsters) - # extra_rule = True_() + time_needed_to_grind = (20 - item.difficulty) / 2 + time_rule = self.logic.time.has_lived_months(time_needed_to_grind) pan_rule = False_() - if item.item_name == "Earth Crystal" or item.item_name == "Fire Quartz" or item.item_name == "Frozen Tear": - pan_rule = self.logic.action.can_pan() - return pan_rule | region_rule | geodes_rule # & monster_rule & extra_rule + if item.item_name == Mineral.earth_crystal or item.item_name == Mineral.fire_quartz or item.item_name == Mineral.frozen_tear: + pan_rule = self.logic.tool.has_tool(Tool.pan, ToolMaterial.iridium) + return (pan_rule | region_rule | geodes_rule) & time_rule # & monster_rule & extra_rule def can_find_museum_artifacts(self, number: int) -> StardewRule: rules = [] @@ -74,7 +79,7 @@ def can_complete_museum(self) -> StardewRule: for donation in all_museum_items: rules.append(self.logic.museum.can_find_museum_item(donation)) - return And(*rules) & self.logic.region.can_reach(Region.museum) + return self.logic.and_(*rules) & self.logic.region.can_reach(Region.museum) def can_donate(self, item: str) -> StardewRule: return self.logic.has(item) & self.logic.region.can_reach(Region.museum) diff --git a/worlds/stardew_valley/logic/pet_logic.py b/worlds/stardew_valley/logic/pet_logic.py index 5d7d79a358ca..0438940a6633 100644 --- a/worlds/stardew_valley/logic/pet_logic.py +++ b/worlds/stardew_valley/logic/pet_logic.py @@ -6,11 +6,9 @@ from .region_logic import RegionLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin -from ..data.villagers_data import Villager -from ..options import Friendsanity +from ..content.feature.friendsanity import pet_heart_item_name from ..stardew_rule import StardewRule, True_ from ..strings.region_names import Region -from ..strings.villager_names import NPC class PetLogicMixin(BaseLogicMixin): @@ -20,21 +18,25 @@ def __init__(self, *args, **kwargs): class PetLogic(BaseLogic[Union[RegionLogicMixin, ReceivedLogicMixin, TimeLogicMixin, ToolLogicMixin]]): - def has_hearts(self, hearts: int = 1) -> StardewRule: - if hearts <= 0: + def has_pet_hearts(self, hearts: int = 1) -> StardewRule: + assert hearts >= 0, "You can't have negative hearts with a pet." + if hearts == 0: return True_() - if self.options.friendsanity == Friendsanity.option_none or self.options.friendsanity == Friendsanity.option_bachelors: - return self.can_befriend_pet(hearts) - return self.received_hearts(NPC.pet, hearts) - def received_hearts(self, npc: Union[str, Villager], hearts: int) -> StardewRule: - if isinstance(npc, Villager): - return self.received_hearts(npc.name, hearts) - return self.logic.received(self.heart(npc), math.ceil(hearts / self.options.friendsanity_heart_size)) + if self.content.features.friendsanity.is_pet_randomized: + return self.received_pet_hearts(hearts) + + return self.can_befriend_pet(hearts) + + def received_pet_hearts(self, hearts: int) -> StardewRule: + return self.logic.received(pet_heart_item_name, + math.ceil(hearts / self.content.features.friendsanity.heart_size)) def can_befriend_pet(self, hearts: int) -> StardewRule: - if hearts <= 0: + assert hearts >= 0, "You can't have negative hearts with a pet." + if hearts == 0: return True_() + points = hearts * 200 points_per_month = 12 * 14 points_per_water_month = 18 * 14 @@ -43,8 +45,3 @@ def can_befriend_pet(self, hearts: int) -> StardewRule: time_without_water_rule = self.logic.time.has_lived_months(points // points_per_month) time_rule = time_with_water_rule | time_without_water_rule return farm_rule & time_rule - - def heart(self, npc: Union[str, Villager]) -> str: - if isinstance(npc, str): - return f"{npc} <3" - return self.heart(npc.name) diff --git a/worlds/stardew_valley/logic/quality_logic.py b/worlds/stardew_valley/logic/quality_logic.py new file mode 100644 index 000000000000..54e2d242654b --- /dev/null +++ b/worlds/stardew_valley/logic/quality_logic.py @@ -0,0 +1,33 @@ +from typing import Union + +from Utils import cache_self1 +from .base_logic import BaseLogicMixin, BaseLogic +from .farming_logic import FarmingLogicMixin +from .skill_logic import SkillLogicMixin +from ..stardew_rule import StardewRule, True_, False_ +from ..strings.quality_names import CropQuality + + +class QualityLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.quality = QualityLogic(*args, **kwargs) + + +class QualityLogic(BaseLogic[Union[SkillLogicMixin, FarmingLogicMixin]]): + + @cache_self1 + def can_grow_crop_quality(self, quality: str) -> StardewRule: + if quality == CropQuality.basic: + return True_() + if quality == CropQuality.silver: + return self.logic.skill.has_farming_level(5) | (self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(2)) | ( + self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(1)) | self.logic.farming.has_fertilizer(3) + if quality == CropQuality.gold: + return self.logic.skill.has_farming_level(10) | ( + self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(5)) | ( + self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(3)) | ( + self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(2)) + if quality == CropQuality.iridium: + return self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(4) + return False_() diff --git a/worlds/stardew_valley/logic/quest_logic.py b/worlds/stardew_valley/logic/quest_logic.py index bc1f731429c6..42f401b96025 100644 --- a/worlds/stardew_valley/logic/quest_logic.py +++ b/worlds/stardew_valley/logic/quest_logic.py @@ -17,6 +17,7 @@ from .tool_logic import ToolLogicMixin from .wallet_logic import WalletLogicMixin from ..stardew_rule import StardewRule, Has, True_ +from ..strings.ap_names.community_upgrade_names import CommunityUpgrade from ..strings.artisan_good_names import ArtisanGood from ..strings.building_names import Building from ..strings.craftable_names import Craftable @@ -43,7 +44,8 @@ def __init__(self, *args, **kwargs): class QuestLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, MoneyLogicMixin, MineLogicMixin, RegionLogicMixin, RelationshipLogicMixin, ToolLogicMixin, -FishingLogicMixin, CookingLogicMixin, CombatLogicMixin, SeasonLogicMixin, SkillLogicMixin, WalletLogicMixin, QuestLogicMixin, BuildingLogicMixin, TimeLogicMixin]]): + FishingLogicMixin, CookingLogicMixin, CombatLogicMixin, SeasonLogicMixin, SkillLogicMixin, WalletLogicMixin, QuestLogicMixin, + BuildingLogicMixin, TimeLogicMixin]]): def initialize_rules(self): self.update_rules({ @@ -52,6 +54,7 @@ def initialize_rules(self): Quest.getting_started: self.logic.has(Vegetable.parsnip), Quest.to_the_beach: self.logic.region.can_reach(Region.beach), Quest.raising_animals: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.building.has_building(Building.coop), + Quest.feeding_animals: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.building.has_building(Building.silo), Quest.advancement: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.has(Craftable.scarecrow), Quest.archaeology: self.logic.tool.has_tool(Tool.hoe) | self.logic.mine.can_mine_in_the_mines_floor_1_40() | self.logic.skill.can_fish(), Quest.rat_problem: self.logic.region.can_reach_all((Region.town, Region.community_center)), @@ -63,7 +66,8 @@ def initialize_rules(self): Quest.jodis_request: self.logic.season.has(Season.spring) & self.logic.has(Vegetable.cauliflower) & self.logic.relationship.can_meet(NPC.jodi), Quest.mayors_shorts: self.logic.season.has(Season.summer) & self.logic.relationship.has_hearts(NPC.marnie, 2) & self.logic.relationship.can_meet(NPC.lewis), - Quest.blackberry_basket: self.logic.season.has(Season.fall) & self.logic.relationship.can_meet(NPC.linus), + Quest.blackberry_basket: self.logic.season.has(Season.fall) & self.logic.relationship.can_meet(NPC.linus) & self.logic.region.can_reach( + Region.tunnel_entrance), Quest.marnies_request: self.logic.relationship.has_hearts(NPC.marnie, 3) & self.logic.has(Forageable.cave_carrot), Quest.pam_is_thirsty: self.logic.season.has(Season.summer) & self.logic.has(ArtisanGood.pale_ale) & self.logic.relationship.can_meet(NPC.pam), Quest.a_dark_reagent: self.logic.season.has(Season.winter) & self.logic.has(Loot.void_essence) & self.logic.relationship.can_meet(NPC.wizard), @@ -104,13 +108,14 @@ def initialize_rules(self): Quest.the_pirates_wife: self.logic.relationship.can_meet(NPC.kent) & self.logic.relationship.can_meet(NPC.gus) & self.logic.relationship.can_meet(NPC.sandy) & self.logic.relationship.can_meet(NPC.george) & self.logic.relationship.can_meet(NPC.wizard) & self.logic.relationship.can_meet(NPC.willy), + Quest.giant_stump: self.logic.has(Material.hardwood) }) def update_rules(self, new_rules: Dict[str, StardewRule]): self.registry.quest_rules.update(new_rules) def can_complete_quest(self, quest: str) -> StardewRule: - return Has(quest, self.registry.quest_rules) + return Has(quest, self.registry.quest_rules, "quest") def has_club_card(self) -> StardewRule: if self.options.quest_locations < 0: @@ -126,3 +131,12 @@ def has_dark_talisman(self) -> StardewRule: if self.options.quest_locations < 0: return self.logic.quest.can_complete_quest(Quest.dark_talisman) return self.logic.received(Wallet.dark_talisman) + + def has_raccoon_shop(self) -> StardewRule: + if self.options.quest_locations < 0: + return self.logic.received(CommunityUpgrade.raccoon, 2) & self.logic.quest.can_complete_quest(Quest.giant_stump) + + # 1 - Break the tree + # 2 - Build the house, which summons the bundle racoon. This one is done manually if quests are turned off + # 3 - Raccoon's wife opens the shop + return self.logic.received(CommunityUpgrade.raccoon, 3) diff --git a/worlds/stardew_valley/logic/received_logic.py b/worlds/stardew_valley/logic/received_logic.py index 66dc078ad46f..f5c5c9f7a206 100644 --- a/worlds/stardew_valley/logic/received_logic.py +++ b/worlds/stardew_valley/logic/received_logic.py @@ -1,26 +1,32 @@ from typing import Optional +from BaseClasses import ItemClassification from .base_logic import BaseLogic, BaseLogicMixin from .has_logic import HasLogicMixin -from ..stardew_rule import StardewRule, Received, And, Or, TotalReceived +from .logic_event import all_events +from ..items import item_table +from ..stardew_rule import StardewRule, Received, TotalReceived class ReceivedLogicMixin(BaseLogic[HasLogicMixin], BaseLogicMixin): - # Should be cached def received(self, item: str, count: Optional[int] = 1) -> StardewRule: assert count >= 0, "Can't receive a negative amount of item." + if item in all_events: + return Received(item, self.player, count, event=True) + + assert item_table[item].classification & ItemClassification.progression, f"Item [{item_table[item].name}] has to be progression to be used in logic" return Received(item, self.player, count) def received_all(self, *items: str): assert items, "Can't receive all of no items." - return And(*(self.received(item) for item in items)) + return self.logic.and_(*(self.received(item) for item in items)) def received_any(self, *items: str): assert items, "Can't receive any of no items." - return Or(*(self.received(item) for item in items)) + return self.logic.or_(*(self.received(item) for item in items)) def received_once(self, *items: str, count: int): assert items, "Can't receive once of no items." @@ -32,4 +38,7 @@ def received_n(self, *items: str, count: int): assert items, "Can't receive n of no items." assert count >= 0, "Can't receive a negative amount of item." + for item in items: + assert item_table[item].classification & ItemClassification.progression, f"Item [{item_table[item].name}] has to be progression to be used in logic" + return TotalReceived(count, items, self.player) diff --git a/worlds/stardew_valley/logic/region_logic.py b/worlds/stardew_valley/logic/region_logic.py index 81dabf45aac5..69afa624f22c 100644 --- a/worlds/stardew_valley/logic/region_logic.py +++ b/worlds/stardew_valley/logic/region_logic.py @@ -4,7 +4,7 @@ from .base_logic import BaseLogic, BaseLogicMixin from .has_logic import HasLogicMixin from ..options import EntranceRandomization -from ..stardew_rule import StardewRule, And, Or, Reach, false_, true_ +from ..stardew_rule import StardewRule, Reach, false_, true_ from ..strings.region_names import Region main_outside_area = {Region.menu, Region.stardew_valley, Region.farm_house, Region.farm, Region.town, Region.beach, Region.mountain, Region.forest, @@ -18,6 +18,7 @@ always_regions_by_setting = {EntranceRandomization.option_disabled: always_accessible_regions_without_er, EntranceRandomization.option_pelican_town: always_accessible_regions_without_er, EntranceRandomization.option_non_progression: always_accessible_regions_without_er, + EntranceRandomization.option_buildings_without_house: main_outside_area, EntranceRandomization.option_buildings: main_outside_area, EntranceRandomization.option_chaos: always_accessible_regions_without_er} @@ -42,11 +43,14 @@ def can_reach(self, region_name: str) -> StardewRule: @cache_self1 def can_reach_any(self, region_names: Tuple[str, ...]) -> StardewRule: - return Or(*(self.logic.region.can_reach(spot) for spot in region_names)) + if any(r in always_regions_by_setting[self.options.entrance_randomization] for r in region_names): + return true_ + + return self.logic.or_(*(self.logic.region.can_reach(spot) for spot in region_names)) @cache_self1 def can_reach_all(self, region_names: Tuple[str, ...]) -> StardewRule: - return And(*(self.logic.region.can_reach(spot) for spot in region_names)) + return self.logic.and_(*(self.logic.region.can_reach(spot) for spot in region_names)) @cache_self1 def can_reach_all_except_one(self, region_names: Tuple[str, ...]) -> StardewRule: diff --git a/worlds/stardew_valley/logic/relationship_logic.py b/worlds/stardew_valley/logic/relationship_logic.py index fb0267bddb1a..61e63a90c83a 100644 --- a/worlds/stardew_valley/logic/relationship_logic.py +++ b/worlds/stardew_valley/logic/relationship_logic.py @@ -1,6 +1,5 @@ import math -from functools import cached_property -from typing import Union, List +from typing import Union from Utils import cache_self1 from .base_logic import BaseLogic, BaseLogicMixin @@ -11,9 +10,9 @@ from .region_logic import RegionLogicMixin from .season_logic import SeasonLogicMixin from .time_logic import TimeLogicMixin -from ..data.villagers_data import all_villagers_by_name, Villager, get_villagers_for_mods -from ..options import Friendsanity -from ..stardew_rule import StardewRule, True_, And, Or +from ..content.feature import friendsanity +from ..data.villagers_data import Villager +from ..stardew_rule import StardewRule, True_, false_, true_ from ..strings.ap_names.mods.mod_items import SVEQuestItem from ..strings.crop_names import Fruit from ..strings.generic_names import Generic @@ -38,12 +37,8 @@ def __init__(self, *args, **kwargs): self.relationship = RelationshipLogic(*args, **kwargs) -class RelationshipLogic(BaseLogic[Union[ - RelationshipLogicMixin, BuildingLogicMixin, SeasonLogicMixin, TimeLogicMixin, GiftLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin]]): - - @cached_property - def all_villagers_given_mods(self) -> List[Villager]: - return get_villagers_for_mods(self.options.mods.value) +class RelationshipLogic(BaseLogic[Union[RelationshipLogicMixin, BuildingLogicMixin, SeasonLogicMixin, TimeLogicMixin, GiftLogicMixin, RegionLogicMixin, +ReceivedLogicMixin, HasLogicMixin]]): def can_date(self, npc: str) -> StardewRule: return self.logic.relationship.has_hearts(npc, 8) & self.logic.has(Gift.bouquet) @@ -52,134 +47,160 @@ def can_marry(self, npc: str) -> StardewRule: return self.logic.relationship.has_hearts(npc, 10) & self.logic.has(Gift.mermaid_pendant) def can_get_married(self) -> StardewRule: - return self.logic.relationship.has_hearts(Generic.bachelor, 10) & self.logic.has(Gift.mermaid_pendant) + return self.logic.relationship.has_hearts_with_any_bachelor(10) & self.logic.has(Gift.mermaid_pendant) def has_children(self, number_children: int) -> StardewRule: - if number_children <= 0: + assert number_children >= 0, "Can't have a negative amount of children." + if number_children == 0: return True_() - if self.options.friendsanity == Friendsanity.option_none: + + if not self.content.features.friendsanity.is_enabled: return self.logic.relationship.can_reproduce(number_children) + return self.logic.received_n(*possible_kids, count=number_children) & self.logic.building.has_house(2) def can_reproduce(self, number_children: int = 1) -> StardewRule: - if number_children <= 0: + assert number_children >= 0, "Can't have a negative amount of children." + if number_children == 0: return True_() - baby_rules = [self.logic.relationship.can_get_married(), self.logic.building.has_house(2), self.logic.relationship.has_hearts(Generic.bachelor, 12), + + baby_rules = [self.logic.relationship.can_get_married(), + self.logic.building.has_house(2), + self.logic.relationship.has_hearts_with_any_bachelor(12), self.logic.relationship.has_children(number_children - 1)] - return And(*baby_rules) - # Should be cached - def has_hearts(self, npc: str, hearts: int = 1) -> StardewRule: - if hearts <= 0: + return self.logic.and_(*baby_rules) + + @cache_self1 + def has_hearts_with_any_bachelor(self, hearts: int = 1) -> StardewRule: + assert hearts >= 0, f"Can't have a negative hearts with any bachelor." + if hearts == 0: return True_() - if self.options.friendsanity == Friendsanity.option_none: - return self.logic.relationship.can_earn_relationship(npc, hearts) - if npc not in all_villagers_by_name: - if npc == Generic.any or npc == Generic.bachelor: - possible_friends = [] - for name in all_villagers_by_name: - if not self.npc_is_in_current_slot(name): - continue - if npc == Generic.any or all_villagers_by_name[name].bachelor: - possible_friends.append(self.logic.relationship.has_hearts(name, hearts)) - return Or(*possible_friends) - if npc == Generic.all: - mandatory_friends = [] - for name in all_villagers_by_name: - if not self.npc_is_in_current_slot(name): - continue - mandatory_friends.append(self.logic.relationship.has_hearts(name, hearts)) - return And(*mandatory_friends) - if npc.isnumeric(): - possible_friends = [] - for name in all_villagers_by_name: - if not self.npc_is_in_current_slot(name): - continue - possible_friends.append(self.logic.relationship.has_hearts(name, hearts)) - return self.logic.count(int(npc), *possible_friends) - return self.can_earn_relationship(npc, hearts) - - if not self.npc_is_in_current_slot(npc): + + return self.logic.or_(*(self.logic.relationship.has_hearts(name, hearts) + for name, villager in self.content.villagers.items() + if villager.bachelor)) + + @cache_self1 + def has_hearts_with_any(self, hearts: int = 1) -> StardewRule: + assert hearts >= 0, f"Can't have a negative hearts with any npc." + if hearts == 0: return True_() - villager = all_villagers_by_name[npc] - if self.options.friendsanity == Friendsanity.option_bachelors and not villager.bachelor: - return self.logic.relationship.can_earn_relationship(npc, hearts) - if self.options.friendsanity == Friendsanity.option_starting_npcs and not villager.available: + + return self.logic.or_(*(self.logic.relationship.has_hearts(name, hearts) + for name, villager in self.content.villagers.items())) + + def has_hearts_with_n(self, amount: int, hearts: int = 1) -> StardewRule: + assert hearts >= 0, f"Can't have a negative hearts with any npc." + assert amount >= 0, f"Can't have a negative amount of npc." + if hearts == 0 or amount == 0: + return True_() + + return self.logic.count(amount, *(self.logic.relationship.has_hearts(name, hearts) + for name, villager in self.content.villagers.items())) + + # Should be cached + def has_hearts(self, npc: str, hearts: int = 1) -> StardewRule: + assert hearts >= 0, f"Can't have a negative hearts with {npc}." + + villager = self.content.villagers.get(npc) + if villager is None: + return false_ + + if hearts == 0: + return true_ + + heart_steps = self.content.features.friendsanity.get_randomized_hearts(villager) + if not heart_steps or hearts > heart_steps[-1]: # Hearts are sorted, bigger is the last one. return self.logic.relationship.can_earn_relationship(npc, hearts) - is_capped_at_8 = villager.bachelor and self.options.friendsanity != Friendsanity.option_all_with_marriage - if is_capped_at_8 and hearts > 8: - return self.logic.relationship.received_hearts(villager.name, 8) & self.logic.relationship.can_earn_relationship(npc, hearts) - return self.logic.relationship.received_hearts(villager.name, hearts) + + return self.logic.relationship.received_hearts(villager, hearts) # Should be cached - def received_hearts(self, npc: str, hearts: int) -> StardewRule: - heart_item = heart_item_name(npc) - number_required = math.ceil(hearts / self.options.friendsanity_heart_size) - return self.logic.received(heart_item, number_required) + def received_hearts(self, villager: Villager, hearts: int) -> StardewRule: + heart_item = friendsanity.to_item_name(villager.name) + + number_required = math.ceil(hearts / self.content.features.friendsanity.heart_size) + return self.logic.received(heart_item, number_required) & self.can_meet(villager.name) @cache_self1 def can_meet(self, npc: str) -> StardewRule: - if npc not in all_villagers_by_name or not self.npc_is_in_current_slot(npc): - return True_() - villager = all_villagers_by_name[npc] + villager = self.content.villagers.get(npc) + if villager is None: + return false_ + rules = [self.logic.region.can_reach_any(villager.locations)] + if npc == NPC.kent: rules.append(self.logic.time.has_year_two) + elif npc == NPC.leo: - rules.append(self.logic.received("Island West Turtle")) + rules.append(self.logic.received("Island North Turtle")) + elif npc == ModNPC.lance: rules.append(self.logic.region.can_reach(Region.volcano_floor_10)) + elif npc == ModNPC.apples: rules.append(self.logic.has(Fruit.starfruit)) + elif npc == ModNPC.scarlett: scarlett_job = self.logic.received(SVEQuestItem.scarlett_job_offer) scarlett_spring = self.logic.season.has(Season.spring) & self.can_meet(ModNPC.andy) scarlett_summer = self.logic.season.has(Season.summer) & self.can_meet(ModNPC.susan) scarlett_fall = self.logic.season.has(Season.fall) & self.can_meet(ModNPC.sophia) rules.append(scarlett_job & (scarlett_spring | scarlett_summer | scarlett_fall)) + elif npc == ModNPC.morgan: rules.append(self.logic.received(SVEQuestItem.morgan_schooling)) + elif npc == ModNPC.goblin: rules.append(self.logic.region.can_reach_all((Region.witch_hut, Region.wizard_tower))) - return And(*rules) + return self.logic.and_(*rules) def can_give_loved_gifts_to_everyone(self) -> StardewRule: rules = [] - for npc in all_villagers_by_name: - if not self.npc_is_in_current_slot(npc): - continue + + for npc in self.content.villagers: meet_rule = self.logic.relationship.can_meet(npc) rules.append(meet_rule) + rules.append(self.logic.gifts.has_any_universal_love) - return And(*rules) + + return self.logic.and_(*rules) # Should be cached def can_earn_relationship(self, npc: str, hearts: int = 0) -> StardewRule: - if hearts <= 0: + assert hearts >= 0, f"Can't have a negative hearts with {npc}." + + villager = self.content.villagers.get(npc) + if villager is None: + return false_ + + if hearts == 0: return True_() - previous_heart = hearts - self.options.friendsanity_heart_size - previous_heart_rule = self.logic.relationship.has_hearts(npc, previous_heart) + rules = [self.logic.relationship.can_meet(npc)] - if npc not in all_villagers_by_name or not self.npc_is_in_current_slot(npc): - return previous_heart_rule + heart_size = self.content.features.friendsanity.heart_size + max_randomized_hearts = self.content.features.friendsanity.get_randomized_hearts(villager) + if max_randomized_hearts: + if hearts > max_randomized_hearts[-1]: + rules.append(self.logic.relationship.has_hearts(npc, hearts - 1)) + else: + previous_heart = max(hearts - heart_size, 0) + rules.append(self.logic.relationship.has_hearts(npc, previous_heart)) - rules = [previous_heart_rule, self.logic.relationship.can_meet(npc)] - villager = all_villagers_by_name[npc] - if hearts > 2 or hearts > self.options.friendsanity_heart_size: + if hearts > 2 or hearts > heart_size: rules.append(self.logic.season.has(villager.birthday)) + if villager.birthday == Generic.any: rules.append(self.logic.season.has_all() | self.logic.time.has_year_three) # push logic back for any birthday-less villager + if villager.bachelor: - if hearts > 8: - rules.append(self.logic.relationship.can_date(npc)) if hearts > 10: rules.append(self.logic.relationship.can_marry(npc)) + elif hearts > 8: + rules.append(self.logic.relationship.can_date(npc)) - return And(*rules) - - @cache_self1 - def npc_is_in_current_slot(self, name: str) -> bool: - npc = all_villagers_by_name[name] - return npc in self.all_villagers_given_mods + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/requirement_logic.py b/worlds/stardew_valley/logic/requirement_logic.py new file mode 100644 index 000000000000..87d9ee021524 --- /dev/null +++ b/worlds/stardew_valley/logic/requirement_logic.py @@ -0,0 +1,52 @@ +import functools +from typing import Union, Iterable + +from .base_logic import BaseLogicMixin, BaseLogic +from .book_logic import BookLogicMixin +from .has_logic import HasLogicMixin +from .received_logic import ReceivedLogicMixin +from .season_logic import SeasonLogicMixin +from .skill_logic import SkillLogicMixin +from .time_logic import TimeLogicMixin +from .tool_logic import ToolLogicMixin +from ..data.game_item import Requirement +from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement + + +class RequirementLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.requirement = RequirementLogic(*args, **kwargs) + + +class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin, +SeasonLogicMixin, TimeLogicMixin]]): + + def meet_all_requirements(self, requirements: Iterable[Requirement]): + if not requirements: + return self.logic.true_ + return self.logic.and_(*(self.logic.requirement.meet_requirement(requirement) for requirement in requirements)) + + @functools.singledispatchmethod + def meet_requirement(self, requirement: Requirement): + raise ValueError(f"Requirements of type{type(requirement)} have no rule registered.") + + @meet_requirement.register + def _(self, requirement: ToolRequirement): + return self.logic.tool.has_tool(requirement.tool, requirement.tier) + + @meet_requirement.register + def _(self, requirement: SkillRequirement): + return self.logic.skill.has_level(requirement.skill, requirement.level) + + @meet_requirement.register + def _(self, requirement: BookRequirement): + return self.logic.book.has_book_power(requirement.book) + + @meet_requirement.register + def _(self, requirement: SeasonRequirement): + return self.logic.season.has(requirement.season) + + @meet_requirement.register + def _(self, requirement: YearRequirement): + return self.logic.time.has_year(requirement.year) diff --git a/worlds/stardew_valley/logic/season_logic.py b/worlds/stardew_valley/logic/season_logic.py index 1953502099b4..6df315c0db94 100644 --- a/worlds/stardew_valley/logic/season_logic.py +++ b/worlds/stardew_valley/logic/season_logic.py @@ -1,11 +1,13 @@ +from functools import cached_property from typing import Iterable, Union from Utils import cache_self1 from .base_logic import BaseLogic, BaseLogicMixin +from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .time_logic import TimeLogicMixin from ..options import SeasonRandomization -from ..stardew_rule import StardewRule, True_, Or, And +from ..stardew_rule import StardewRule, True_, true_ from ..strings.generic_names import Generic from ..strings.season_names import Season @@ -16,7 +18,23 @@ def __init__(self, *args, **kwargs): self.season = SeasonLogic(*args, **kwargs) -class SeasonLogic(BaseLogic[Union[SeasonLogicMixin, TimeLogicMixin, ReceivedLogicMixin]]): +class SeasonLogic(BaseLogic[Union[HasLogicMixin, SeasonLogicMixin, TimeLogicMixin, ReceivedLogicMixin]]): + + @cached_property + def has_spring(self) -> StardewRule: + return self.logic.season.has(Season.spring) + + @cached_property + def has_summer(self) -> StardewRule: + return self.logic.season.has(Season.summer) + + @cached_property + def has_fall(self) -> StardewRule: + return self.logic.season.has(Season.fall) + + @cached_property + def has_winter(self) -> StardewRule: + return self.logic.season.has(Season.winter) @cache_self1 def has(self, season: str) -> StardewRule: @@ -32,13 +50,16 @@ def has(self, season: str) -> StardewRule: return self.logic.received(season) def has_any(self, seasons: Iterable[str]): + if seasons == Season.all: + return true_ if not seasons: + # That should be false, but I'm scared. return True_() - return Or(*(self.logic.season.has(season) for season in seasons)) + return self.logic.or_(*(self.logic.season.has(season) for season in seasons)) def has_any_not_winter(self): return self.logic.season.has_any([Season.spring, Season.summer, Season.fall]) def has_all(self): seasons = [Season.spring, Season.summer, Season.fall, Season.winter] - return And(*(self.logic.season.has(season) for season in seasons)) + return self.logic.and_(*(self.logic.season.has(season) for season in seasons)) diff --git a/worlds/stardew_valley/logic/shipping_logic.py b/worlds/stardew_valley/logic/shipping_logic.py index 52c97561b326..8d545e219627 100644 --- a/worlds/stardew_valley/logic/shipping_logic.py +++ b/worlds/stardew_valley/logic/shipping_logic.py @@ -10,7 +10,7 @@ from ..locations import LocationTags, locations_by_tag from ..options import ExcludeGingerIsland, Shipsanity from ..options import SpecialOrderLocations -from ..stardew_rule import StardewRule, And +from ..stardew_rule import StardewRule from ..strings.ap_names.event_names import Event from ..strings.building_names import Building @@ -35,7 +35,7 @@ def can_ship_everything(self) -> StardewRule: shipsanity_prefix = "Shipsanity: " all_items_to_ship = [] exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true - exclude_qi = self.options.special_order_locations != SpecialOrderLocations.option_board_qi + exclude_qi = not (self.options.special_order_locations & SpecialOrderLocations.value_qi) mod_list = self.options.mods.value for location in locations_by_tag[LocationTags.SHIPSANITY_FULL_SHIPMENT]: if exclude_island and LocationTags.GINGER_ISLAND in location.tags: @@ -57,4 +57,4 @@ def can_ship_everything_in_slot(self, all_location_names_in_slot: List[str]) -> if shipsanity_location.name not in all_location_names_in_slot: continue rules.append(self.logic.region.can_reach_location(shipsanity_location.name)) - return And(*rules) + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/skill_logic.py b/worlds/stardew_valley/logic/skill_logic.py index 35946a0a4d36..4d5567302afe 100644 --- a/worlds/stardew_valley/logic/skill_logic.py +++ b/worlds/stardew_valley/logic/skill_logic.py @@ -4,7 +4,7 @@ from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic from .combat_logic import CombatLogicMixin -from .crop_logic import CropLogicMixin +from .harvesting_logic import HarvestingLogicMixin from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin @@ -12,10 +12,10 @@ from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin from .. import options -from ..data import all_crops +from ..data.harvest import HarvestCropSource from ..mods.logic.magic_logic import MagicLogicMixin from ..mods.logic.mod_skills_levels import get_mod_skill_levels -from ..stardew_rule import StardewRule, True_, Or, False_ +from ..stardew_rule import StardewRule, True_, False_, true_, And from ..strings.craftable_names import Fishing from ..strings.machine_names import Machine from ..strings.performance_names import Performance @@ -23,8 +23,10 @@ from ..strings.region_names import Region from ..strings.skill_names import Skill, all_mod_skills from ..strings.tool_names import ToolMaterial, Tool +from ..strings.wallet_item_names import Wallet fishing_regions = (Region.beach, Region.town, Region.forest, Region.mountain, Region.island_south, Region.island_west) +vanilla_skill_items = ("Farming Level", "Mining Level", "Foraging Level", "Fishing Level", "Combat Level") class SkillLogicMixin(BaseLogicMixin): @@ -34,7 +36,8 @@ def __init__(self, *args, **kwargs): class SkillLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, TimeLogicMixin, ToolLogicMixin, SkillLogicMixin, -CombatLogicMixin, CropLogicMixin, MagicLogicMixin]]): +CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]): + # Should be cached def can_earn_level(self, skill: str, level: int) -> StardewRule: if level <= 0: @@ -48,14 +51,15 @@ def can_earn_level(self, skill: str, level: int) -> StardewRule: if self.options.skill_progression != options.SkillProgression.option_vanilla: previous_level_rule = self.logic.skill.has_level(skill, level - 1) else: - previous_level_rule = True_() + previous_level_rule = true_ if skill == Skill.fishing: - xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 1)) + xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 3)) elif skill == Skill.farming: - xp_rule = self.logic.tool.has_tool(Tool.hoe, tool_material) & self.logic.tool.can_water(tool_level) + xp_rule = self.can_get_farming_xp & self.logic.tool.has_tool(Tool.hoe, tool_material) & self.logic.tool.can_water(tool_level) elif skill == Skill.foraging: - xp_rule = self.logic.tool.has_tool(Tool.axe, tool_material) | self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level) + xp_rule = (self.can_get_foraging_xp & self.logic.tool.has_tool(Tool.axe, tool_material)) |\ + self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level) elif skill == Skill.mining: xp_rule = self.logic.tool.has_tool(Tool.pickaxe, tool_material) | \ self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level) @@ -66,7 +70,7 @@ def can_earn_level(self, skill: str, level: int) -> StardewRule: xp_rule = xp_rule & self.logic.region.can_reach(Region.mines_floor_5) elif skill in all_mod_skills: # Ideal solution would be to add a logic registry, but I'm too lazy. - return self.logic.mod.skill.can_earn_mod_skill_level(skill, level) + return previous_level_rule & months_rule & self.logic.mod.skill.can_earn_mod_skill_level(skill, level) else: raise Exception(f"Unknown skill: {skill}") @@ -77,10 +81,10 @@ def has_level(self, skill: str, level: int) -> StardewRule: if level <= 0: return True_() - if self.options.skill_progression == options.SkillProgression.option_progressive: - return self.logic.received(f"{skill} Level", level) + if self.options.skill_progression == options.SkillProgression.option_vanilla: + return self.logic.skill.can_earn_level(skill, level) - return self.logic.skill.can_earn_level(skill, level) + return self.logic.received(f"{skill} Level", level) @cache_self1 def has_farming_level(self, level: int) -> StardewRule: @@ -91,8 +95,8 @@ def has_total_level(self, level: int, allow_modded_skills: bool = False) -> Star if level <= 0: return True_() - if self.options.skill_progression == options.SkillProgression.option_progressive: - skills_items = ("Farming Level", "Mining Level", "Foraging Level", "Fishing Level", "Combat Level") + if self.options.skill_progression >= options.SkillProgression.option_progressive: + skills_items = vanilla_skill_items if allow_modded_skills: skills_items += get_mod_skill_levels(self.options.mods) return self.logic.received_n(*skills_items, count=level) @@ -104,12 +108,26 @@ def has_total_level(self, level: int, allow_modded_skills: bool = False) -> Star return rule_with_fishing return self.logic.time.has_lived_months(months_with_4_skills) | rule_with_fishing + def has_all_skills_maxed(self, included_modded_skills: bool = True) -> StardewRule: + if self.options.skill_progression == options.SkillProgression.option_vanilla: + return self.has_total_level(50) + skills_items = vanilla_skill_items + if included_modded_skills: + skills_items += get_mod_skill_levels(self.options.mods) + return And(*[self.logic.received(skill, 10) for skill in skills_items]) + + def can_enter_mastery_cave(self) -> StardewRule: + if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries: + return self.logic.received(Wallet.mastery_of_the_five_ways) + return self.has_all_skills_maxed() + @cached_property def can_get_farming_xp(self) -> StardewRule: + sources = self.content.find_sources_of_type(HarvestCropSource) crop_rules = [] - for crop in all_crops: - crop_rules.append(self.logic.crop.can_grow(crop)) - return Or(*crop_rules) + for crop_source in sources: + crop_rules.append(self.logic.harvesting.can_harvest_crop_from(crop_source)) + return self.logic.or_(*crop_rules) @cached_property def can_get_foraging_xp(self) -> StardewRule: @@ -132,7 +150,7 @@ def can_get_combat_xp(self) -> StardewRule: @cached_property def can_get_fishing_xp(self) -> StardewRule: - if self.options.skill_progression == options.SkillProgression.option_progressive: + if self.options.skill_progression >= options.SkillProgression.option_progressive: return self.logic.skill.can_fish() | self.logic.skill.can_crab_pot return self.logic.skill.can_fish() @@ -162,7 +180,7 @@ def can_crab_pot_at(self, region: str) -> StardewRule: @cached_property def can_crab_pot(self) -> StardewRule: crab_pot_rule = self.logic.has(Fishing.bait) - if self.options.skill_progression == options.SkillProgression.option_progressive: + if self.options.skill_progression >= options.SkillProgression.option_progressive: crab_pot_rule = crab_pot_rule & self.logic.has(Machine.crab_pot) else: crab_pot_rule = crab_pot_rule & self.logic.skill.can_get_fishing_xp @@ -178,3 +196,14 @@ def can_forage_quality(self, quality: str) -> StardewRule: if quality == ForageQuality.gold: return self.has_level(Skill.foraging, 9) return False_() + + @cached_property + def can_earn_mastery_experience(self) -> StardewRule: + if self.options.skill_progression != options.SkillProgression.option_progressive_with_masteries: + return self.has_all_skills_maxed() & self.logic.time.has_lived_max_months + return self.logic.time.has_lived_max_months + + def has_mastery(self, skill: str) -> StardewRule: + if self.options.skill_progression != options.SkillProgression.option_progressive_with_masteries: + return self.can_earn_mastery_experience and self.logic.region.can_reach(Region.mastery_cave) + return self.logic.received(f"{skill} Mastery") diff --git a/worlds/stardew_valley/logic/source_logic.py b/worlds/stardew_valley/logic/source_logic.py new file mode 100644 index 000000000000..0e9b8e976f5b --- /dev/null +++ b/worlds/stardew_valley/logic/source_logic.py @@ -0,0 +1,106 @@ +import functools +from typing import Union, Any, Iterable + +from .artisan_logic import ArtisanLogicMixin +from .base_logic import BaseLogicMixin, BaseLogic +from .grind_logic import GrindLogicMixin +from .harvesting_logic import HarvestingLogicMixin +from .has_logic import HasLogicMixin +from .money_logic import MoneyLogicMixin +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from .requirement_logic import RequirementLogicMixin +from .tool_logic import ToolLogicMixin +from ..data.artisan import MachineSource +from ..data.game_item import GenericSource, ItemSource, GameItem, CustomRuleSource +from ..data.harvest import ForagingSource, FruitBatsSource, MushroomCaveSource, SeasonalForagingSource, \ + HarvestCropSource, HarvestFruitTreeSource, ArtifactSpotSource +from ..data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource + + +class SourceLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.source = SourceLogic(*args, **kwargs) + + +class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogicMixin, HarvestingLogicMixin, MoneyLogicMixin, RegionLogicMixin, +ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]): + + def has_access_to_item(self, item: GameItem): + rules = [] + + if self.content.features.cropsanity.is_included(item): + rules.append(self.logic.received(item.name)) + + rules.append(self.logic.source.has_access_to_any(item.sources)) + return self.logic.and_(*rules) + + def has_access_to_any(self, sources: Iterable[ItemSource]): + return self.logic.or_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements) + for source in sources)) + + @functools.singledispatchmethod + def has_access_to(self, source: Any): + raise ValueError(f"Sources of type{type(source)} have no rule registered.") + + @has_access_to.register + def _(self, source: GenericSource): + return self.logic.region.can_reach_any(source.regions) if source.regions else self.logic.true_ + + @has_access_to.register + def _(self, source: CustomRuleSource): + return source.create_rule(self.logic) + + @has_access_to.register + def _(self, source: ForagingSource): + return self.logic.harvesting.can_forage_from(source) + + @has_access_to.register + def _(self, source: SeasonalForagingSource): + # Implementation could be different with some kind of "calendar shuffle" + return self.logic.harvesting.can_forage_from(source.as_foraging_source()) + + @has_access_to.register + def _(self, _: FruitBatsSource): + return self.logic.harvesting.can_harvest_from_fruit_bats + + @has_access_to.register + def _(self, _: MushroomCaveSource): + return self.logic.harvesting.can_harvest_from_mushroom_cave + + @has_access_to.register + def _(self, source: ShopSource): + return self.logic.money.can_shop_from(source) + + @has_access_to.register + def _(self, source: HarvestFruitTreeSource): + return self.logic.harvesting.can_harvest_tree_from(source) + + @has_access_to.register + def _(self, source: HarvestCropSource): + return self.logic.harvesting.can_harvest_crop_from(source) + + @has_access_to.register + def _(self, source: MachineSource): + return self.logic.artisan.can_produce_from(source) + + @has_access_to.register + def _(self, source: MysteryBoxSource): + return self.logic.grind.can_grind_mystery_boxes(source.amount) + + @has_access_to.register + def _(self, source: ArtifactTroveSource): + return self.logic.grind.can_grind_artifact_troves(source.amount) + + @has_access_to.register + def _(self, source: PrizeMachineSource): + return self.logic.grind.can_grind_prize_tickets(source.amount) + + @has_access_to.register + def _(self, source: FishingTreasureChestSource): + return self.logic.grind.can_grind_fishing_treasure_chests(source.amount) + + @has_access_to.register + def _(self, source: ArtifactSpotSource): + return self.logic.grind.can_grind_artifact_spots(source.amount) diff --git a/worlds/stardew_valley/logic/special_order_logic.py b/worlds/stardew_valley/logic/special_order_logic.py index e0b1a7e2fb27..65497df477b8 100644 --- a/worlds/stardew_valley/logic/special_order_logic.py +++ b/worlds/stardew_valley/logic/special_order_logic.py @@ -4,7 +4,6 @@ from .arcade_logic import ArcadeLogicMixin from .artisan_logic import ArtisanLogicMixin from .base_logic import BaseLogicMixin, BaseLogic -from .buff_logic import BuffLogicMixin from .cooking_logic import CookingLogicMixin from .has_logic import HasLogicMixin from .mine_logic import MineLogicMixin @@ -18,7 +17,9 @@ from .skill_logic import SkillLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin -from ..stardew_rule import StardewRule, Has +from ..content.vanilla.ginger_island import ginger_island_content_pack +from ..content.vanilla.qi_board import qi_board_content_pack +from ..stardew_rule import StardewRule, Has, false_ from ..strings.animal_product_names import AnimalProduct from ..strings.ap_names.event_names import Event from ..strings.ap_names.transport_names import Transportation @@ -35,7 +36,6 @@ from ..strings.region_names import Region from ..strings.season_names import Season from ..strings.special_order_names import SpecialOrder -from ..strings.tool_names import Tool from ..strings.villager_names import NPC @@ -47,14 +47,11 @@ def __init__(self, *args, **kwargs): class SpecialOrderLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, TimeLogicMixin, MoneyLogicMixin, ShippingLogicMixin, ArcadeLogicMixin, ArtisanLogicMixin, RelationshipLogicMixin, ToolLogicMixin, SkillLogicMixin, -MineLogicMixin, CookingLogicMixin, BuffLogicMixin, +MineLogicMixin, CookingLogicMixin, AbilityLogicMixin, SpecialOrderLogicMixin, MonsterLogicMixin]]): def initialize_rules(self): self.update_rules({ - SpecialOrder.island_ingredients: self.logic.relationship.can_meet(NPC.caroline) & self.logic.special_order.has_island_transport() & - self.logic.ability.can_farm_perfectly() & self.logic.shipping.can_ship(Vegetable.taro_root) & - self.logic.shipping.can_ship(Fruit.pineapple) & self.logic.shipping.can_ship(Forageable.ginger), SpecialOrder.cave_patrol: self.logic.relationship.can_meet(NPC.clint), SpecialOrder.aquatic_overpopulation: self.logic.relationship.can_meet(NPC.demetrius) & self.logic.ability.can_fish_perfectly(), SpecialOrder.biome_balance: self.logic.relationship.can_meet(NPC.demetrius) & self.logic.ability.can_fish_perfectly(), @@ -66,46 +63,63 @@ def initialize_rules(self): SpecialOrder.gus_famous_omelet: self.logic.has(AnimalProduct.any_egg), SpecialOrder.crop_order: self.logic.ability.can_farm_perfectly() & self.logic.received(Event.can_ship_items), SpecialOrder.community_cleanup: self.logic.skill.can_crab_pot, - SpecialOrder.the_strong_stuff: self.logic.artisan.can_keg(Vegetable.potato), + SpecialOrder.the_strong_stuff: self.logic.has(ArtisanGood.specific_juice(Vegetable.potato)), SpecialOrder.pierres_prime_produce: self.logic.ability.can_farm_perfectly(), SpecialOrder.robins_project: self.logic.relationship.can_meet(NPC.robin) & self.logic.ability.can_chop_perfectly() & self.logic.has(Material.hardwood), SpecialOrder.robins_resource_rush: self.logic.relationship.can_meet(NPC.robin) & self.logic.ability.can_chop_perfectly() & self.logic.has(Fertilizer.tree) & self.logic.ability.can_mine_perfectly(), SpecialOrder.juicy_bugs_wanted: self.logic.has(Loot.bug_meat), - SpecialOrder.tropical_fish: self.logic.relationship.can_meet(NPC.willy) & self.logic.received("Island Resort") & - self.logic.special_order.has_island_transport() & - self.logic.has(Fish.stingray) & self.logic.has(Fish.blue_discus) & self.logic.has(Fish.lionfish), SpecialOrder.a_curious_substance: self.logic.region.can_reach(Region.wizard_tower), SpecialOrder.prismatic_jelly: self.logic.region.can_reach(Region.wizard_tower), - SpecialOrder.qis_crop: self.logic.ability.can_farm_perfectly() & self.logic.region.can_reach(Region.greenhouse) & - self.logic.region.can_reach(Region.island_west) & self.logic.skill.has_total_level(50) & - self.logic.has(Machine.seed_maker) & self.logic.received(Event.can_ship_items), - SpecialOrder.lets_play_a_game: self.logic.arcade.has_junimo_kart_max_level(), - SpecialOrder.four_precious_stones: self.logic.time.has_lived_max_months & self.logic.has("Prismatic Shard") & - self.logic.ability.can_mine_perfectly_in_the_skull_cavern(), - SpecialOrder.qis_hungry_challenge: self.logic.ability.can_mine_perfectly_in_the_skull_cavern() & self.logic.buff.has_max_buffs(), - SpecialOrder.qis_cuisine: self.logic.cooking.can_cook() & self.logic.received(Event.can_ship_items) & - (self.logic.money.can_spend_at(Region.saloon, 205000) | self.logic.money.can_spend_at(Region.pierre_store, 170000)), - SpecialOrder.qis_kindness: self.logic.relationship.can_give_loved_gifts_to_everyone(), - SpecialOrder.extended_family: self.logic.ability.can_fish_perfectly() & self.logic.has(Fish.angler) & self.logic.has(Fish.glacierfish) & - self.logic.has(Fish.crimsonfish) & self.logic.has(Fish.mutant_carp) & self.logic.has(Fish.legend), - SpecialOrder.danger_in_the_deep: self.logic.ability.can_mine_perfectly() & self.logic.mine.has_mine_elevator_to_floor(120), - SpecialOrder.skull_cavern_invasion: self.logic.ability.can_mine_perfectly_in_the_skull_cavern() & self.logic.buff.has_max_buffs(), - SpecialOrder.qis_prismatic_grange: self.logic.has(Loot.bug_meat) & # 100 Bug Meat - self.logic.money.can_spend_at(Region.saloon, 24000) & # 100 Spaghetti - self.logic.money.can_spend_at(Region.blacksmith, 15000) & # 100 Copper Ore - self.logic.money.can_spend_at(Region.ranch, 5000) & # 100 Hay - self.logic.money.can_spend_at(Region.saloon, 22000) & # 100 Salads - self.logic.money.can_spend_at(Region.saloon, 7500) & # 100 Joja Cola - self.logic.money.can_spend(80000), # I need this extra rule because money rules aren't additive... + }) + if ginger_island_content_pack.name in self.content.registered_packs: + self.update_rules({ + SpecialOrder.island_ingredients: self.logic.relationship.can_meet(NPC.caroline) & self.logic.special_order.has_island_transport() & + self.logic.ability.can_farm_perfectly() & self.logic.shipping.can_ship(Vegetable.taro_root) & + self.logic.shipping.can_ship(Fruit.pineapple) & self.logic.shipping.can_ship(Forageable.ginger), + SpecialOrder.tropical_fish: self.logic.relationship.can_meet(NPC.willy) & self.logic.received("Island Resort") & + self.logic.special_order.has_island_transport() & + self.logic.has(Fish.stingray) & self.logic.has(Fish.blue_discus) & self.logic.has(Fish.lionfish), + }) + else: + self.update_rules({ + SpecialOrder.island_ingredients: false_, + SpecialOrder.tropical_fish: false_, + }) + + if qi_board_content_pack.name in self.content.registered_packs: + self.update_rules({ + SpecialOrder.qis_crop: self.logic.ability.can_farm_perfectly() & self.logic.region.can_reach(Region.greenhouse) & + self.logic.region.can_reach(Region.island_west) & self.logic.skill.has_total_level(50) & + self.logic.has(Machine.seed_maker) & self.logic.received(Event.can_ship_items), + SpecialOrder.lets_play_a_game: self.logic.arcade.has_junimo_kart_max_level(), + SpecialOrder.four_precious_stones: self.logic.time.has_lived_max_months & self.logic.has("Prismatic Shard") & + self.logic.ability.can_mine_perfectly_in_the_skull_cavern(), + SpecialOrder.qis_hungry_challenge: self.logic.ability.can_mine_perfectly_in_the_skull_cavern(), + SpecialOrder.qis_cuisine: self.logic.cooking.can_cook() & self.logic.received(Event.can_ship_items) & + (self.logic.money.can_spend_at(Region.saloon, 205000) | self.logic.money.can_spend_at(Region.pierre_store, 170000)), + SpecialOrder.qis_kindness: self.logic.relationship.can_give_loved_gifts_to_everyone(), + SpecialOrder.extended_family: self.logic.ability.can_fish_perfectly() & self.logic.has(Fish.angler) & self.logic.has(Fish.glacierfish) & + self.logic.has(Fish.crimsonfish) & self.logic.has(Fish.mutant_carp) & self.logic.has(Fish.legend), + SpecialOrder.danger_in_the_deep: self.logic.ability.can_mine_perfectly() & self.logic.mine.has_mine_elevator_to_floor(120), + SpecialOrder.skull_cavern_invasion: self.logic.ability.can_mine_perfectly_in_the_skull_cavern(), + SpecialOrder.qis_prismatic_grange: self.logic.has(Loot.bug_meat) & # 100 Bug Meat + self.logic.money.can_spend_at(Region.saloon, 24000) & # 100 Spaghetti + self.logic.money.can_spend_at(Region.blacksmith, 15000) & # 100 Copper Ore + self.logic.money.can_spend_at(Region.ranch, 5000) & # 100 Hay + self.logic.money.can_spend_at(Region.saloon, 22000) & # 100 Salads + self.logic.money.can_spend_at(Region.saloon, 7500) & # 100 Joja Cola + self.logic.money.can_spend(80000), # I need this extra rule because money rules aren't additive...) + }) + def update_rules(self, new_rules: Dict[str, StardewRule]): self.registry.special_order_rules.update(new_rules) def can_complete_special_order(self, special_order: str) -> StardewRule: - return Has(special_order, self.registry.special_order_rules) + return Has(special_order, self.registry.special_order_rules, "special order") def has_island_transport(self) -> StardewRule: return self.logic.received(Transportation.island_obelisk) | self.logic.received(Transportation.boat_repair) diff --git a/worlds/stardew_valley/logic/time_logic.py b/worlds/stardew_valley/logic/time_logic.py index 9dcebfe82a4f..94e0e277c86c 100644 --- a/worlds/stardew_valley/logic/time_logic.py +++ b/worlds/stardew_valley/logic/time_logic.py @@ -1,38 +1,52 @@ -from functools import cached_property -from typing import Union - -from Utils import cache_self1 -from .base_logic import BaseLogic, BaseLogicMixin -from .received_logic import ReceivedLogicMixin -from ..stardew_rule import StardewRule, HasProgressionPercent, True_ - -MAX_MONTHS = 12 -MONTH_COEFFICIENT = 24 // MAX_MONTHS - - -class TimeLogicMixin(BaseLogicMixin): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.time = TimeLogic(*args, **kwargs) - - -class TimeLogic(BaseLogic[Union[TimeLogicMixin, ReceivedLogicMixin]]): - - @cache_self1 - def has_lived_months(self, number: int) -> StardewRule: - if number <= 0: - return True_() - number = min(number, MAX_MONTHS) - return HasProgressionPercent(self.player, number * MONTH_COEFFICIENT) - - @cached_property - def has_lived_max_months(self) -> StardewRule: - return self.logic.time.has_lived_months(MAX_MONTHS) - - @cached_property - def has_year_two(self) -> StardewRule: - return self.logic.time.has_lived_months(4) - - @cached_property - def has_year_three(self) -> StardewRule: - return self.logic.time.has_lived_months(8) +from functools import cached_property +from typing import Union + +from Utils import cache_self1 +from .base_logic import BaseLogic, BaseLogicMixin +from .has_logic import HasLogicMixin +from ..stardew_rule import StardewRule, HasProgressionPercent + +ONE_YEAR = 4 +MAX_MONTHS = 3 * ONE_YEAR +PERCENT_REQUIRED_FOR_MAX_MONTHS = 48 +MONTH_COEFFICIENT = PERCENT_REQUIRED_FOR_MAX_MONTHS // MAX_MONTHS + +MIN_ITEMS = 10 +MAX_ITEMS = 999 +PERCENT_REQUIRED_FOR_MAX_ITEM = 24 + + +class TimeLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.time = TimeLogic(*args, **kwargs) + + +class TimeLogic(BaseLogic[Union[TimeLogicMixin, HasLogicMixin]]): + + @cache_self1 + def has_lived_months(self, number: int) -> StardewRule: + if number <= 0: + return self.logic.true_ + number = min(number, MAX_MONTHS) + return HasProgressionPercent(self.player, number * MONTH_COEFFICIENT) + + @cached_property + def has_lived_max_months(self) -> StardewRule: + return self.logic.time.has_lived_months(MAX_MONTHS) + + @cache_self1 + def has_lived_year(self, number: int) -> StardewRule: + return self.logic.time.has_lived_months(number * ONE_YEAR) + + @cache_self1 + def has_year(self, number: int) -> StardewRule: + return self.logic.time.has_lived_year(number - 1) + + @cached_property + def has_year_two(self) -> StardewRule: + return self.logic.time.has_year(2) + + @cached_property + def has_year_three(self) -> StardewRule: + return self.logic.time.has_year(3) diff --git a/worlds/stardew_valley/logic/tool_logic.py b/worlds/stardew_valley/logic/tool_logic.py index 1b1dc2a52120..ba593c085ae4 100644 --- a/worlds/stardew_valley/logic/tool_logic.py +++ b/worlds/stardew_valley/logic/tool_logic.py @@ -1,4 +1,4 @@ -from typing import Union, Iterable +from typing import Union, Iterable, Tuple from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic @@ -42,9 +42,17 @@ def __init__(self, *args, **kwargs): class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, MoneyLogicMixin, MagicLogicMixin]]): + + def has_all_tools(self, tools: Iterable[Tuple[str, str]]): + return self.logic.and_(*(self.logic.tool.has_tool(tool, material) for tool, material in tools)) + # Should be cached def has_tool(self, tool: str, material: str = ToolMaterial.basic) -> StardewRule: - assert tool != Tool.fishing_rod, "Use `has_fishing_rod` instead of `has_tool`." + if tool == Tool.fishing_rod: + return self.logic.tool.has_fishing_rod(tool_materials[material]) + + if tool == Tool.pan and material == ToolMaterial.basic: + material = ToolMaterial.copper # The first Pan is the copper one, so the basic one does not exist if material == ToolMaterial.basic or tool == Tool.scythe: return True_() @@ -52,7 +60,14 @@ def has_tool(self, tool: str, material: str = ToolMaterial.basic) -> StardewRule if self.options.tool_progression & ToolProgression.option_progressive: return self.logic.received(f"Progressive {tool}", tool_materials[material]) - return self.logic.has(f"{material} Bar") & self.logic.money.can_spend_at(Region.blacksmith, tool_upgrade_prices[material]) + can_upgrade_rule = self.logic.has(f"{material} Bar") & self.logic.money.can_spend_at(Region.blacksmith, tool_upgrade_prices[material]) + if tool == Tool.pan: + has_base_pan = self.logic.received("Glittering Boulder Removed") & self.logic.region.can_reach(Region.mountain) + if material == ToolMaterial.copper: + return has_base_pan + return has_base_pan & can_upgrade_rule + + return can_upgrade_rule def can_use_tool_at(self, tool: str, material: str, region: str) -> StardewRule: return self.has_tool(tool, material) & self.logic.region.can_reach(region) diff --git a/worlds/stardew_valley/mods/logic/deepwoods_logic.py b/worlds/stardew_valley/mods/logic/deepwoods_logic.py index 7699521542a7..26704eb7d11b 100644 --- a/worlds/stardew_valley/mods/logic/deepwoods_logic.py +++ b/worlds/stardew_valley/mods/logic/deepwoods_logic.py @@ -10,13 +10,13 @@ from ...logic.tool_logic import ToolLogicMixin from ...mods.mod_data import ModNames from ...options import ElevatorProgression -from ...stardew_rule import StardewRule, True_, And, true_ -from ...strings.ap_names.mods.mod_items import DeepWoodsItem, SkillLevel +from ...stardew_rule import StardewRule, True_, true_ +from ...strings.ap_names.mods.mod_items import DeepWoodsItem from ...strings.ap_names.transport_names import ModTransportation from ...strings.craftable_names import Bomb from ...strings.food_names import Meal from ...strings.performance_names import Performance -from ...strings.skill_names import Skill +from ...strings.skill_names import Skill, ModSkill from ...strings.tool_names import Tool, ToolMaterial @@ -45,11 +45,11 @@ def can_reach_woods_depth(self, depth: int) -> StardewRule: self.logic.received(ModTransportation.woods_obelisk)) tier = int(depth / 25) + 1 - if self.options.skill_progression == options.SkillProgression.option_progressive: + if self.options.skill_progression >= options.SkillProgression.option_progressive: combat_tier = min(10, max(0, tier + 5)) rules.append(self.logic.skill.has_level(Skill.combat, combat_tier)) - return And(*rules) + return self.logic.and_(*rules) def has_woods_rune_to_depth(self, floor: int) -> StardewRule: if self.options.elevator_progression == ElevatorProgression.option_vanilla: @@ -66,8 +66,8 @@ def can_pull_sword(self) -> StardewRule: self.logic.received(DeepWoodsItem.pendant_elder), self.logic.skill.has_total_level(40)] if ModNames.luck_skill in self.options.mods: - rules.append(self.logic.received(SkillLevel.luck, 7)) + rules.append(self.logic.skill.has_level(ModSkill.luck, 7)) else: rules.append( self.logic.has(Meal.magic_rock_candy)) # You need more luck than this, but it'll push the logic down a ways; you can get the rest there. - return And(*rules) + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/mods/logic/item_logic.py b/worlds/stardew_valley/mods/logic/item_logic.py index 8f5e676d8c2d..cfafc88e83f5 100644 --- a/worlds/stardew_valley/mods/logic/item_logic.py +++ b/worlds/stardew_valley/mods/logic/item_logic.py @@ -7,7 +7,7 @@ from ...logic.combat_logic import CombatLogicMixin from ...logic.cooking_logic import CookingLogicMixin from ...logic.crafting_logic import CraftingLogicMixin -from ...logic.crop_logic import CropLogicMixin +from ...logic.farming_logic import FarmingLogicMixin from ...logic.fishing_logic import FishingLogicMixin from ...logic.has_logic import HasLogicMixin from ...logic.money_logic import MoneyLogicMixin @@ -24,11 +24,10 @@ from ...stardew_rule import StardewRule, True_ from ...strings.artisan_good_names import ModArtisanGood from ...strings.craftable_names import ModCraftable, ModEdible, ModMachine -from ...strings.crop_names import SVEVegetable, SVEFruit, DistantLandsCrop, Fruit -from ...strings.fish_names import WaterItem -from ...strings.flower_names import Flower +from ...strings.crop_names import SVEVegetable, SVEFruit, DistantLandsCrop +from ...strings.fish_names import ModTrash, SVEFish from ...strings.food_names import SVEMeal, SVEBeverage -from ...strings.forageable_names import SVEForage, DistantLandsForageable, Forageable +from ...strings.forageable_names import SVEForage, DistantLandsForageable from ...strings.gift_names import SVEGift from ...strings.ingredient_names import Ingredient from ...strings.material_names import Material @@ -53,8 +52,9 @@ def __init__(self, *args, **kwargs): self.item = ModItemLogic(*args, **kwargs) -class ModItemLogic(BaseLogic[Union[CombatLogicMixin, ReceivedLogicMixin, CropLogicMixin, CookingLogicMixin, FishingLogicMixin, HasLogicMixin, MoneyLogicMixin, -RegionLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MuseumLogicMixin, ToolLogicMixin, CraftingLogicMixin, SkillLogicMixin, TimeLogicMixin, QuestLogicMixin]]): +class ModItemLogic(BaseLogic[Union[CombatLogicMixin, ReceivedLogicMixin, CookingLogicMixin, FishingLogicMixin, HasLogicMixin, MoneyLogicMixin, +RegionLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MuseumLogicMixin, ToolLogicMixin, CraftingLogicMixin, SkillLogicMixin, TimeLogicMixin, QuestLogicMixin, +FarmingLogicMixin]]): def get_modded_item_rules(self) -> Dict[str, StardewRule]: items = dict() @@ -78,53 +78,53 @@ def modify_vanilla_item_rules_with_mod_additions(self, item_rule: Dict[str, Star def get_sve_item_rules(self): return {SVEGift.aged_blue_moon_wine: self.logic.money.can_spend_at(SVERegion.sophias_house, 28000), SVEGift.blue_moon_wine: self.logic.money.can_spend_at(SVERegion.sophias_house, 3000), - SVESeed.fungus_seed: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, + SVESeed.fungus: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, ModLoot.green_mushroom: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.tool.has_tool(Tool.axe, ToolMaterial.iron) & self.logic.season.has_any_not_winter(), - SVEFruit.monster_fruit: self.logic.season.has(Season.summer) & self.logic.has(SVESeed.stalk_seed), - SVEVegetable.monster_mushroom: self.logic.season.has(Season.fall) & self.logic.has(SVESeed.fungus_seed), - SVEForage.ornate_treasure_chest: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_galaxy_weapon & + SVEFruit.monster_fruit: self.logic.season.has(Season.summer) & self.logic.has(SVESeed.stalk), + SVEVegetable.monster_mushroom: self.logic.season.has(Season.fall) & self.logic.has(SVESeed.fungus), + ModLoot.ornate_treasure_chest: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_galaxy_weapon & self.logic.tool.has_tool(Tool.axe, ToolMaterial.iron), - SVEFruit.slime_berry: self.logic.season.has(Season.spring) & self.logic.has(SVESeed.slime_seed), - SVESeed.slime_seed: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, - SVESeed.stalk_seed: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, - SVEForage.swirl_stone: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - SVEVegetable.void_root: self.logic.season.has(Season.winter) & self.logic.has(SVESeed.void_seed), - SVESeed.void_seed: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, - SVEForage.void_soul: self.logic.region.can_reach( + SVEFruit.slime_berry: self.logic.season.has(Season.spring) & self.logic.has(SVESeed.slime), + SVESeed.slime: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, + SVESeed.stalk: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, + ModLoot.swirl_stone: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, + SVEVegetable.void_root: self.logic.season.has(Season.winter) & self.logic.has(SVESeed.void), + SVESeed.void: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, + ModLoot.void_soul: self.logic.region.can_reach( SVERegion.crimson_badlands) & self.logic.combat.has_good_weapon & self.logic.cooking.can_cook(), SVEForage.winter_star_rose: self.logic.region.can_reach(SVERegion.summit) & self.logic.season.has(Season.winter), - SVEForage.bearberrys: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has(Season.winter), + SVEForage.bearberry: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has(Season.winter), SVEForage.poison_mushroom: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has_any([Season.summer, Season.fall]), SVEForage.red_baneberry: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has(Season.summer), SVEForage.ferngill_primrose: self.logic.region.can_reach(SVERegion.summit) & self.logic.season.has(Season.spring), SVEForage.goldenrod: self.logic.region.can_reach(SVERegion.summit) & ( self.logic.season.has(Season.summer) | self.logic.season.has(Season.fall)), - SVESeed.shrub_seed: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), - SVEFruit.salal_berry: self.logic.crop.can_plant_and_grow_item([Season.spring, Season.summer]) & self.logic.has(SVESeed.shrub_seed), + SVESeed.shrub: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), + SVEFruit.salal_berry: self.logic.farming.can_plant_and_grow_item((Season.spring, Season.summer)) & self.logic.has(SVESeed.shrub), ModEdible.aegis_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 28000), ModEdible.lightning_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 12000), ModEdible.barbarian_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 22000), ModEdible.gravity_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 4000), - SVESeed.ancient_ferns_seed: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), - SVEVegetable.ancient_fiber: self.logic.crop.can_plant_and_grow_item(Season.summer) & self.logic.has(SVESeed.ancient_ferns_seed), - SVEForage.big_conch: self.logic.region.can_reach_any((Region.beach, SVERegion.fable_reef)), + SVESeed.ancient_fern: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), + SVEVegetable.ancient_fiber: self.logic.farming.can_plant_and_grow_item(Season.summer) & self.logic.has(SVESeed.ancient_fern), + SVEForage.conch: self.logic.region.can_reach_any((Region.beach, SVERegion.fable_reef)), SVEForage.dewdrop_berry: self.logic.region.can_reach(SVERegion.enchanted_grove), - SVEForage.dried_sand_dollar: self.logic.region.can_reach(SVERegion.fable_reef) | (self.logic.region.can_reach(Region.beach) & - self.logic.season.has_any([Season.summer, Season.fall])), + SVEForage.sand_dollar: self.logic.region.can_reach(SVERegion.fable_reef) | (self.logic.region.can_reach(Region.beach) & + self.logic.season.has_any([Season.summer, Season.fall])), SVEForage.golden_ocean_flower: self.logic.region.can_reach(SVERegion.fable_reef), SVEMeal.grampleton_orange_chicken: self.logic.money.can_spend_at(Region.saloon, 650) & self.logic.relationship.has_hearts(ModNPC.sophia, 6), ModEdible.hero_elixir: self.logic.money.can_spend_at(SVERegion.isaac_shop, 8000), - SVEForage.lucky_four_leaf_clover: self.logic.region.can_reach_any((Region.secret_woods, SVERegion.forest_west)) & - self.logic.season.has_any([Season.spring, Season.summer]), + SVEForage.four_leaf_clover: self.logic.region.can_reach_any((Region.secret_woods, SVERegion.forest_west)) & + self.logic.season.has_any([Season.spring, Season.summer]), SVEForage.mushroom_colony: self.logic.region.can_reach_any((Region.secret_woods, SVERegion.junimo_woods, SVERegion.forest_west)) & self.logic.season.has(Season.fall), SVEForage.rusty_blade: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - SVEForage.smelly_rafflesia: self.logic.region.can_reach(Region.secret_woods), + SVEForage.rafflesia: self.logic.region.can_reach(Region.secret_woods), SVEBeverage.sports_drink: self.logic.money.can_spend_at(Region.hospital, 750), "Stamina Capsule": self.logic.money.can_spend_at(Region.hospital, 4000), SVEForage.thistle: self.logic.region.can_reach(SVERegion.summit), - SVEForage.void_pebble: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, + ModLoot.void_pebble: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, ModLoot.void_shard: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_galaxy_weapon & self.logic.skill.has_level(Skill.combat, 10) & self.logic.region.can_reach(Region.saloon) & self.logic.time.has_year_three } @@ -135,49 +135,17 @@ def get_modified_item_rules_for_sve(self, items: Dict[str, StardewRule]): Loot.void_essence: items[Loot.void_essence] | self.logic.region.can_reach(SVERegion.highlands_cavern) | self.logic.region.can_reach( SVERegion.crimson_badlands), Loot.solar_essence: items[Loot.solar_essence] | self.logic.region.can_reach(SVERegion.crimson_badlands), - Flower.tulip: items[Flower.tulip] | self.logic.tool.can_forage(Season.spring, SVERegion.sprite_spring), - Flower.blue_jazz: items[Flower.blue_jazz] | self.logic.tool.can_forage(Season.spring, SVERegion.sprite_spring), - Flower.summer_spangle: items[Flower.summer_spangle] | self.logic.tool.can_forage(Season.summer, SVERegion.sprite_spring), - Flower.sunflower: items[Flower.sunflower] | self.logic.tool.can_forage((Season.summer, Season.fall), SVERegion.sprite_spring), - Flower.fairy_rose: items[Flower.fairy_rose] | self.logic.tool.can_forage(Season.fall, SVERegion.sprite_spring), - Fruit.ancient_fruit: items[Fruit.ancient_fruit] | ( - self.logic.tool.can_forage((Season.spring, Season.summer, Season.fall), SVERegion.sprite_spring) & - self.logic.time.has_year_three) | self.logic.region.can_reach(SVERegion.sprite_spring_cave), - Fruit.sweet_gem_berry: items[Fruit.sweet_gem_berry] | ( - self.logic.tool.can_forage((Season.spring, Season.summer, Season.fall), SVERegion.sprite_spring) & - self.logic.time.has_year_three), - WaterItem.coral: items[WaterItem.coral] | self.logic.region.can_reach(SVERegion.fable_reef), - Forageable.rainbow_shell: items[Forageable.rainbow_shell] | self.logic.region.can_reach(SVERegion.fable_reef), - WaterItem.sea_urchin: items[WaterItem.sea_urchin] | self.logic.region.can_reach(SVERegion.fable_reef), - Forageable.red_mushroom: items[Forageable.red_mushroom] | self.logic.tool.can_forage((Season.summer, Season.fall), SVERegion.forest_west) | - self.logic.region.can_reach(SVERegion.sprite_spring_cave), - Forageable.purple_mushroom: items[Forageable.purple_mushroom] | self.logic.tool.can_forage(Season.fall, SVERegion.forest_west) | - self.logic.region.can_reach(SVERegion.sprite_spring_cave), - Forageable.morel: items[Forageable.morel] | self.logic.tool.can_forage(Season.fall, SVERegion.forest_west), - Forageable.chanterelle: items[Forageable.chanterelle] | self.logic.tool.can_forage(Season.fall, SVERegion.forest_west) | - self.logic.region.can_reach(SVERegion.sprite_spring_cave), Ore.copper: items[Ore.copper] | (self.logic.tool.can_use_tool_at(Tool.pickaxe, ToolMaterial.basic, SVERegion.highlands_cavern) & self.logic.combat.can_fight_at_level(Performance.great)), Ore.iron: items[Ore.iron] | (self.logic.tool.can_use_tool_at(Tool.pickaxe, ToolMaterial.basic, SVERegion.highlands_cavern) & self.logic.combat.can_fight_at_level(Performance.great)), Ore.iridium: items[Ore.iridium] | (self.logic.tool.can_use_tool_at(Tool.pickaxe, ToolMaterial.basic, SVERegion.crimson_badlands) & self.logic.combat.can_fight_at_level(Performance.maximum)), + SVEFish.dulse_seaweed: self.logic.fishing.can_fish_at(Region.beach) & self.logic.season.has_any([Season.spring, Season.summer, Season.winter]) } def get_modified_item_rules_for_deep_woods(self, items: Dict[str, StardewRule]): options_to_update = { - Fruit.apple: items[Fruit.apple] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), # Deep enough to have seen such a tree at least once - Fruit.apricot: items[Fruit.apricot] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Fruit.cherry: items[Fruit.cherry] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Fruit.orange: items[Fruit.orange] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Fruit.peach: items[Fruit.peach] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Fruit.pomegranate: items[Fruit.pomegranate] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Fruit.mango: items[Fruit.mango] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Flower.tulip: items[Flower.tulip] | self.logic.tool.can_forage(Season.not_winter, DeepWoodsRegion.floor_10), - Flower.blue_jazz: items[Flower.blue_jazz] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Flower.summer_spangle: items[Flower.summer_spangle] | self.logic.tool.can_forage(Season.not_winter, DeepWoodsRegion.floor_10), - Flower.poppy: items[Flower.poppy] | self.logic.tool.can_forage(Season.not_winter, DeepWoodsRegion.floor_10), - Flower.fairy_rose: items[Flower.fairy_rose] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), Material.hardwood: items[Material.hardwood] | self.logic.tool.can_use_tool_at(Tool.axe, ToolMaterial.iron, DeepWoodsRegion.floor_10), Ingredient.sugar: items[Ingredient.sugar] | self.logic.tool.can_use_tool_at(Tool.axe, ToolMaterial.gold, DeepWoodsRegion.floor_50), # Gingerbread House @@ -207,6 +175,7 @@ def get_archaeology_item_rules(self): archaeology_item_rules[location_name] = display_item_rule & preservation_chamber_rule else: archaeology_item_rules[location_name] = display_item_rule & hardwood_preservation_chamber_rule + archaeology_item_rules[ModTrash.rusty_scrap] = self.logic.has(ModMachine.grinder) & self.logic.has_any(*all_artifacts) return archaeology_item_rules def get_distant_lands_item_rules(self): diff --git a/worlds/stardew_valley/mods/logic/mod_skills_levels.py b/worlds/stardew_valley/mods/logic/mod_skills_levels.py index 18402283857b..32b3368a8c8b 100644 --- a/worlds/stardew_valley/mods/logic/mod_skills_levels.py +++ b/worlds/stardew_valley/mods/logic/mod_skills_levels.py @@ -2,20 +2,21 @@ from ...mods.mod_data import ModNames from ...options import Mods +from ...strings.ap_names.mods.mod_items import SkillLevel def get_mod_skill_levels(mods: Mods) -> Tuple[str]: skills_items = [] if ModNames.luck_skill in mods: - skills_items.append("Luck Level") + skills_items.append(SkillLevel.luck) if ModNames.socializing_skill in mods: - skills_items.append("Socializing Level") + skills_items.append(SkillLevel.socializing) if ModNames.magic in mods: - skills_items.append("Magic Level") + skills_items.append(SkillLevel.magic) if ModNames.archaeology in mods: - skills_items.append("Archaeology Level") + skills_items.append(SkillLevel.archaeology) if ModNames.binning_skill in mods: - skills_items.append("Binning Level") + skills_items.append(SkillLevel.binning) if ModNames.cooking_skill in mods: - skills_items.append("Cooking Level") + skills_items.append(SkillLevel.cooking) return tuple(skills_items) diff --git a/worlds/stardew_valley/mods/logic/quests_logic.py b/worlds/stardew_valley/mods/logic/quests_logic.py index 40b5545ee39f..1aa71404ae51 100644 --- a/worlds/stardew_valley/mods/logic/quests_logic.py +++ b/worlds/stardew_valley/mods/logic/quests_logic.py @@ -19,7 +19,7 @@ from ...strings.forageable_names import SVEForage from ...strings.material_names import Material from ...strings.metal_names import Ore, MetalBar -from ...strings.monster_drop_names import Loot +from ...strings.monster_drop_names import Loot, ModLoot from ...strings.monster_names import Monster from ...strings.quest_names import Quest, ModQuest from ...strings.region_names import Region, SVERegion, BoardingHouseRegion @@ -86,7 +86,7 @@ def _get_sve_quest_rules(self): self.logic.relationship.can_meet(ModNPC.lance) & self.logic.region.can_reach(SVERegion.guild_summit), ModQuest.AuroraVineyard: self.logic.has(Fruit.starfruit) & self.logic.region.can_reach(SVERegion.aurora_vineyard), ModQuest.MonsterCrops: self.logic.has_all(*(SVEVegetable.monster_mushroom, SVEFruit.slime_berry, SVEFruit.monster_fruit, SVEVegetable.void_root)), - ModQuest.VoidSoul: self.logic.has(SVEForage.void_soul) & self.logic.region.can_reach(Region.farm) & + ModQuest.VoidSoul: self.logic.has(ModLoot.void_soul) & self.logic.region.can_reach(Region.farm) & self.logic.season.has_any_not_winter() & self.logic.region.can_reach(SVERegion.badlands_entrance) & self.logic.relationship.has_hearts(NPC.krobus, 10) & self.logic.quest.can_complete_quest(ModQuest.MonsterCrops) & self.logic.monster.can_kill_any((Monster.shadow_brute, Monster.shadow_shaman, Monster.shadow_sniper)), diff --git a/worlds/stardew_valley/mods/logic/skills_logic.py b/worlds/stardew_valley/mods/logic/skills_logic.py index ce8bebbffef5..cb12274dc651 100644 --- a/worlds/stardew_valley/mods/logic/skills_logic.py +++ b/worlds/stardew_valley/mods/logic/skills_logic.py @@ -1,11 +1,11 @@ from typing import Union from .magic_logic import MagicLogicMixin -from ...data.villagers_data import all_villagers from ...logic.action_logic import ActionLogicMixin from ...logic.base_logic import BaseLogicMixin, BaseLogic from ...logic.building_logic import BuildingLogicMixin from ...logic.cooking_logic import CookingLogicMixin +from ...logic.crafting_logic import CraftingLogicMixin from ...logic.fishing_logic import FishingLogicMixin from ...logic.has_logic import HasLogicMixin from ...logic.received_logic import ReceivedLogicMixin @@ -14,10 +14,9 @@ from ...logic.tool_logic import ToolLogicMixin from ...mods.mod_data import ModNames from ...options import SkillProgression -from ...stardew_rule import StardewRule, False_, True_ -from ...strings.ap_names.mods.mod_items import SkillLevel -from ...strings.craftable_names import ModCraftable, ModMachine +from ...stardew_rule import StardewRule, False_, True_, And from ...strings.building_names import Building +from ...strings.craftable_names import ModCraftable, ModMachine from ...strings.geode_names import Geode from ...strings.machine_names import Machine from ...strings.region_names import Region @@ -33,7 +32,7 @@ def __init__(self, *args, **kwargs): class ModSkillLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, ActionLogicMixin, RelationshipLogicMixin, BuildingLogicMixin, -ToolLogicMixin, FishingLogicMixin, CookingLogicMixin, MagicLogicMixin]]): +ToolLogicMixin, FishingLogicMixin, CookingLogicMixin, CraftingLogicMixin, MagicLogicMixin]]): def has_mod_level(self, skill: str, level: int) -> StardewRule: if level <= 0: return True_() @@ -77,9 +76,10 @@ def can_earn_magic_skill_level(self, level: int) -> StardewRule: def can_earn_socializing_skill_level(self, level: int) -> StardewRule: villager_count = [] - for villager in all_villagers: - if villager.mod_name in self.options.mods or villager.mod_name is None: - villager_count.append(self.logic.relationship.can_earn_relationship(villager.name, level)) + + for villager in self.content.villagers.values(): + villager_count.append(self.logic.relationship.can_earn_relationship(villager.name, level)) + return self.logic.count(level * 2, *villager_count) def can_earn_archaeology_skill_level(self, level: int) -> StardewRule: @@ -89,12 +89,12 @@ def can_earn_archaeology_skill_level(self, level: int) -> StardewRule: shifter_rule = self.logic.has(ModCraftable.water_shifter) preservation_rule = self.logic.has(ModMachine.hardwood_preservation_chamber) if level >= 8: - return (self.logic.action.can_pan() & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.gold)) & shifter_rule & preservation_rule + return (self.logic.tool.has_tool(Tool.pan, ToolMaterial.iridium) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.gold)) & shifter_rule & preservation_rule if level >= 5: - return (self.logic.action.can_pan() & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.iron)) & shifter_rule + return (self.logic.tool.has_tool(Tool.pan, ToolMaterial.gold) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.iron)) & shifter_rule if level >= 3: - return self.logic.action.can_pan() | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.copper) - return self.logic.action.can_pan() | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic) + return self.logic.tool.has_tool(Tool.pan, ToolMaterial.iron) | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.copper) + return self.logic.tool.has_tool(Tool.pan, ToolMaterial.copper) | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic) def can_earn_cooking_skill_level(self, level: int) -> StardewRule: if level >= 6: @@ -104,7 +104,13 @@ def can_earn_cooking_skill_level(self, level: int) -> StardewRule: return self.logic.cooking.can_cook() def can_earn_binning_skill_level(self, level: int) -> StardewRule: - if level >= 6: - return self.logic.has(Machine.recycling_machine) - else: - return True_() # You can always earn levels 1-5 with trash cans + if level <= 2: + return True_() + binning_rule = [self.logic.has(ModMachine.trash_bin) & self.logic.has(Machine.recycling_machine)] + if level > 4: + binning_rule.append(self.logic.has(ModMachine.composter)) + if level > 7: + binning_rule.append(self.logic.has(ModMachine.recycling_bin)) + if level > 9: + binning_rule.append(self.logic.has(ModMachine.advanced_recycling_machine)) + return And(*binning_rule) diff --git a/worlds/stardew_valley/mods/logic/special_orders_logic.py b/worlds/stardew_valley/mods/logic/special_orders_logic.py index e51a23d50254..1a0934282e09 100644 --- a/worlds/stardew_valley/mods/logic/special_orders_logic.py +++ b/worlds/stardew_valley/mods/logic/special_orders_logic.py @@ -1,12 +1,11 @@ from typing import Union -from ...data.craftable_data import all_crafting_recipes_by_name from ..mod_data import ModNames +from ...data.craftable_data import all_crafting_recipes_by_name from ...logic.action_logic import ActionLogicMixin from ...logic.artisan_logic import ArtisanLogicMixin from ...logic.base_logic import BaseLogicMixin, BaseLogic from ...logic.crafting_logic import CraftingLogicMixin -from ...logic.crop_logic import CropLogicMixin from ...logic.has_logic import HasLogicMixin from ...logic.received_logic import ReceivedLogicMixin from ...logic.region_logic import RegionLogicMixin @@ -34,7 +33,7 @@ def __init__(self, *args, **kwargs): self.special_order = ModSpecialOrderLogic(*args, **kwargs) -class ModSpecialOrderLogic(BaseLogic[Union[ActionLogicMixin, ArtisanLogicMixin, CraftingLogicMixin, CropLogicMixin, HasLogicMixin, RegionLogicMixin, +class ModSpecialOrderLogic(BaseLogic[Union[ActionLogicMixin, ArtisanLogicMixin, CraftingLogicMixin, HasLogicMixin, RegionLogicMixin, ReceivedLogicMixin, RelationshipLogicMixin, SeasonLogicMixin, WalletLogicMixin]]): def get_modded_special_orders_rules(self): special_orders = {} @@ -54,7 +53,7 @@ def get_modded_special_orders_rules(self): self.logic.region.can_reach(SVERegion.fairhaven_farm), ModSpecialOrder.a_mysterious_venture: self.logic.has(Bomb.cherry_bomb) & self.logic.has(Bomb.bomb) & self.logic.has(Bomb.mega_bomb) & self.logic.region.can_reach(Region.adventurer_guild), - ModSpecialOrder.an_elegant_reception: self.logic.artisan.can_keg(Fruit.starfruit) & self.logic.has(ArtisanGood.cheese) & + ModSpecialOrder.an_elegant_reception: self.logic.has(ArtisanGood.specific_wine(Fruit.starfruit)) & self.logic.has(ArtisanGood.cheese) & self.logic.has(ArtisanGood.goat_cheese) & self.logic.season.has_any_not_winter() & self.logic.region.can_reach(SVERegion.jenkins_cellar), ModSpecialOrder.fairy_garden: self.logic.has(Consumable.fairy_dust) & diff --git a/worlds/stardew_valley/mods/logic/sve_logic.py b/worlds/stardew_valley/mods/logic/sve_logic.py index 1254338fe2fc..fc093554d8e6 100644 --- a/worlds/stardew_valley/mods/logic/sve_logic.py +++ b/worlds/stardew_valley/mods/logic/sve_logic.py @@ -14,12 +14,11 @@ from ...logic.time_logic import TimeLogicMixin from ...logic.tool_logic import ToolLogicMixin from ...strings.ap_names.mods.mod_items import SVELocation, SVERunes, SVEQuestItem +from ...strings.quest_names import ModQuest from ...strings.quest_names import Quest from ...strings.region_names import Region from ...strings.tool_names import Tool, ToolMaterial from ...strings.wallet_item_names import Wallet -from ...stardew_rule import Or -from ...strings.quest_names import ModQuest class SVELogicMixin(BaseLogicMixin): @@ -29,7 +28,7 @@ def __init__(self, *args, **kwargs): class SVELogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, QuestLogicMixin, RegionLogicMixin, RelationshipLogicMixin, TimeLogicMixin, ToolLogicMixin, - CookingLogicMixin, MoneyLogicMixin, CombatLogicMixin, SeasonLogicMixin, QuestLogicMixin]]): + CookingLogicMixin, MoneyLogicMixin, CombatLogicMixin, SeasonLogicMixin]]): def initialize_rules(self): self.registry.sve_location_rules.update({ SVELocation.tempered_galaxy_sword: self.logic.money.can_spend_at(SVERegion.alesia_shop, 350000), @@ -39,17 +38,31 @@ def initialize_rules(self): def has_any_rune(self): rune_list = SVERunes.nexus_items - return Or(*(self.logic.received(rune) for rune in rune_list)) + return self.logic.or_(*(self.logic.received(rune) for rune in rune_list)) def has_iridium_bomb(self): if self.options.quest_locations < 0: return self.logic.quest.can_complete_quest(ModQuest.RailroadBoulder) return self.logic.received(SVEQuestItem.iridium_bomb) + def has_marlon_boat(self): + if self.options.quest_locations < 0: + return self.logic.quest.can_complete_quest(ModQuest.MarlonsBoat) + return self.logic.received(SVEQuestItem.marlon_boat_paddle) + + def has_grandpa_shed_repaired(self): + if self.options.quest_locations < 0: + return self.logic.quest.can_complete_quest(ModQuest.GrandpasShed) + return self.logic.received(SVEQuestItem.grandpa_shed) + + def has_bear_knowledge(self): + if self.options.quest_locations < 0: + return self.logic.quest.can_complete_quest(Quest.strange_note) + return self.logic.received(Wallet.bears_knowledge) + def can_buy_bear_recipe(self): access_rule = (self.logic.quest.can_complete_quest(Quest.strange_note) & self.logic.tool.has_tool(Tool.axe, ToolMaterial.basic) & self.logic.tool.has_tool(Tool.pickaxe, ToolMaterial.basic)) forage_rule = self.logic.region.can_reach_any((Region.forest, Region.backwoods, Region.mountain)) - knowledge_rule = self.logic.received(Wallet.bears_knowledge) + knowledge_rule = self.has_bear_knowledge() return access_rule & forage_rule & knowledge_rule - diff --git a/worlds/stardew_valley/mods/mod_data.py b/worlds/stardew_valley/mods/mod_data.py index a4d3b9828aa6..54408fb2c571 100644 --- a/worlds/stardew_valley/mods/mod_data.py +++ b/worlds/stardew_valley/mods/mod_data.py @@ -26,14 +26,3 @@ class ModNames: distant_lands = "Distant Lands - Witch Swamp Overhaul" lacey = "Hat Mouse Lacey" boarding_house = "Boarding House and Bus Stop Extension" - - jasper_sve = jasper + "," + sve - - -all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, - ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, - ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, - ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, - ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, - ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator, ModNames.sve, ModNames.alecto, - ModNames.distant_lands, ModNames.lacey, ModNames.boarding_house}) diff --git a/worlds/stardew_valley/mods/mod_regions.py b/worlds/stardew_valley/mods/mod_regions.py index df0a12f6ef18..c075bd4d106f 100644 --- a/worlds/stardew_valley/mods/mod_regions.py +++ b/worlds/stardew_valley/mods/mod_regions.py @@ -1,11 +1,11 @@ from typing import Dict, List +from .mod_data import ModNames +from ..region_classes import RegionData, ConnectionData, ModificationFlag, RandomizationFlag, ModRegionData from ..strings.entrance_names import Entrance, DeepWoodsEntrance, EugeneEntrance, LaceyEntrance, BoardingHouseEntrance, \ JasperEntrance, AlecEntrance, YobaEntrance, JunaEntrance, MagicEntrance, AyeishaEntrance, RileyEntrance, SVEEntrance, AlectoEntrance from ..strings.region_names import Region, DeepWoodsRegion, EugeneRegion, JasperRegion, BoardingHouseRegion, \ AlecRegion, YobaRegion, JunaRegion, MagicRegion, AyeishaRegion, RileyRegion, SVERegion, AlectoRegion, LaceyRegion -from ..region_classes import RegionData, ConnectionData, ModificationFlag, RandomizationFlag, ModRegionData -from .mod_data import ModNames deep_woods_regions = [ RegionData(Region.farm, [DeepWoodsEntrance.use_woods_obelisk]), @@ -179,15 +179,15 @@ RegionData(SVERegion.grampleton_suburbs, [SVEEntrance.grampleton_suburbs_to_scarlett_house]), RegionData(SVERegion.scarlett_house), RegionData(Region.wizard_basement, [SVEEntrance.wizard_to_fable_reef]), - RegionData(SVERegion.fable_reef, [SVEEntrance.fable_reef_to_guild]), - RegionData(SVERegion.first_slash_guild, [SVEEntrance.first_slash_guild_to_hallway]), - RegionData(SVERegion.first_slash_hallway, [SVEEntrance.first_slash_hallway_to_room]), - RegionData(SVERegion.first_slash_spare_room), - RegionData(SVERegion.highlands_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave]), - RegionData(SVERegion.highlands_cavern, [SVEEntrance.to_dwarf_prison]), - RegionData(SVERegion.dwarf_prison), - RegionData(SVERegion.lances_house, [SVEEntrance.lance_to_ladder]), - RegionData(SVERegion.lances_ladder, [SVEEntrance.lance_ladder_to_highlands]), + RegionData(SVERegion.fable_reef, [SVEEntrance.fable_reef_to_guild], is_ginger_island=True), + RegionData(SVERegion.first_slash_guild, [SVEEntrance.first_slash_guild_to_hallway], is_ginger_island=True), + RegionData(SVERegion.first_slash_hallway, [SVEEntrance.first_slash_hallway_to_room], is_ginger_island=True), + RegionData(SVERegion.first_slash_spare_room, is_ginger_island=True), + RegionData(SVERegion.highlands_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave], is_ginger_island=True), + RegionData(SVERegion.highlands_cavern, [SVEEntrance.to_dwarf_prison], is_ginger_island=True), + RegionData(SVERegion.dwarf_prison, is_ginger_island=True), + RegionData(SVERegion.lances_house, [SVEEntrance.lance_to_ladder], is_ginger_island=True), + RegionData(SVERegion.lances_ladder, [SVEEntrance.lance_ladder_to_highlands], is_ginger_island=True), RegionData(SVERegion.forest_west, [SVEEntrance.forest_west_to_spring, SVEEntrance.west_to_aurora, SVEEntrance.use_bear_shop]), RegionData(SVERegion.aurora_vineyard, [SVEEntrance.to_aurora_basement]), @@ -217,7 +217,8 @@ ConnectionData(SVEEntrance.town_to_bridge, SVERegion.shearwater), ConnectionData(SVEEntrance.plot_to_bridge, SVERegion.shearwater), ConnectionData(SVEEntrance.bus_stop_to_shed, SVERegion.grandpas_shed), - ConnectionData(SVEEntrance.grandpa_shed_to_interior, SVERegion.grandpas_shed_interior, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), + ConnectionData(SVEEntrance.grandpa_shed_to_interior, SVERegion.grandpas_shed_interior, + flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(SVEEntrance.grandpa_interior_to_upstairs, SVERegion.grandpas_shed_upstairs, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.grandpa_shed_to_town, Region.town), ConnectionData(SVEEntrance.bmv_to_sophia, SVERegion.sophias_house, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), @@ -270,8 +271,9 @@ ConnectionData(SVEEntrance.grampleton_station_to_grampleton_suburbs, SVERegion.grampleton_suburbs), ConnectionData(SVEEntrance.grampleton_suburbs_to_scarlett_house, SVERegion.scarlett_house, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.first_slash_guild_to_hallway, SVERegion.first_slash_hallway, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(SVEEntrance.first_slash_hallway_to_room, SVERegion.first_slash_spare_room, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(SVEEntrance.sprite_spring_to_cave, SVERegion.sprite_spring_cave, flag=RandomizationFlag.BUILDINGS), + ConnectionData(SVEEntrance.first_slash_hallway_to_room, SVERegion.first_slash_spare_room, + flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), + ConnectionData(SVEEntrance.sprite_spring_to_cave, SVERegion.sprite_spring_cave, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.fish_shop_to_willy_bedroom, SVERegion.willy_bedroom, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.museum_to_gunther_bedroom, SVERegion.gunther_bedroom, flag=RandomizationFlag.BUILDINGS), ] @@ -332,7 +334,8 @@ flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(BoardingHouseEntrance.boarding_house_plateau_to_abandoned_mines_entrance, BoardingHouseRegion.abandoned_mines_entrance, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(BoardingHouseEntrance.abandoned_mines_entrance_to_the_lost_valley, BoardingHouseRegion.lost_valley_minecart, flag=RandomizationFlag.BUILDINGS), + ConnectionData(BoardingHouseEntrance.abandoned_mines_entrance_to_the_lost_valley, BoardingHouseRegion.lost_valley_minecart, + flag=RandomizationFlag.BUILDINGS), ConnectionData(BoardingHouseEntrance.abandoned_mines_entrance_to_abandoned_mines_1a, BoardingHouseRegion.abandoned_mines_1a, flag=RandomizationFlag.BUILDINGS), ConnectionData(BoardingHouseEntrance.abandoned_mines_1a_to_abandoned_mines_1b, BoardingHouseRegion.abandoned_mines_1b, flag=RandomizationFlag.BUILDINGS), @@ -347,16 +350,15 @@ ConnectionData(BoardingHouseEntrance.the_lost_valley_to_lost_valley_ruins, BoardingHouseRegion.lost_valley_ruins, flag=RandomizationFlag.BUILDINGS), ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1, BoardingHouseRegion.lost_valley_house_1, flag=RandomizationFlag.BUILDINGS), ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2, BoardingHouseRegion.lost_valley_house_2, flag=RandomizationFlag.BUILDINGS) - - ] vanilla_connections_to_remove_by_mod: Dict[str, List[ConnectionData]] = { - ModNames.sve: [ConnectionData(Entrance.mountain_to_the_mines, Region.mines, - flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild, - flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), - ] + ModNames.sve: [ + ConnectionData(Entrance.mountain_to_the_mines, Region.mines, + flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), + ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild, + flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), + ] } ModDataList = { diff --git a/worlds/stardew_valley/option_groups.py b/worlds/stardew_valley/option_groups.py index 50709c10fd49..d0f052348a7e 100644 --- a/worlds/stardew_valley/option_groups.py +++ b/worlds/stardew_valley/option_groups.py @@ -1,65 +1,76 @@ -from Options import OptionGroup, DeathLink, ProgressionBalancing, Accessibility +import logging + +from Options import DeathLink, ProgressionBalancing, Accessibility from .options import (Goal, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, EntranceRandomization, SeasonRandomization, Cropsanity, BackpackProgression, ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, FestivalLocations, ArcadeMachineLocations, SpecialOrderLocations, QuestLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize, - NumberOfMovementBuffs, NumberOfLuckBuffs, ExcludeGingerIsland, TrapItems, + NumberOfMovementBuffs, EnabledFillerBuffs, ExcludeGingerIsland, TrapItems, MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, FriendshipMultiplier, DebrisMultiplier, QuickStart, Gifting, FarmType, - Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, Mods) + Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, Mods, Booksanity, Walnutsanity, BundlePlando) -sv_option_groups = [ - OptionGroup("General", [ - Goal, - FarmType, - BundleRandomization, - BundlePrice, - EntranceRandomization, - ExcludeGingerIsland, - ]), - OptionGroup("Major Unlocks", [ - SeasonRandomization, - Cropsanity, - BackpackProgression, - ToolProgression, - ElevatorProgression, - SkillProgression, - BuildingProgression, - ]), - OptionGroup("Extra Shuffling", [ - FestivalLocations, - ArcadeMachineLocations, - SpecialOrderLocations, - QuestLocations, - Fishsanity, - Museumsanity, - Friendsanity, - FriendsanityHeartSize, - Monstersanity, - Shipsanity, - Cooksanity, - Chefsanity, - Craftsanity, - ]), - OptionGroup("Multipliers and Buffs", [ - StartingMoney, - ProfitMargin, - ExperienceMultiplier, - FriendshipMultiplier, - DebrisMultiplier, - NumberOfMovementBuffs, - NumberOfLuckBuffs, - TrapItems, - MultipleDaySleepEnabled, - MultipleDaySleepCost, - QuickStart, - ]), - OptionGroup("Advanced Options", [ - Gifting, - DeathLink, - Mods, - ProgressionBalancing, - Accessibility, - ]), -] +sv_option_groups = [] +try: + from Options import OptionGroup +except: + logging.warning("Old AP Version, OptionGroup not available.") +else: + sv_option_groups = [ + OptionGroup("General", [ + Goal, + FarmType, + BundleRandomization, + BundlePrice, + EntranceRandomization, + ExcludeGingerIsland, + ]), + OptionGroup("Major Unlocks", [ + SeasonRandomization, + Cropsanity, + BackpackProgression, + ToolProgression, + ElevatorProgression, + SkillProgression, + BuildingProgression, + ]), + OptionGroup("Extra Shuffling", [ + FestivalLocations, + ArcadeMachineLocations, + SpecialOrderLocations, + QuestLocations, + Fishsanity, + Museumsanity, + Friendsanity, + FriendsanityHeartSize, + Monstersanity, + Shipsanity, + Cooksanity, + Chefsanity, + Craftsanity, + Booksanity, + Walnutsanity, + ]), + OptionGroup("Multipliers and Buffs", [ + StartingMoney, + ProfitMargin, + ExperienceMultiplier, + FriendshipMultiplier, + DebrisMultiplier, + NumberOfMovementBuffs, + EnabledFillerBuffs, + TrapItems, + MultipleDaySleepEnabled, + MultipleDaySleepCost, + QuickStart, + ]), + OptionGroup("Advanced Options", [ + Gifting, + DeathLink, + Mods, + BundlePlando, + ProgressionBalancing, + Accessibility, + ]), + ] diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index ba1ebfb9c177..5369e88a2dcb 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -1,8 +1,12 @@ +import sys +import typing from dataclasses import dataclass from typing import Protocol, ClassVar -from Options import Range, NamedRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink +from Options import Range, NamedRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, OptionList, Visibility from .mods.mod_data import ModNames +from .strings.ap_names.ap_option_names import OptionName +from .strings.bundle_names import all_cc_bundle_names class StardewValleyOption(Protocol): @@ -18,7 +22,7 @@ class Goal(Choice): Master Angler: Catch every fish. Adapts to Fishsanity Complete Collection: Complete the museum collection Full House: Get married and have 2 children - Greatest Walnut Hunter: Find 130 Golden Walnuts + Greatest Walnut Hunter: Find 130 Golden Walnuts. Pairs well with Walnutsanity Protector of the Valley: Complete the monster slayer goals. Adapts to Monstersanity Full Shipment: Ship every item. Adapts to Shipsanity Gourmet Chef: Cook every recipe. Adapts to Cooksanity @@ -73,6 +77,7 @@ class FarmType(Choice): option_wilderness = 4 option_four_corners = 5 option_beach = 6 + option_meadowlands = 7 class StartingMoney(NamedRange): @@ -118,14 +123,16 @@ class BundleRandomization(Choice): Vanilla: Standard bundles from the vanilla game Thematic: Every bundle will require random items compatible with their original theme Remixed: Picks bundles at random from thematic, vanilla remixed and new custom ones + Remixed Anywhere: Remixed, but bundles are not locked to specific rooms. Shuffled: Every bundle will require random items and follow no particular structure""" internal_name = "bundle_randomization" display_name = "Bundle Randomization" - default = 2 option_vanilla = 0 option_thematic = 1 - option_remixed = 2 - option_shuffled = 3 + option_remixed = 3 + option_remixed_anywhere = 4 + option_shuffled = 6 + default = option_remixed class BundlePrice(Choice): @@ -155,6 +162,7 @@ class EntranceRandomization(Choice): Pelican Town: Only doors in the main town area are randomized with each other Non Progression: Only entrances that are always available are randomized with each other Buildings: All entrances that allow you to enter a building are randomized with each other + Buildings Without House: Buildings, but excluding the farmhouse Chaos: Same as "Buildings", but the entrances get reshuffled every single day! """ # Everything: All buildings and areas are randomized with each other @@ -169,9 +177,10 @@ class EntranceRandomization(Choice): option_disabled = 0 option_pelican_town = 1 option_non_progression = 2 - option_buildings = 3 - # option_everything = 4 - option_chaos = 5 + option_buildings_without_house = 3 + option_buildings = 4 + # option_everything = 10 + option_chaos = 12 # option_buildings_one_way = 6 # option_everything_one_way = 7 # option_chaos_one_way = 8 @@ -255,12 +264,14 @@ class ElevatorProgression(Choice): class SkillProgression(Choice): """Shuffle skill levels? Vanilla: Leveling up skills is normal - Progressive: Skill levels are unlocked randomly, and earning xp sends checks""" + Progressive: Skill levels are unlocked randomly, and earning xp sends checks. Masteries are excluded + With Masteries: Skill levels are unlocked randomly, and earning xp sends checks. Masteries are included""" internal_name = "skill_progression" display_name = "Skill Progression" - default = 1 + default = 2 option_vanilla = 0 option_progressive = 1 + option_progressive_with_masteries = 2 class BuildingProgression(Choice): @@ -319,13 +330,26 @@ class SpecialOrderLocations(Choice): Disabled: The special orders are not included in the Archipelago shuffling. Board Only: The Special Orders on the board in town are location checks Board and Qi: The Special Orders from Mr Qi's walnut room are checks, in addition to the board in town + Short: All Special Order requirements are reduced by 40% + Very Short: All Special Order requirements are reduced by 80% """ internal_name = "special_order_locations" display_name = "Special Order Locations" - default = 1 - option_disabled = 0 - option_board_only = 1 - option_board_qi = 2 + option_vanilla = 0b0000 # 0 + option_board = 0b0001 # 1 + value_qi = 0b0010 # 2 + value_short = 0b0100 # 4 + value_very_short = 0b1000 # 8 + option_board_qi = option_board | value_qi # 3 + option_vanilla_short = value_short # 4 + option_board_short = option_board | value_short # 5 + option_board_qi_short = option_board_qi | value_short # 7 + option_vanilla_very_short = value_very_short # 8 + option_board_very_short = option_board | value_very_short # 9 + option_board_qi_very_short = option_board_qi | value_very_short # 11 + alias_disabled = option_vanilla + alias_board_only = option_board + default = option_board_short class QuestLocations(NamedRange): @@ -533,6 +557,46 @@ class FriendsanityHeartSize(Range): # step = 1 +class Booksanity(Choice): + """Shuffle Books? + None: All books behave like vanilla + Power: Power books are turned into checks + Power and Skill: Power and skill books are turned into checks. + All: Lost books are also included in the shuffling + """ + internal_name = "booksanity" + display_name = "Booksanity" + default = 2 + option_none = 0 + option_power = 1 + option_power_skill = 2 + option_all = 3 + + +class Walnutsanity(OptionSet): + """Shuffle walnuts? + 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 + Dig spots: Walnuts that are underground and must be digged up. Includes Journal scrap walnuts + Repeatables: Random chance walnuts from normal actions (fishing, farming, combat, etc) + """ + internal_name = "walnutsanity" + display_name = "Walnutsanity" + valid_keys = frozenset({OptionName.walnutsanity_puzzles, OptionName.walnutsanity_bushes, OptionName.walnutsanity_dig_spots, + OptionName.walnutsanity_repeatables, }) + preset_none = frozenset() + preset_all = valid_keys + default = preset_none + + def __eq__(self, other: typing.Any) -> bool: + if isinstance(other, OptionSet): + return set(self.value) == other.value + if isinstance(other, OptionList): + return set(self.value) == set(other.value) + else: + return typing.cast(bool, self.value == other) + + class NumberOfMovementBuffs(Range): """Number of movement speed buffs to the player that exist as items in the pool. Each movement speed buff is a +25% multiplier that stacks additively""" @@ -544,15 +608,26 @@ class NumberOfMovementBuffs(Range): # step = 1 -class NumberOfLuckBuffs(Range): - """Number of luck buffs to the player that exist as items in the pool. - Each luck buff is a bonus to daily luck of 0.025""" - internal_name = "luck_buff_number" - display_name = "Number of Luck Buffs" - range_start = 0 - range_end = 12 - default = 4 - # step = 1 +class EnabledFillerBuffs(OptionSet): + """Enable various permanent player buffs to roll as filler items + Luck: Increase daily luck + Damage: Increased Damage % + Defense: Increased Defense + Immunity: Increased Immunity + Health: Increased Max Health + Energy: Increased Max Energy + Bite Rate: Shorter delay to get a bite when fishing + Fish Trap: Effect similar to the Trap Bobber, but weaker + Fishing Bar Size: Increased Fishing Bar Size + """ + internal_name = "enabled_filler_buffs" + display_name = "Enabled Filler Buffs" + valid_keys = frozenset({OptionName.buff_luck, OptionName.buff_damage, OptionName.buff_defense, OptionName.buff_immunity, OptionName.buff_health, + OptionName.buff_energy, OptionName.buff_bite, OptionName.buff_fish_trap, OptionName.buff_fishing_bar}) + # OptionName.buff_quality, OptionName.buff_glow}) # Disabled these two buffs because they are too hard to make on the mod side + preset_none = frozenset() + preset_all = valid_keys + default = frozenset({OptionName.buff_luck, OptionName.buff_defense, OptionName.buff_bite}) class ExcludeGingerIsland(Toggle): @@ -678,19 +753,40 @@ class Gifting(Toggle): default = 1 +# These mods have been disabled because either they are not updated for the current supported version of Stardew Valley, +# or we didn't find the time to validate that they work or fix compatibility issues if they do. +# Once a mod is validated to be functional, it can simply be removed from this list +disabled_mods = {ModNames.deepwoods, ModNames.magic, + ModNames.cooking_skill, + ModNames.yoba, ModNames.eugene, + ModNames.wellwick, ModNames.shiko, ModNames.delores, ModNames.riley, + ModNames.boarding_house} + + +if 'unittest' in sys.modules.keys() or 'pytest' in sys.modules.keys(): + disabled_mods = {} + + class Mods(OptionSet): """List of mods that will be included in the shuffling.""" internal_name = "mods" display_name = "Mods" - valid_keys = { - ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, - ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, - ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, - ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, - ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, - ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator, ModNames.sve, ModNames.distant_lands, - ModNames.alecto, ModNames.lacey, ModNames.boarding_house - } + valid_keys = {ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, + ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, + ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, + ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, + ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, + ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator, ModNames.sve, ModNames.distant_lands, + ModNames.alecto, ModNames.lacey, ModNames.boarding_house}.difference(disabled_mods) + + +class BundlePlando(OptionSet): + """If using Remixed bundles, this guarantees some of them will show up in your community center. + If more bundles are specified than what fits in their parent room, that room will randomly pick from only the plando ones""" + internal_name = "bundle_plando" + display_name = "Bundle Plando" + visibility = Visibility.template | Visibility.spoiler + valid_keys = set(all_cc_bundle_names) @dataclass @@ -720,6 +816,8 @@ class StardewValleyOptions(PerGameCommonOptions): craftsanity: Craftsanity friendsanity: Friendsanity friendsanity_heart_size: FriendsanityHeartSize + booksanity: Booksanity + walnutsanity: Walnutsanity exclude_ginger_island: ExcludeGingerIsland quick_start: QuickStart starting_money: StartingMoney @@ -728,10 +826,11 @@ class StardewValleyOptions(PerGameCommonOptions): friendship_multiplier: FriendshipMultiplier debris_multiplier: DebrisMultiplier movement_buff_number: NumberOfMovementBuffs - luck_buff_number: NumberOfLuckBuffs + enabled_filler_buffs: EnabledFillerBuffs trap_items: TrapItems multiple_day_sleep_enabled: MultipleDaySleepEnabled multiple_day_sleep_cost: MultipleDaySleepCost gifting: Gifting mods: Mods + bundle_plando: BundlePlando death_link: DeathLink diff --git a/worlds/stardew_valley/presets.py b/worlds/stardew_valley/presets.py index e75eb5c5fcde..e663241ac6af 100644 --- a/worlds/stardew_valley/presets.py +++ b/worlds/stardew_valley/presets.py @@ -3,9 +3,12 @@ from Options import Accessibility, ProgressionBalancing, DeathLink from .options import Goal, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, EntranceRandomization, SeasonRandomization, Cropsanity, \ BackpackProgression, ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, FestivalLocations, ArcadeMachineLocations, \ - SpecialOrderLocations, QuestLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize, NumberOfMovementBuffs, NumberOfLuckBuffs, \ - ExcludeGingerIsland, TrapItems, MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, FriendshipMultiplier, DebrisMultiplier, QuickStart, \ - Gifting, FarmType, Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity + SpecialOrderLocations, QuestLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize, NumberOfMovementBuffs, ExcludeGingerIsland, TrapItems, \ + MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, FriendshipMultiplier, DebrisMultiplier, QuickStart, \ + Gifting, FarmType, Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, Booksanity, Walnutsanity, EnabledFillerBuffs + +# @formatter:off +from .strings.ap_names.ap_option_names import OptionName all_random_settings = { "progression_balancing": "random", @@ -37,8 +40,10 @@ Craftsanity.internal_name: "random", Friendsanity.internal_name: "random", FriendsanityHeartSize.internal_name: "random", + Booksanity.internal_name: "random", + Walnutsanity.internal_name: "random", NumberOfMovementBuffs.internal_name: "random", - NumberOfLuckBuffs.internal_name: "random", + EnabledFillerBuffs.internal_name: "random", ExcludeGingerIsland.internal_name: "random", TrapItems.internal_name: "random", MultipleDaySleepEnabled.internal_name: "random", @@ -70,7 +75,7 @@ BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap, FestivalLocations.internal_name: FestivalLocations.option_easy, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla_very_short, QuestLocations.internal_name: "minimum", Fishsanity.internal_name: Fishsanity.option_only_easy_fish, Museumsanity.internal_name: Museumsanity.option_milestones, @@ -81,8 +86,10 @@ Craftsanity.internal_name: Craftsanity.option_none, Friendsanity.internal_name: Friendsanity.option_none, FriendsanityHeartSize.internal_name: 4, + Booksanity.internal_name: Booksanity.option_none, + Walnutsanity.internal_name: Walnutsanity.preset_none, NumberOfMovementBuffs.internal_name: 8, - NumberOfLuckBuffs.internal_name: 8, + EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, TrapItems.internal_name: TrapItems.option_easy, MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, @@ -109,12 +116,12 @@ Cropsanity.internal_name: Cropsanity.option_enabled, BackpackProgression.internal_name: BackpackProgression.option_early_progressive, ToolProgression.internal_name: ToolProgression.option_progressive_cheap, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive, SkillProgression.internal_name: SkillProgression.option_progressive, BuildingProgression.internal_name: BuildingProgression.option_progressive_cheap, FestivalLocations.internal_name: FestivalLocations.option_hard, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories_easy, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_short, QuestLocations.internal_name: "normal", Fishsanity.internal_name: Fishsanity.option_exclude_legendaries, Museumsanity.internal_name: Museumsanity.option_milestones, @@ -125,8 +132,10 @@ Craftsanity.internal_name: Craftsanity.option_none, Friendsanity.internal_name: Friendsanity.option_starting_npcs, FriendsanityHeartSize.internal_name: 4, + Booksanity.internal_name: Booksanity.option_power_skill, + Walnutsanity.internal_name: [OptionName.walnutsanity_puzzles], NumberOfMovementBuffs.internal_name: 6, - NumberOfLuckBuffs.internal_name: 6, + EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, TrapItems.internal_name: TrapItems.option_medium, MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, @@ -148,17 +157,17 @@ ProfitMargin.internal_name: "normal", BundleRandomization.internal_name: BundleRandomization.option_remixed, BundlePrice.internal_name: BundlePrice.option_expensive, - EntranceRandomization.internal_name: EntranceRandomization.option_buildings, + EntranceRandomization.internal_name: EntranceRandomization.option_buildings_without_house, SeasonRandomization.internal_name: SeasonRandomization.option_randomized, Cropsanity.internal_name: Cropsanity.option_enabled, BackpackProgression.internal_name: BackpackProgression.option_progressive, ToolProgression.internal_name: ToolProgression.option_progressive, ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, - SkillProgression.internal_name: SkillProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, BuildingProgression.internal_name: BuildingProgression.option_progressive, FestivalLocations.internal_name: FestivalLocations.option_hard, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi_short, QuestLocations.internal_name: "lots", Fishsanity.internal_name: Fishsanity.option_all, Museumsanity.internal_name: Museumsanity.option_all, @@ -169,8 +178,10 @@ Craftsanity.internal_name: Craftsanity.option_none, Friendsanity.internal_name: Friendsanity.option_all, FriendsanityHeartSize.internal_name: 4, + Booksanity.internal_name: Booksanity.option_all, + Walnutsanity.internal_name: Walnutsanity.preset_all, NumberOfMovementBuffs.internal_name: 4, - NumberOfLuckBuffs.internal_name: 4, + EnabledFillerBuffs.internal_name: EnabledFillerBuffs.default, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, TrapItems.internal_name: TrapItems.option_hard, MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, @@ -198,7 +209,7 @@ BackpackProgression.internal_name: BackpackProgression.option_progressive, ToolProgression.internal_name: ToolProgression.option_progressive, ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, - SkillProgression.internal_name: SkillProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, BuildingProgression.internal_name: BuildingProgression.option_progressive, FestivalLocations.internal_name: FestivalLocations.option_hard, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, @@ -213,8 +224,10 @@ Craftsanity.internal_name: Craftsanity.option_none, Friendsanity.internal_name: Friendsanity.option_all_with_marriage, FriendsanityHeartSize.internal_name: 4, + Booksanity.internal_name: Booksanity.option_all, + Walnutsanity.internal_name: Walnutsanity.preset_all, NumberOfMovementBuffs.internal_name: 2, - NumberOfLuckBuffs.internal_name: 2, + EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_none, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, TrapItems.internal_name: TrapItems.option_hell, MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, @@ -241,12 +254,12 @@ Cropsanity.internal_name: Cropsanity.option_disabled, BackpackProgression.internal_name: BackpackProgression.option_early_progressive, ToolProgression.internal_name: ToolProgression.option_progressive_very_cheap, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive, SkillProgression.internal_name: SkillProgression.option_progressive, BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap, FestivalLocations.internal_name: FestivalLocations.option_disabled, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla_very_short, QuestLocations.internal_name: "none", Fishsanity.internal_name: Fishsanity.option_none, Museumsanity.internal_name: Museumsanity.option_none, @@ -257,8 +270,10 @@ Craftsanity.internal_name: Craftsanity.option_none, Friendsanity.internal_name: Friendsanity.option_none, FriendsanityHeartSize.internal_name: 4, + Booksanity.internal_name: Booksanity.option_none, + Walnutsanity.internal_name: Walnutsanity.preset_none, NumberOfMovementBuffs.internal_name: 10, - NumberOfLuckBuffs.internal_name: 10, + EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, TrapItems.internal_name: TrapItems.option_easy, MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, @@ -290,7 +305,7 @@ BuildingProgression.internal_name: BuildingProgression.option_vanilla, FestivalLocations.internal_name: FestivalLocations.option_disabled, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla_very_short, QuestLocations.internal_name: "none", Fishsanity.internal_name: Fishsanity.option_none, Museumsanity.internal_name: Museumsanity.option_none, @@ -301,8 +316,10 @@ Craftsanity.internal_name: Craftsanity.option_none, Friendsanity.internal_name: Friendsanity.option_none, FriendsanityHeartSize.internal_name: FriendsanityHeartSize.default, + Booksanity.internal_name: Booksanity.option_none, + Walnutsanity.internal_name: Walnutsanity.preset_none, NumberOfMovementBuffs.internal_name: NumberOfMovementBuffs.default, - NumberOfLuckBuffs.internal_name: NumberOfLuckBuffs.default, + EnabledFillerBuffs.internal_name: EnabledFillerBuffs.default, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, TrapItems.internal_name: TrapItems.default, MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.default, @@ -330,7 +347,7 @@ BackpackProgression.internal_name: BackpackProgression.option_early_progressive, ToolProgression.internal_name: ToolProgression.option_progressive, ElevatorProgression.internal_name: ElevatorProgression.option_progressive, - SkillProgression.internal_name: SkillProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, BuildingProgression.internal_name: BuildingProgression.option_progressive, FestivalLocations.internal_name: FestivalLocations.option_hard, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, @@ -345,8 +362,10 @@ Craftsanity.internal_name: Craftsanity.option_all, Friendsanity.internal_name: Friendsanity.option_all, FriendsanityHeartSize.internal_name: 1, + Booksanity.internal_name: Booksanity.option_all, + Walnutsanity.internal_name: Walnutsanity.preset_all, NumberOfMovementBuffs.internal_name: 12, - NumberOfLuckBuffs.internal_name: 12, + EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, TrapItems.internal_name: TrapItems.default, MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.default, @@ -358,6 +377,8 @@ Gifting.internal_name: Gifting.default, "death_link": DeathLink.default, } +# @formatter:on + sv_options_presets: Dict[str, Dict[str, Any]] = { "All random": all_random_settings, diff --git a/worlds/stardew_valley/region_classes.py b/worlds/stardew_valley/region_classes.py index eaabcfa5fd36..bd64518ea153 100644 --- a/worlds/stardew_valley/region_classes.py +++ b/worlds/stardew_valley/region_classes.py @@ -1,6 +1,7 @@ -from enum import IntFlag -from typing import Optional, List +from copy import deepcopy from dataclasses import dataclass, field +from enum import IntFlag +from typing import Optional, List, Set connector_keyword = " to " @@ -9,15 +10,16 @@ class ModificationFlag(IntFlag): NOT_MODIFIED = 0 MODIFIED = 1 + class RandomizationFlag(IntFlag): NOT_RANDOMIZED = 0b0 - PELICAN_TOWN = 0b11111 - NON_PROGRESSION = 0b11110 - BUILDINGS = 0b11100 - EVERYTHING = 0b11000 - CHAOS = 0b10000 - GINGER_ISLAND = 0b0100000 - LEAD_TO_OPEN_AREA = 0b1000000 + PELICAN_TOWN = 0b00011111 + NON_PROGRESSION = 0b00011110 + BUILDINGS = 0b00011100 + EVERYTHING = 0b00011000 + GINGER_ISLAND = 0b00100000 + LEAD_TO_OPEN_AREA = 0b01000000 + MASTERIES = 0b10000000 @dataclass(frozen=True) @@ -25,6 +27,7 @@ class RegionData: name: str exits: List[str] = field(default_factory=list) flag: ModificationFlag = ModificationFlag.NOT_MODIFIED + is_ginger_island: bool = False def get_merged_with(self, exits: List[str]): merged_exits = [] @@ -32,14 +35,14 @@ def get_merged_with(self, exits: List[str]): if exits is not None: merged_exits.extend(exits) merged_exits = list(set(merged_exits)) - return RegionData(self.name, merged_exits) + return RegionData(self.name, merged_exits, is_ginger_island=self.is_ginger_island) - def get_without_exit(self, exit_to_remove: str): - exits = [exit for exit in self.exits if exit != exit_to_remove] - return RegionData(self.name, exits) + def get_without_exits(self, exits_to_remove: Set[str]): + exits = [exit_ for exit_ in self.exits if exit_ not in exits_to_remove] + return RegionData(self.name, exits, is_ginger_island=self.is_ginger_island) def get_clone(self): - return self.get_merged_with(None) + return deepcopy(self) @dataclass(frozen=True) @@ -62,6 +65,3 @@ class ModRegionData: mod_name: str regions: List[RegionData] connections: List[ConnectionData] - - - diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py index 4284b438f806..2aca2d3f4d3e 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -2,11 +2,11 @@ from typing import Iterable, Dict, Protocol, List, Tuple, Set from BaseClasses import Region, Entrance -from .options import EntranceRandomization, ExcludeGingerIsland, Museumsanity, StardewValleyOptions -from .strings.entrance_names import Entrance -from .strings.region_names import Region -from .region_classes import RegionData, ConnectionData, RandomizationFlag, ModificationFlag from .mods.mod_regions import ModDataList, vanilla_connections_to_remove_by_mod +from .options import EntranceRandomization, ExcludeGingerIsland, StardewValleyOptions, SkillProgression +from .region_classes import RegionData, ConnectionData, RandomizationFlag, ModificationFlag +from .strings.entrance_names import Entrance, LogicEntrance +from .strings.region_names import Region, LogicRegion class RegionFactory(Protocol): @@ -17,78 +17,57 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: vanilla_regions = [ RegionData(Region.menu, [Entrance.to_stardew_valley]), RegionData(Region.stardew_valley, [Entrance.to_farmhouse]), - RegionData(Region.farm_house, [Entrance.farmhouse_to_farm, Entrance.downstairs_to_cellar, Entrance.farmhouse_cooking, Entrance.watch_queen_of_sauce]), + RegionData(Region.farm_house, + [Entrance.farmhouse_to_farm, Entrance.downstairs_to_cellar, LogicEntrance.farmhouse_cooking, LogicEntrance.watch_queen_of_sauce]), RegionData(Region.cellar), - RegionData(Region.kitchen), - RegionData(Region.queen_of_sauce), RegionData(Region.farm, - [Entrance.farm_to_backwoods, Entrance.farm_to_bus_stop, Entrance.farm_to_forest, - Entrance.farm_to_farmcave, Entrance.enter_greenhouse, - Entrance.enter_coop, Entrance.enter_barn, - Entrance.enter_shed, Entrance.enter_slime_hutch, - Entrance.farming, Entrance.shipping]), - RegionData(Region.farming), - RegionData(Region.shipping), + [Entrance.farm_to_backwoods, Entrance.farm_to_bus_stop, Entrance.farm_to_forest, Entrance.farm_to_farmcave, Entrance.enter_greenhouse, + Entrance.enter_coop, Entrance.enter_barn, Entrance.enter_shed, Entrance.enter_slime_hutch, LogicEntrance.grow_spring_crops, + LogicEntrance.grow_summer_crops, LogicEntrance.grow_fall_crops, LogicEntrance.grow_winter_crops, LogicEntrance.shipping]), RegionData(Region.backwoods, [Entrance.backwoods_to_mountain]), RegionData(Region.bus_stop, [Entrance.bus_stop_to_town, Entrance.take_bus_to_desert, Entrance.bus_stop_to_tunnel_entrance]), RegionData(Region.forest, - [Entrance.forest_to_town, Entrance.enter_secret_woods, Entrance.forest_to_wizard_tower, - Entrance.forest_to_marnie_ranch, - Entrance.forest_to_leah_cottage, Entrance.forest_to_sewer, - Entrance.buy_from_traveling_merchant, - Entrance.attend_flower_dance, Entrance.attend_festival_of_ice]), - RegionData(Region.traveling_cart, [Entrance.buy_from_traveling_merchant_sunday, - Entrance.buy_from_traveling_merchant_monday, - Entrance.buy_from_traveling_merchant_tuesday, - Entrance.buy_from_traveling_merchant_wednesday, - Entrance.buy_from_traveling_merchant_thursday, - Entrance.buy_from_traveling_merchant_friday, - Entrance.buy_from_traveling_merchant_saturday]), - RegionData(Region.traveling_cart_sunday), - RegionData(Region.traveling_cart_monday), - RegionData(Region.traveling_cart_tuesday), - RegionData(Region.traveling_cart_wednesday), - RegionData(Region.traveling_cart_thursday), - RegionData(Region.traveling_cart_friday), - RegionData(Region.traveling_cart_saturday), + [Entrance.forest_to_town, Entrance.enter_secret_woods, Entrance.forest_to_wizard_tower, Entrance.forest_to_marnie_ranch, + Entrance.forest_to_leah_cottage, Entrance.forest_to_sewer, Entrance.forest_to_mastery_cave, LogicEntrance.buy_from_traveling_merchant, + LogicEntrance.complete_raccoon_requests, LogicEntrance.fish_in_waterfall, LogicEntrance.attend_flower_dance, LogicEntrance.attend_trout_derby, + LogicEntrance.attend_festival_of_ice]), + RegionData(LogicRegion.forest_waterfall), RegionData(Region.farm_cave), - RegionData(Region.greenhouse), + RegionData(Region.greenhouse, + [LogicEntrance.grow_spring_crops_in_greenhouse, LogicEntrance.grow_summer_crops_in_greenhouse, LogicEntrance.grow_fall_crops_in_greenhouse, + LogicEntrance.grow_winter_crops_in_greenhouse, LogicEntrance.grow_indoor_crops_in_greenhouse]), RegionData(Region.mountain, [Entrance.mountain_to_railroad, Entrance.mountain_to_tent, Entrance.mountain_to_carpenter_shop, Entrance.mountain_to_the_mines, Entrance.enter_quarry, Entrance.mountain_to_adventurer_guild, Entrance.mountain_to_town, Entrance.mountain_to_maru_room, Entrance.mountain_to_leo_treehouse]), - RegionData(Region.leo_treehouse), + RegionData(Region.leo_treehouse, is_ginger_island=True), RegionData(Region.maru_room), RegionData(Region.tunnel_entrance, [Entrance.tunnel_entrance_to_bus_tunnel]), RegionData(Region.bus_tunnel), RegionData(Region.town, - [Entrance.town_to_community_center, Entrance.town_to_beach, Entrance.town_to_hospital, - Entrance.town_to_pierre_general_store, Entrance.town_to_saloon, Entrance.town_to_alex_house, - Entrance.town_to_trailer, - Entrance.town_to_mayor_manor, - Entrance.town_to_sam_house, Entrance.town_to_haley_house, Entrance.town_to_sewer, - Entrance.town_to_clint_blacksmith, - Entrance.town_to_museum, - Entrance.town_to_jojamart, Entrance.purchase_movie_ticket, - Entrance.attend_egg_festival, Entrance.attend_fair, Entrance.attend_spirit_eve, Entrance.attend_winter_star]), - RegionData(Region.beach, [Entrance.beach_to_willy_fish_shop, Entrance.enter_elliott_house, Entrance.enter_tide_pools, - Entrance.fishing, - Entrance.attend_luau, Entrance.attend_moonlight_jellies, Entrance.attend_night_market]), - RegionData(Region.fishing), + [Entrance.town_to_community_center, Entrance.town_to_beach, Entrance.town_to_hospital, Entrance.town_to_pierre_general_store, + Entrance.town_to_saloon, Entrance.town_to_alex_house, Entrance.town_to_trailer, Entrance.town_to_mayor_manor, Entrance.town_to_sam_house, + Entrance.town_to_haley_house, Entrance.town_to_sewer, Entrance.town_to_clint_blacksmith, Entrance.town_to_museum, Entrance.town_to_jojamart, + Entrance.purchase_movie_ticket, LogicEntrance.buy_experience_books, LogicEntrance.attend_egg_festival, LogicEntrance.attend_fair, + LogicEntrance.attend_spirit_eve, LogicEntrance.attend_winter_star]), + RegionData(Region.beach, + [Entrance.beach_to_willy_fish_shop, Entrance.enter_elliott_house, Entrance.enter_tide_pools, LogicEntrance.fishing, LogicEntrance.attend_luau, + LogicEntrance.attend_moonlight_jellies, LogicEntrance.attend_night_market, LogicEntrance.attend_squidfest]), RegionData(Region.railroad, [Entrance.enter_bathhouse_entrance, Entrance.enter_witch_warp_cave]), RegionData(Region.ranch), RegionData(Region.leah_house), + RegionData(Region.mastery_cave), RegionData(Region.sewer, [Entrance.enter_mutant_bug_lair]), RegionData(Region.mutant_bug_lair), - RegionData(Region.wizard_tower, [Entrance.enter_wizard_basement, - Entrance.use_desert_obelisk, Entrance.use_island_obelisk]), + RegionData(Region.wizard_tower, [Entrance.enter_wizard_basement, Entrance.use_desert_obelisk, Entrance.use_island_obelisk]), RegionData(Region.wizard_basement), RegionData(Region.tent), RegionData(Region.carpenter, [Entrance.enter_sebastian_room]), RegionData(Region.sebastian_room), - RegionData(Region.adventurer_guild), + RegionData(Region.adventurer_guild, [Entrance.adventurer_guild_to_bedroom]), + RegionData(Region.adventurer_guild_bedroom), RegionData(Region.community_center, [Entrance.access_crafts_room, Entrance.access_pantry, Entrance.access_fish_tank, Entrance.access_boiler_room, Entrance.access_bulletin_board, Entrance.access_vault]), @@ -114,18 +93,14 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: RegionData(Region.mayor_house), RegionData(Region.sam_house), RegionData(Region.haley_house), - RegionData(Region.blacksmith, [Entrance.blacksmith_copper]), - RegionData(Region.blacksmith_copper, [Entrance.blacksmith_iron]), - RegionData(Region.blacksmith_iron, [Entrance.blacksmith_gold]), - RegionData(Region.blacksmith_gold, [Entrance.blacksmith_iridium]), - RegionData(Region.blacksmith_iridium), + RegionData(Region.blacksmith, [LogicEntrance.blacksmith_copper]), RegionData(Region.museum), RegionData(Region.jojamart, [Entrance.enter_abandoned_jojamart]), RegionData(Region.abandoned_jojamart, [Entrance.enter_movie_theater]), RegionData(Region.movie_ticket_stand), RegionData(Region.movie_theater), RegionData(Region.fish_shop, [Entrance.fish_shop_to_boat_tunnel]), - RegionData(Region.boat_tunnel, [Entrance.boat_to_ginger_island]), + RegionData(Region.boat_tunnel, [Entrance.boat_to_ginger_island], is_ginger_island=True), RegionData(Region.elliott_house), RegionData(Region.tide_pools), RegionData(Region.bathhouse_entrance, [Entrance.enter_locker_room]), @@ -138,7 +113,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: RegionData(Region.quarry_mine_entrance, [Entrance.enter_quarry_mine]), RegionData(Region.quarry_mine), RegionData(Region.secret_woods), - RegionData(Region.desert, [Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis]), + RegionData(Region.desert, [Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis, LogicEntrance.attend_desert_festival]), RegionData(Region.oasis, [Entrance.enter_casino]), RegionData(Region.casino), RegionData(Region.skull_cavern_entrance, [Entrance.enter_skull_cavern]), @@ -151,49 +126,52 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: RegionData(Region.skull_cavern_150, [Entrance.mine_to_skull_cavern_floor_175]), RegionData(Region.skull_cavern_175, [Entrance.mine_to_skull_cavern_floor_200]), RegionData(Region.skull_cavern_200, [Entrance.enter_dangerous_skull_cavern]), - RegionData(Region.dangerous_skull_cavern), - RegionData(Region.island_south, [Entrance.island_south_to_west, Entrance.island_south_to_north, - Entrance.island_south_to_east, Entrance.island_south_to_southeast, - Entrance.use_island_resort, - Entrance.parrot_express_docks_to_volcano, - Entrance.parrot_express_docks_to_dig_site, - Entrance.parrot_express_docks_to_jungle]), - RegionData(Region.island_resort), + RegionData(Region.dangerous_skull_cavern, is_ginger_island=True), + RegionData(Region.island_south, + [Entrance.island_south_to_west, Entrance.island_south_to_north, Entrance.island_south_to_east, Entrance.island_south_to_southeast, + Entrance.use_island_resort, Entrance.parrot_express_docks_to_volcano, Entrance.parrot_express_docks_to_dig_site, + Entrance.parrot_express_docks_to_jungle], + is_ginger_island=True), + RegionData(Region.island_resort, is_ginger_island=True), RegionData(Region.island_west, - [Entrance.island_west_to_islandfarmhouse, Entrance.island_west_to_gourmand_cave, - Entrance.island_west_to_crystals_cave, Entrance.island_west_to_shipwreck, - Entrance.island_west_to_qi_walnut_room, Entrance.use_farm_obelisk, - Entrance.parrot_express_jungle_to_docks, Entrance.parrot_express_jungle_to_dig_site, - Entrance.parrot_express_jungle_to_volcano]), - RegionData(Region.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine]), - RegionData(Region.island_shrine), - RegionData(Region.island_south_east, [Entrance.island_southeast_to_pirate_cove]), - RegionData(Region.island_north, [Entrance.talk_to_island_trader, Entrance.island_north_to_field_office, - Entrance.island_north_to_dig_site, Entrance.island_north_to_volcano, - Entrance.parrot_express_volcano_to_dig_site, - Entrance.parrot_express_volcano_to_jungle, - Entrance.parrot_express_volcano_to_docks]), - RegionData(Region.volcano, [Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach]), - RegionData(Region.volcano_secret_beach), - RegionData(Region.volcano_floor_5, [Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10]), - RegionData(Region.volcano_dwarf_shop), - RegionData(Region.volcano_floor_10), - RegionData(Region.island_trader), - RegionData(Region.island_farmhouse, [Entrance.island_cooking]), - RegionData(Region.gourmand_frog_cave), - RegionData(Region.colored_crystals_cave), - RegionData(Region.shipwreck), - RegionData(Region.qi_walnut_room), - RegionData(Region.leo_hut), - RegionData(Region.pirate_cove), - RegionData(Region.field_office), + [Entrance.island_west_to_islandfarmhouse, Entrance.island_west_to_gourmand_cave, Entrance.island_west_to_crystals_cave, + Entrance.island_west_to_shipwreck, Entrance.island_west_to_qi_walnut_room, Entrance.use_farm_obelisk, Entrance.parrot_express_jungle_to_docks, + Entrance.parrot_express_jungle_to_dig_site, Entrance.parrot_express_jungle_to_volcano, LogicEntrance.grow_spring_crops_on_island, + LogicEntrance.grow_summer_crops_on_island, LogicEntrance.grow_fall_crops_on_island, LogicEntrance.grow_winter_crops_on_island, LogicEntrance.grow_indoor_crops_on_island], + is_ginger_island=True), + RegionData(Region.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine], is_ginger_island=True), + RegionData(Region.island_shrine, is_ginger_island=True), + RegionData(Region.island_south_east, [Entrance.island_southeast_to_pirate_cove], is_ginger_island=True), + RegionData(Region.island_north, + [Entrance.talk_to_island_trader, Entrance.island_north_to_field_office, Entrance.island_north_to_dig_site, Entrance.island_north_to_volcano, + Entrance.parrot_express_volcano_to_dig_site, Entrance.parrot_express_volcano_to_jungle, Entrance.parrot_express_volcano_to_docks], + is_ginger_island=True), + RegionData(Region.volcano, [Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach], is_ginger_island=True), + RegionData(Region.volcano_secret_beach, is_ginger_island=True), + RegionData(Region.volcano_floor_5, [Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10], is_ginger_island=True), + RegionData(Region.volcano_dwarf_shop, is_ginger_island=True), + RegionData(Region.volcano_floor_10, is_ginger_island=True), + RegionData(Region.island_trader, is_ginger_island=True), + RegionData(Region.island_farmhouse, [LogicEntrance.island_cooking], is_ginger_island=True), + RegionData(Region.gourmand_frog_cave, is_ginger_island=True), + RegionData(Region.colored_crystals_cave, is_ginger_island=True), + RegionData(Region.shipwreck, is_ginger_island=True), + RegionData(Region.qi_walnut_room, is_ginger_island=True), + RegionData(Region.leo_hut, is_ginger_island=True), + RegionData(Region.pirate_cove, is_ginger_island=True), + RegionData(Region.field_office, is_ginger_island=True), RegionData(Region.dig_site, [Entrance.dig_site_to_professor_snail_cave, Entrance.parrot_express_dig_site_to_volcano, - Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_dig_site_to_jungle]), - RegionData(Region.professor_snail_cave), - RegionData(Region.mines, [Entrance.talk_to_mines_dwarf, + Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_dig_site_to_jungle], + is_ginger_island=True), + RegionData(Region.professor_snail_cave, is_ginger_island=True), + RegionData(Region.coop), + RegionData(Region.barn), + RegionData(Region.shed), + RegionData(Region.slime_hutch), + + RegionData(Region.mines, [LogicEntrance.talk_to_mines_dwarf, Entrance.dig_to_mines_floor_5]), - RegionData(Region.mines_dwarf_shop), RegionData(Region.mines_floor_5, [Entrance.dig_to_mines_floor_10]), RegionData(Region.mines_floor_10, [Entrance.dig_to_mines_floor_15]), RegionData(Region.mines_floor_15, [Entrance.dig_to_mines_floor_20]), @@ -218,22 +196,59 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: RegionData(Region.mines_floor_110, [Entrance.dig_to_mines_floor_115]), RegionData(Region.mines_floor_115, [Entrance.dig_to_mines_floor_120]), RegionData(Region.mines_floor_120, [Entrance.dig_to_dangerous_mines_20, Entrance.dig_to_dangerous_mines_60, Entrance.dig_to_dangerous_mines_100]), - RegionData(Region.dangerous_mines_20), - RegionData(Region.dangerous_mines_60), - RegionData(Region.dangerous_mines_100), - RegionData(Region.coop), - RegionData(Region.barn), - RegionData(Region.shed), - RegionData(Region.slime_hutch), - RegionData(Region.egg_festival), - RegionData(Region.flower_dance), - RegionData(Region.luau), - RegionData(Region.moonlight_jellies), - RegionData(Region.fair), - RegionData(Region.spirit_eve), - RegionData(Region.festival_of_ice), - RegionData(Region.night_market), - RegionData(Region.winter_star), + RegionData(Region.dangerous_mines_20, is_ginger_island=True), + RegionData(Region.dangerous_mines_60, is_ginger_island=True), + RegionData(Region.dangerous_mines_100, is_ginger_island=True), + + RegionData(LogicRegion.mines_dwarf_shop), + RegionData(LogicRegion.blacksmith_copper, [LogicEntrance.blacksmith_iron]), + RegionData(LogicRegion.blacksmith_iron, [LogicEntrance.blacksmith_gold]), + RegionData(LogicRegion.blacksmith_gold, [LogicEntrance.blacksmith_iridium]), + RegionData(LogicRegion.blacksmith_iridium), + RegionData(LogicRegion.kitchen), + RegionData(LogicRegion.queen_of_sauce), + RegionData(LogicRegion.fishing), + + RegionData(LogicRegion.spring_farming), + RegionData(LogicRegion.summer_farming, [LogicEntrance.grow_summer_fall_crops_in_summer]), + RegionData(LogicRegion.fall_farming, [LogicEntrance.grow_summer_fall_crops_in_fall]), + RegionData(LogicRegion.winter_farming), + RegionData(LogicRegion.summer_or_fall_farming), + RegionData(LogicRegion.indoor_farming), + + RegionData(LogicRegion.shipping), + RegionData(LogicRegion.traveling_cart, [LogicEntrance.buy_from_traveling_merchant_sunday, + LogicEntrance.buy_from_traveling_merchant_monday, + LogicEntrance.buy_from_traveling_merchant_tuesday, + LogicEntrance.buy_from_traveling_merchant_wednesday, + LogicEntrance.buy_from_traveling_merchant_thursday, + LogicEntrance.buy_from_traveling_merchant_friday, + LogicEntrance.buy_from_traveling_merchant_saturday]), + RegionData(LogicRegion.traveling_cart_sunday), + RegionData(LogicRegion.traveling_cart_monday), + RegionData(LogicRegion.traveling_cart_tuesday), + RegionData(LogicRegion.traveling_cart_wednesday), + RegionData(LogicRegion.traveling_cart_thursday), + RegionData(LogicRegion.traveling_cart_friday), + RegionData(LogicRegion.traveling_cart_saturday), + RegionData(LogicRegion.raccoon_daddy, [LogicEntrance.buy_from_raccoon]), + RegionData(LogicRegion.raccoon_shop), + + RegionData(LogicRegion.egg_festival), + RegionData(LogicRegion.desert_festival), + RegionData(LogicRegion.flower_dance), + RegionData(LogicRegion.luau), + RegionData(LogicRegion.trout_derby), + RegionData(LogicRegion.moonlight_jellies), + RegionData(LogicRegion.fair), + RegionData(LogicRegion.spirit_eve), + RegionData(LogicRegion.festival_of_ice), + RegionData(LogicRegion.night_market), + RegionData(LogicRegion.winter_star), + RegionData(LogicRegion.squidfest), + RegionData(LogicRegion.bookseller_1, [LogicEntrance.buy_year1_books]), + RegionData(LogicRegion.bookseller_2, [LogicEntrance.buy_year3_books]), + RegionData(LogicRegion.bookseller_3), ] # Exists and where they lead @@ -242,19 +257,15 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.to_farmhouse, Region.farm_house), ConnectionData(Entrance.farmhouse_to_farm, Region.farm), ConnectionData(Entrance.downstairs_to_cellar, Region.cellar), - ConnectionData(Entrance.farmhouse_cooking, Region.kitchen), - ConnectionData(Entrance.watch_queen_of_sauce, Region.queen_of_sauce), ConnectionData(Entrance.farm_to_backwoods, Region.backwoods), ConnectionData(Entrance.farm_to_bus_stop, Region.bus_stop), ConnectionData(Entrance.farm_to_forest, Region.forest), ConnectionData(Entrance.farm_to_farmcave, Region.farm_cave, flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData(Entrance.farming, Region.farming), ConnectionData(Entrance.enter_greenhouse, Region.greenhouse), ConnectionData(Entrance.enter_coop, Region.coop), ConnectionData(Entrance.enter_barn, Region.barn), ConnectionData(Entrance.enter_shed, Region.shed), ConnectionData(Entrance.enter_slime_hutch, Region.slime_hutch), - ConnectionData(Entrance.shipping, Region.shipping), ConnectionData(Entrance.use_desert_obelisk, Region.desert), ConnectionData(Entrance.use_island_obelisk, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.use_farm_obelisk, Region.farm), @@ -273,14 +284,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(Entrance.enter_secret_woods, Region.secret_woods), ConnectionData(Entrance.forest_to_sewer, Region.sewer, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.buy_from_traveling_merchant, Region.traveling_cart), - ConnectionData(Entrance.buy_from_traveling_merchant_sunday, Region.traveling_cart_sunday), - ConnectionData(Entrance.buy_from_traveling_merchant_monday, Region.traveling_cart_monday), - ConnectionData(Entrance.buy_from_traveling_merchant_tuesday, Region.traveling_cart_tuesday), - ConnectionData(Entrance.buy_from_traveling_merchant_wednesday, Region.traveling_cart_wednesday), - ConnectionData(Entrance.buy_from_traveling_merchant_thursday, Region.traveling_cart_thursday), - ConnectionData(Entrance.buy_from_traveling_merchant_friday, Region.traveling_cart_friday), - ConnectionData(Entrance.buy_from_traveling_merchant_saturday, Region.traveling_cart_saturday), + ConnectionData(Entrance.forest_to_mastery_cave, Region.mastery_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.MASTERIES), ConnectionData(Entrance.town_to_sewer, Region.sewer, flag=RandomizationFlag.BUILDINGS), ConnectionData(Entrance.enter_mutant_bug_lair, Region.mutant_bug_lair, flag=RandomizationFlag.BUILDINGS), ConnectionData(Entrance.mountain_to_railroad, Region.railroad), @@ -295,6 +299,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.enter_sebastian_room, Region.sebastian_room, flag=RandomizationFlag.BUILDINGS), ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), + ConnectionData(Entrance.adventurer_guild_to_bedroom, Region.adventurer_guild_bedroom), ConnectionData(Entrance.enter_quarry, Region.quarry), ConnectionData(Entrance.enter_quarry_mine_entrance, Region.quarry_mine_entrance, flag=RandomizationFlag.BUILDINGS), @@ -316,10 +321,6 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.enter_sunroom, Region.sunroom, flag=RandomizationFlag.BUILDINGS), ConnectionData(Entrance.town_to_clint_blacksmith, Region.blacksmith, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.blacksmith_copper, Region.blacksmith_copper), - ConnectionData(Entrance.blacksmith_iron, Region.blacksmith_iron), - ConnectionData(Entrance.blacksmith_gold, Region.blacksmith_gold), - ConnectionData(Entrance.blacksmith_iridium, Region.blacksmith_iridium), ConnectionData(Entrance.town_to_saloon, Region.saloon, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(Entrance.play_journey_of_the_prairie_king, Region.jotpk_world_1), @@ -354,10 +355,8 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.boat_to_ginger_island, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.enter_tide_pools, Region.tide_pools), - ConnectionData(Entrance.fishing, Region.fishing), ConnectionData(Entrance.mountain_to_the_mines, Region.mines, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.talk_to_mines_dwarf, Region.mines_dwarf_shop), ConnectionData(Entrance.dig_to_mines_floor_5, Region.mines_floor_5), ConnectionData(Entrance.dig_to_mines_floor_10, Region.mines_floor_10), ConnectionData(Entrance.dig_to_mines_floor_15, Region.mines_floor_15), @@ -416,7 +415,6 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.use_island_resort, Region.island_resort, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.island_west_to_islandfarmhouse, Region.island_farmhouse, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_cooking, Region.kitchen), ConnectionData(Entrance.island_west_to_gourmand_cave, Region.gourmand_frog_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.island_west_to_crystals_cave, Region.colored_crystals_cave, @@ -454,15 +452,62 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.parrot_express_dig_site_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.parrot_express_docks_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.parrot_express_jungle_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.attend_egg_festival, Region.egg_festival), - ConnectionData(Entrance.attend_flower_dance, Region.flower_dance), - ConnectionData(Entrance.attend_luau, Region.luau), - ConnectionData(Entrance.attend_moonlight_jellies, Region.moonlight_jellies), - ConnectionData(Entrance.attend_fair, Region.fair), - ConnectionData(Entrance.attend_spirit_eve, Region.spirit_eve), - ConnectionData(Entrance.attend_festival_of_ice, Region.festival_of_ice), - ConnectionData(Entrance.attend_night_market, Region.night_market), - ConnectionData(Entrance.attend_winter_star, Region.winter_star), + + ConnectionData(LogicEntrance.talk_to_mines_dwarf, LogicRegion.mines_dwarf_shop), + + ConnectionData(LogicEntrance.buy_from_traveling_merchant, LogicRegion.traveling_cart), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_sunday, LogicRegion.traveling_cart_sunday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_monday, LogicRegion.traveling_cart_monday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_tuesday, LogicRegion.traveling_cart_tuesday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_wednesday, LogicRegion.traveling_cart_wednesday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_thursday, LogicRegion.traveling_cart_thursday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_friday, LogicRegion.traveling_cart_friday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_saturday, LogicRegion.traveling_cart_saturday), + ConnectionData(LogicEntrance.complete_raccoon_requests, LogicRegion.raccoon_daddy), + ConnectionData(LogicEntrance.fish_in_waterfall, LogicRegion.forest_waterfall), + ConnectionData(LogicEntrance.buy_from_raccoon, LogicRegion.raccoon_shop), + ConnectionData(LogicEntrance.farmhouse_cooking, LogicRegion.kitchen), + ConnectionData(LogicEntrance.watch_queen_of_sauce, LogicRegion.queen_of_sauce), + + ConnectionData(LogicEntrance.grow_spring_crops, LogicRegion.spring_farming), + ConnectionData(LogicEntrance.grow_summer_crops, LogicRegion.summer_farming), + ConnectionData(LogicEntrance.grow_fall_crops, LogicRegion.fall_farming), + ConnectionData(LogicEntrance.grow_winter_crops, LogicRegion.winter_farming), + ConnectionData(LogicEntrance.grow_spring_crops_in_greenhouse, LogicRegion.spring_farming), + ConnectionData(LogicEntrance.grow_summer_crops_in_greenhouse, LogicRegion.summer_farming), + ConnectionData(LogicEntrance.grow_fall_crops_in_greenhouse, LogicRegion.fall_farming), + ConnectionData(LogicEntrance.grow_winter_crops_in_greenhouse, LogicRegion.winter_farming), + ConnectionData(LogicEntrance.grow_indoor_crops_in_greenhouse, LogicRegion.indoor_farming), + ConnectionData(LogicEntrance.grow_spring_crops_on_island, LogicRegion.spring_farming, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(LogicEntrance.grow_summer_crops_on_island, LogicRegion.summer_farming, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(LogicEntrance.grow_fall_crops_on_island, LogicRegion.fall_farming, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(LogicEntrance.grow_winter_crops_on_island, LogicRegion.winter_farming, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(LogicEntrance.grow_indoor_crops_on_island, LogicRegion.indoor_farming, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(LogicEntrance.grow_summer_fall_crops_in_summer, LogicRegion.summer_or_fall_farming), + ConnectionData(LogicEntrance.grow_summer_fall_crops_in_fall, LogicRegion.summer_or_fall_farming), + + ConnectionData(LogicEntrance.shipping, LogicRegion.shipping), + ConnectionData(LogicEntrance.blacksmith_copper, LogicRegion.blacksmith_copper), + ConnectionData(LogicEntrance.blacksmith_iron, LogicRegion.blacksmith_iron), + ConnectionData(LogicEntrance.blacksmith_gold, LogicRegion.blacksmith_gold), + ConnectionData(LogicEntrance.blacksmith_iridium, LogicRegion.blacksmith_iridium), + ConnectionData(LogicEntrance.fishing, LogicRegion.fishing), + ConnectionData(LogicEntrance.island_cooking, LogicRegion.kitchen), + ConnectionData(LogicEntrance.attend_egg_festival, LogicRegion.egg_festival), + ConnectionData(LogicEntrance.attend_desert_festival, LogicRegion.desert_festival), + ConnectionData(LogicEntrance.attend_flower_dance, LogicRegion.flower_dance), + ConnectionData(LogicEntrance.attend_luau, LogicRegion.luau), + ConnectionData(LogicEntrance.attend_trout_derby, LogicRegion.trout_derby), + ConnectionData(LogicEntrance.attend_moonlight_jellies, LogicRegion.moonlight_jellies), + ConnectionData(LogicEntrance.attend_fair, LogicRegion.fair), + ConnectionData(LogicEntrance.attend_spirit_eve, LogicRegion.spirit_eve), + ConnectionData(LogicEntrance.attend_festival_of_ice, LogicRegion.festival_of_ice), + ConnectionData(LogicEntrance.attend_night_market, LogicRegion.night_market), + ConnectionData(LogicEntrance.attend_winter_star, LogicRegion.winter_star), + ConnectionData(LogicEntrance.attend_squidfest, LogicRegion.squidfest), + ConnectionData(LogicEntrance.buy_experience_books, LogicRegion.bookseller_1), + ConnectionData(LogicEntrance.buy_year1_books, LogicRegion.bookseller_2), + ConnectionData(LogicEntrance.buy_year3_books, LogicRegion.bookseller_3), ] @@ -499,12 +544,22 @@ def create_final_connections_and_regions(world_options) -> Tuple[Dict[str, Conne def remove_ginger_island_regions_and_connections(regions_by_name: Dict[str, RegionData], connections: Dict[str, ConnectionData], include_island: bool): if include_island: return connections, regions_by_name - for connection_name in list(connections): + + removed_connections = set() + + for connection_name in tuple(connections): connection = connections[connection_name] if connection.flag & RandomizationFlag.GINGER_ISLAND: - regions_by_name.pop(connection.destination, None) connections.pop(connection_name) - regions_by_name = {name: regions_by_name[name].get_without_exit(connection_name) for name in regions_by_name} + removed_connections.add(connection_name) + + for region_name in tuple(regions_by_name): + region = regions_by_name[region_name] + if region.is_ginger_island: + regions_by_name.pop(region_name) + else: + regions_by_name[region_name] = region.get_without_exits(removed_connections) + return connections, regions_by_name @@ -522,7 +577,6 @@ def modify_connections_for_mods(connections: Dict[str, ConnectionData], mods) -> def modify_vanilla_regions(existing_region: RegionData, modified_region: RegionData) -> RegionData: - updated_region = existing_region region_exits = updated_region.exits modified_exits = modified_region.exits @@ -532,12 +586,16 @@ def modify_vanilla_regions(existing_region: RegionData, modified_region: RegionD return updated_region -def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewValleyOptions) -> Tuple[ - Dict[str, Region], Dict[str, Entrance], Dict[str, str]]: +def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewValleyOptions) \ + -> Tuple[Dict[str, Region], Dict[str, Entrance], Dict[str, str]]: entrances_data, regions_data = create_final_connections_and_regions(world_options) regions_by_name: Dict[str: Region] = {region_name: region_factory(region_name, regions_data[region_name].exits) for region_name in regions_data} - entrances_by_name: Dict[str: Entrance] = {entrance.name: entrance for region in regions_by_name.values() for entrance in region.exits - if entrance.name in entrances_data} + entrances_by_name: Dict[str: Entrance] = { + entrance.name: entrance + for region in regions_by_name.values() + for entrance in region.exits + if entrance.name in entrances_data + } connections, randomized_data = randomize_connections(random, world_options, regions_data, entrances_data) @@ -556,7 +614,7 @@ def randomize_connections(random: Random, world_options: StardewValleyOptions, r elif world_options.entrance_randomization == EntranceRandomization.option_non_progression: connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if RandomizationFlag.NON_PROGRESSION in connections_by_name[connection].flag] - elif world_options.entrance_randomization == EntranceRandomization.option_buildings: + elif world_options.entrance_randomization == EntranceRandomization.option_buildings or world_options.entrance_randomization == EntranceRandomization.option_buildings_without_house: connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if RandomizationFlag.BUILDINGS in connections_by_name[connection].flag] elif world_options.entrance_randomization == EntranceRandomization.option_chaos: @@ -590,6 +648,9 @@ def remove_excluded_entrances(connections_to_randomize: List[ConnectionData], wo exclude_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_true if exclude_island: connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.GINGER_ISLAND not in connection.flag] + exclude_masteries = world_options.skill_progression != SkillProgression.option_progressive_with_masteries + if exclude_masteries: + connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.MASTERIES not in connection.flag] return connections_to_randomize @@ -685,7 +746,7 @@ def swap_one_random_connection(regions_by_name, connections_by_name, randomized_ for connection in randomized_connections if connection != randomized_connections[connection]} unreachable_regions_names_leading_somewhere = tuple([region for region in unreachable_regions - if len(regions_by_name[region].exits) > 0]) + if len(regions_by_name[region].exits) > 0]) unreachable_regions_leading_somewhere = [regions_by_name[region_name] for region_name in unreachable_regions_names_leading_somewhere] unreachable_regions_exits_names = [exit_name for region in unreachable_regions_leading_somewhere for exit_name in region.exits] unreachable_connections = [connections_by_name[exit_name] for exit_name in unreachable_regions_exits_names] diff --git a/worlds/stardew_valley/requirements.txt b/worlds/stardew_valley/requirements.txt index b0922176e43b..65e922a64483 100644 --- a/worlds/stardew_valley/requirements.txt +++ b/worlds/stardew_valley/requirements.txt @@ -1 +1,2 @@ importlib_resources; python_version <= '3.8' +graphlib_backport; python_version <= '3.8' diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 8002031ac792..c30d04c8a6f2 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -1,11 +1,16 @@ import itertools +import logging from typing import List, Dict, Set -from BaseClasses import MultiWorld +from BaseClasses import MultiWorld, CollectionState from worlds.generic import Rules as MultiWorldRules from . import locations from .bundles.bundle_room import BundleRoom +from .content import StardewContent +from .content.feature import friendsanity from .data.craftable_data import all_crafting_recipes_by_name +from .data.game_item import ItemTag +from .data.harvest import HarvestCropSource, HarvestFruitTreeSource from .data.museum_data import all_museum_items, dwarf_scrolls, skeleton_front, skeleton_middle, skeleton_back, all_museum_items_by_name, all_museum_minerals, \ all_museum_artifacts, Artifact from .data.recipe_data import all_cooking_recipes_by_name @@ -14,11 +19,14 @@ from .logic.time_logic import MAX_MONTHS from .logic.tool_logic import tool_upgrade_prices from .mods.mod_data import ModNames -from .options import StardewValleyOptions, Friendsanity +from .options import StardewValleyOptions, Walnutsanity from .options import ToolProgression, BuildingProgression, ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \ - Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity, Cropsanity, SkillProgression -from .stardew_rule import And, StardewRule + Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity, SkillProgression +from .stardew_rule import And, StardewRule, true_ from .stardew_rule.indirect_connection import look_for_indirect_connection +from .stardew_rule.rule_explain import explain +from .strings.ap_names.ap_option_names import OptionName +from .strings.ap_names.community_upgrade_names import CommunityUpgrade from .strings.ap_names.event_names import Event from .strings.ap_names.mods.mod_items import SVEQuestItem, SVERunes from .strings.ap_names.transport_names import Transportation @@ -26,13 +34,15 @@ from .strings.building_names import Building from .strings.bundle_names import CCRoom from .strings.calendar_names import Weekday -from .strings.craftable_names import Bomb -from .strings.crop_names import Fruit +from .strings.craftable_names import Bomb, Furniture +from .strings.crop_names import Fruit, Vegetable from .strings.entrance_names import dig_to_mines_floor, dig_to_skull_floor, Entrance, move_to_woods_depth, DeepWoodsEntrance, AlecEntrance, \ - SVEEntrance, LaceyEntrance, BoardingHouseEntrance -from .strings.generic_names import Generic + SVEEntrance, LaceyEntrance, BoardingHouseEntrance, LogicEntrance +from .strings.forageable_names import Forageable +from .strings.geode_names import Geode from .strings.material_names import Material -from .strings.metal_names import MetalBar +from .strings.metal_names import MetalBar, Mineral +from .strings.monster_names import Monster from .strings.performance_names import Performance from .strings.quest_names import Quest from .strings.region_names import Region @@ -43,10 +53,13 @@ from .strings.villager_names import NPC, ModNPC from .strings.wallet_item_names import Wallet +logger = logging.getLogger(__name__) + def set_rules(world): multiworld = world.multiworld world_options = world.options + world_content = world.content player = world.player logic = world.logic bundle_rooms: List[BundleRoom] = world.modified_bundles @@ -58,16 +71,16 @@ def set_rules(world): set_tool_rules(logic, multiworld, player, world_options) set_skills_rules(logic, multiworld, player, world_options) - set_bundle_rules(bundle_rooms, logic, multiworld, player) + set_bundle_rules(bundle_rooms, logic, multiworld, player, world_options) set_building_rules(logic, multiworld, player, world_options) - set_cropsanity_rules(all_location_names, logic, multiworld, player, world_options) + set_cropsanity_rules(logic, multiworld, player, world_content) set_story_quests_rules(all_location_names, logic, multiworld, player, world_options) set_special_order_rules(all_location_names, logic, multiworld, player, world_options) set_help_wanted_quests_rules(logic, multiworld, player, world_options) set_fishsanity_rules(all_location_names, logic, multiworld, player) set_museumsanity_rules(all_location_names, logic, multiworld, player, world_options) - set_friendsanity_rules(all_location_names, logic, multiworld, player, world_options) + set_friendsanity_rules(logic, multiworld, player, world_content) set_backpack_rules(logic, multiworld, player, world_options) set_festival_rules(all_location_names, logic, multiworld, player) set_monstersanity_rules(all_location_names, logic, multiworld, player, world_options) @@ -75,6 +88,7 @@ def set_rules(world): set_cooksanity_rules(all_location_names, logic, multiworld, player, world_options) set_chefsanity_rules(all_location_names, logic, multiworld, player, world_options) set_craftsanity_rules(all_location_names, logic, multiworld, player, world_options) + set_booksanity_rules(logic, multiworld, player, world_content) set_isolated_locations_rules(logic, multiworld, player) set_traveling_merchant_day_rules(logic, multiworld, player) set_arcade_machine_rules(logic, multiworld, player, world_options) @@ -93,6 +107,8 @@ def set_isolated_locations_rules(logic: StardewLogic, multiworld, player): logic.money.can_spend(20000)) MultiWorldRules.add_rule(multiworld.get_location("Demetrius's Breakthrough", player), logic.money.can_have_earned_total(25000)) + MultiWorldRules.add_rule(multiworld.get_location("Pot Of Gold", player), + logic.season.has(Season.spring)) def set_tool_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): @@ -104,8 +120,10 @@ def set_tool_rules(logic: StardewLogic, multiworld, player, world_options: Stard MultiWorldRules.add_rule(multiworld.get_location("Purchase Iridium Rod", player), (logic.skill.has_level(Skill.fishing, 6) & logic.money.can_spend(7500))) + MultiWorldRules.add_rule(multiworld.get_location("Copper Pan Cutscene", player), logic.received("Glittering Boulder Removed")) + materials = [None, "Copper", "Iron", "Gold", "Iridium"] - tool = [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.watering_can, Tool.trash_can] + tool = [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can, Tool.pan] for (previous, material), tool in itertools.product(zip(materials[:4], materials[1:]), tool): if previous is None: continue @@ -124,15 +142,23 @@ def set_building_rules(logic: StardewLogic, multiworld, player, world_options: S logic.registry.building_rules[building.name.replace(" Blueprint", "")]) -def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiworld, player): +def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): for bundle_room in bundle_rooms: room_rules = [] for bundle in bundle_room.bundles: location = multiworld.get_location(bundle.name, player) bundle_rules = logic.bundle.can_complete_bundle(bundle) + if bundle_room.name == CCRoom.raccoon_requests: + num = int(bundle.name[-1]) + extra_raccoons = 1 if world_options.quest_locations >= 0 else 0 + extra_raccoons = extra_raccoons + num + bundle_rules = logic.received(CommunityUpgrade.raccoon, extra_raccoons) & bundle_rules + if num > 1: + previous_bundle_name = f"Raccoon Request {num-1}" + bundle_rules = bundle_rules & logic.region.can_reach_location(previous_bundle_name) room_rules.append(bundle_rules) MultiWorldRules.set_rule(location, bundle_rules) - if bundle_room.name == CCRoom.abandoned_joja_mart: + if bundle_room.name == CCRoom.abandoned_joja_mart or bundle_room.name == CCRoom.raccoon_requests: continue room_location = f"Complete {bundle_room.name}" MultiWorldRules.add_rule(multiworld.get_location(room_location, player), And(*room_rules)) @@ -145,6 +171,10 @@ def set_skills_rules(logic: StardewLogic, multiworld, player, world_options: Sta for i in range(1, 11): set_vanilla_skill_rule_for_level(logic, multiworld, player, i) set_modded_skill_rule_for_level(logic, multiworld, player, mods, i) + if world_options.skill_progression != SkillProgression.option_progressive_with_masteries: + return + for skill in [Skill.farming, Skill.fishing, Skill.foraging, Skill.mining, Skill.combat]: + MultiWorldRules.set_rule(multiworld.get_location(f"{skill} Mastery", player), logic.skill.can_earn_mastery_experience) def set_vanilla_skill_rule_for_level(logic: StardewLogic, multiworld, player, level: int): @@ -181,7 +211,7 @@ def set_vanilla_skill_rule(logic: StardewLogic, multiworld, player, skill: str, def set_modded_skill_rule(logic: StardewLogic, multiworld, player, skill: str, level: int): - rule = logic.mod.skill.can_earn_mod_skill_level(skill, level) + rule = logic.skill.can_earn_level(skill, level) MultiWorldRules.set_rule(get_skill_level_location(multiworld, player, skill, level), rule) @@ -189,7 +219,7 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S set_mines_floor_entrance_rules(logic, multiworld, player) set_skull_cavern_floor_entrance_rules(logic, multiworld, player) set_blacksmith_entrance_rules(logic, multiworld, player) - set_skill_entrance_rules(logic, multiworld, player) + set_skill_entrance_rules(logic, multiworld, player, world_options) set_traveling_merchant_day_rules(logic, multiworld, player) set_dangerous_mine_rules(logic, multiworld, player, world_options) @@ -204,8 +234,12 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S set_entrance_rule(multiworld, player, Entrance.purchase_movie_ticket, movie_theater_rule) set_entrance_rule(multiworld, player, Entrance.take_bus_to_desert, logic.received("Bus Repair")) set_entrance_rule(multiworld, player, Entrance.enter_skull_cavern, logic.received(Wallet.skull_key)) - set_entrance_rule(multiworld, player, Entrance.talk_to_mines_dwarf, logic.wallet.can_speak_dwarf() & logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron)) - set_entrance_rule(multiworld, player, Entrance.buy_from_traveling_merchant, logic.traveling_merchant.has_days()) + set_entrance_rule(multiworld, player, LogicEntrance.talk_to_mines_dwarf, + logic.wallet.can_speak_dwarf() & logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron)) + set_entrance_rule(multiworld, player, LogicEntrance.buy_from_traveling_merchant, logic.traveling_merchant.has_days()) + set_entrance_rule(multiworld, player, LogicEntrance.buy_from_raccoon, logic.quest.has_raccoon_shop()) + set_entrance_rule(multiworld, player, LogicEntrance.fish_in_waterfall, + logic.skill.has_level(Skill.fishing, 5) & logic.tool.has_fishing_rod(2)) set_farm_buildings_entrance_rules(logic, multiworld, player) @@ -218,10 +252,15 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S set_bedroom_entrance_rules(logic, multiworld, player, world_options) set_festival_entrance_rules(logic, multiworld, player) - set_island_entrance_rule(multiworld, player, Entrance.island_cooking, logic.cooking.can_cook_in_kitchen, world_options) - set_entrance_rule(multiworld, player, Entrance.farmhouse_cooking, logic.cooking.can_cook_in_kitchen) - set_entrance_rule(multiworld, player, Entrance.shipping, logic.shipping.can_use_shipping_bin) - set_entrance_rule(multiworld, player, Entrance.watch_queen_of_sauce, logic.action.can_watch(Channel.queen_of_sauce)) + set_island_entrance_rule(multiworld, player, LogicEntrance.island_cooking, logic.cooking.can_cook_in_kitchen, world_options) + set_entrance_rule(multiworld, player, LogicEntrance.farmhouse_cooking, logic.cooking.can_cook_in_kitchen) + set_entrance_rule(multiworld, player, LogicEntrance.shipping, logic.shipping.can_use_shipping_bin) + set_entrance_rule(multiworld, player, LogicEntrance.watch_queen_of_sauce, logic.action.can_watch(Channel.queen_of_sauce)) + set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave()) + set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave()) + set_entrance_rule(multiworld, player, LogicEntrance.buy_experience_books, logic.time.has_lived_months(2)) + set_entrance_rule(multiworld, player, LogicEntrance.buy_year1_books, logic.time.has_year_two) + set_entrance_rule(multiworld, player, LogicEntrance.buy_year3_books, logic.time.has_year_three) def set_dangerous_mine_rules(logic, multiworld, player, world_options: StardewValleyOptions): @@ -236,6 +275,7 @@ def set_dangerous_mine_rules(logic, multiworld, player, world_options: StardewVa def set_farm_buildings_entrance_rules(logic, multiworld, player): + set_entrance_rule(multiworld, player, Entrance.downstairs_to_cellar, logic.building.has_house(3)) set_entrance_rule(multiworld, player, Entrance.use_desert_obelisk, logic.can_use_obelisk(Transportation.desert_obelisk)) set_entrance_rule(multiworld, player, Entrance.enter_greenhouse, logic.received("Greenhouse")) set_entrance_rule(multiworld, player, Entrance.enter_coop, logic.building.has_building(Building.coop)) @@ -277,15 +317,28 @@ def set_skull_cavern_floor_entrance_rules(logic, multiworld, player): def set_blacksmith_entrance_rules(logic, multiworld, player): - set_blacksmith_upgrade_rule(logic, multiworld, player, Entrance.blacksmith_copper, MetalBar.copper, ToolMaterial.copper) - set_blacksmith_upgrade_rule(logic, multiworld, player, Entrance.blacksmith_iron, MetalBar.iron, ToolMaterial.iron) - set_blacksmith_upgrade_rule(logic, multiworld, player, Entrance.blacksmith_gold, MetalBar.gold, ToolMaterial.gold) - set_blacksmith_upgrade_rule(logic, multiworld, player, Entrance.blacksmith_iridium, MetalBar.iridium, ToolMaterial.iridium) - - -def set_skill_entrance_rules(logic, multiworld, player): - set_entrance_rule(multiworld, player, Entrance.farming, logic.skill.can_get_farming_xp) - set_entrance_rule(multiworld, player, Entrance.fishing, logic.skill.can_get_fishing_xp) + set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_copper, MetalBar.copper, ToolMaterial.copper) + set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iron, MetalBar.iron, ToolMaterial.iron) + set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_gold, MetalBar.gold, ToolMaterial.gold) + set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iridium, MetalBar.iridium, ToolMaterial.iridium) + + +def set_skill_entrance_rules(logic, multiworld, player, world_options: StardewValleyOptions): + set_entrance_rule(multiworld, player, LogicEntrance.grow_spring_crops, logic.farming.has_farming_tools & logic.season.has_spring) + set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_crops, logic.farming.has_farming_tools & logic.season.has_summer) + set_entrance_rule(multiworld, player, LogicEntrance.grow_fall_crops, logic.farming.has_farming_tools & logic.season.has_fall) + set_entrance_rule(multiworld, player, LogicEntrance.grow_spring_crops_in_greenhouse, logic.farming.has_farming_tools) + set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_crops_in_greenhouse, logic.farming.has_farming_tools) + set_entrance_rule(multiworld, player, LogicEntrance.grow_fall_crops_in_greenhouse, logic.farming.has_farming_tools) + set_entrance_rule(multiworld, player, LogicEntrance.grow_indoor_crops_in_greenhouse, logic.farming.has_farming_tools) + set_island_entrance_rule(multiworld, player, LogicEntrance.grow_spring_crops_on_island, logic.farming.has_farming_tools, world_options) + set_island_entrance_rule(multiworld, player, LogicEntrance.grow_summer_crops_on_island, logic.farming.has_farming_tools, world_options) + set_island_entrance_rule(multiworld, player, LogicEntrance.grow_fall_crops_on_island, logic.farming.has_farming_tools, world_options) + set_island_entrance_rule(multiworld, player, LogicEntrance.grow_indoor_crops_on_island, logic.farming.has_farming_tools, world_options) + set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_fall_crops_in_summer, true_) + set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_fall_crops_in_fall, true_) + + set_entrance_rule(multiworld, player, LogicEntrance.fishing, logic.skill.can_get_fishing_xp) def set_blacksmith_upgrade_rule(logic, multiworld, player, entrance_name: str, item_name: str, tool_material: str): @@ -295,18 +348,21 @@ def set_blacksmith_upgrade_rule(logic, multiworld, player, entrance_name: str, i def set_festival_entrance_rules(logic, multiworld, player): - set_entrance_rule(multiworld, player, Entrance.attend_egg_festival, logic.season.has(Season.spring)) - set_entrance_rule(multiworld, player, Entrance.attend_flower_dance, logic.season.has(Season.spring)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_egg_festival, logic.season.has(Season.spring)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_desert_festival, logic.season.has(Season.spring) & logic.received("Bus Repair")) + set_entrance_rule(multiworld, player, LogicEntrance.attend_flower_dance, logic.season.has(Season.spring)) - set_entrance_rule(multiworld, player, Entrance.attend_luau, logic.season.has(Season.summer)) - set_entrance_rule(multiworld, player, Entrance.attend_moonlight_jellies, logic.season.has(Season.summer)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_luau, logic.season.has(Season.summer)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_trout_derby, logic.season.has(Season.summer)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_moonlight_jellies, logic.season.has(Season.summer)) - set_entrance_rule(multiworld, player, Entrance.attend_fair, logic.season.has(Season.fall)) - set_entrance_rule(multiworld, player, Entrance.attend_spirit_eve, logic.season.has(Season.fall)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_fair, logic.season.has(Season.fall)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_spirit_eve, logic.season.has(Season.fall)) - set_entrance_rule(multiworld, player, Entrance.attend_festival_of_ice, logic.season.has(Season.winter)) - set_entrance_rule(multiworld, player, Entrance.attend_night_market, logic.season.has(Season.winter)) - set_entrance_rule(multiworld, player, Entrance.attend_winter_star, logic.season.has(Season.winter)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_festival_of_ice, logic.season.has(Season.winter)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_squidfest, logic.season.has(Season.winter)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_night_market, logic.season.has(Season.winter)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_winter_star, logic.season.has(Season.winter)) def set_ginger_island_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): @@ -320,6 +376,7 @@ def set_ginger_island_rules(logic: StardewLogic, multiworld, player, world_optio logic.has(Bomb.cherry_bomb)) MultiWorldRules.add_rule(multiworld.get_location("Complete Island Field Office", player), logic.can_complete_field_office()) + set_walnut_rules(logic, multiworld, player, world_options) def set_boat_repair_rules(logic: StardewLogic, multiworld, player): @@ -374,10 +431,11 @@ def set_island_entrances_rules(logic: StardewLogic, multiworld, player, world_op def set_island_parrot_rules(logic: StardewLogic, multiworld, player): - has_walnut = logic.has_walnut(1) - has_5_walnut = logic.has_walnut(5) - has_10_walnut = logic.has_walnut(10) - has_20_walnut = logic.has_walnut(20) + # Logic rules require more walnuts than in reality, to allow the player to spend them "wrong" + has_walnut = logic.has_walnut(5) + has_5_walnut = logic.has_walnut(15) + has_10_walnut = logic.has_walnut(40) + has_20_walnut = logic.has_walnut(60) MultiWorldRules.add_rule(multiworld.get_location("Leo's Parrot", player), has_walnut) MultiWorldRules.add_rule(multiworld.get_location("Island West Turtle", player), @@ -403,17 +461,82 @@ def set_island_parrot_rules(logic: StardewLogic, multiworld, player): has_10_walnut) -def set_cropsanity_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): - if world_options.cropsanity == Cropsanity.option_disabled: +def set_walnut_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): + if world_options.walnutsanity == Walnutsanity.preset_none: + return + + set_walnut_puzzle_rules(logic, multiworld, player, world_options) + set_walnut_bushes_rules(logic, multiworld, player, world_options) + set_walnut_dig_spot_rules(logic, multiworld, player, world_options) + set_walnut_repeatable_rules(logic, multiworld, player, world_options) + + +def set_walnut_puzzle_rules(logic, multiworld, player, world_options): + if OptionName.walnutsanity_puzzles not in world_options.walnutsanity: + return + + MultiWorldRules.add_rule(multiworld.get_location("Open Golden Coconut", player), logic.has(Geode.golden_coconut)) + MultiWorldRules.add_rule(multiworld.get_location("Banana Altar", player), logic.has(Fruit.banana)) + MultiWorldRules.add_rule(multiworld.get_location("Leo's Tree", player), logic.tool.has_tool(Tool.axe)) + MultiWorldRules.add_rule(multiworld.get_location("Gem Birds Shrine", player), logic.has(Mineral.amethyst) & logic.has(Mineral.aquamarine) & + logic.has(Mineral.emerald) & logic.has(Mineral.ruby) & logic.has(Mineral.topaz) & + logic.region.can_reach_all((Region.island_north, Region.island_west, Region.island_east, Region.island_south))) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Melon", player), logic.has(Fruit.melon) & logic.region.can_reach(Region.island_west)) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Wheat", player), logic.has(Vegetable.wheat) & logic.region.can_reach(Region.island_west)) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) & logic.region.can_reach(Region.island_west)) + MultiWorldRules.add_rule(multiworld.get_location("Whack A Mole", player), logic.tool.has_tool(Tool.watering_can, ToolMaterial.iridium)) + MultiWorldRules.add_rule(multiworld.get_location("Complete Large Animal Collection", player), logic.can_complete_large_animal_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Snake Collection", player), logic.can_complete_snake_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Frog Collection", player), logic.can_complete_frog_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Bat Collection", player), logic.can_complete_bat_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Purple Flowers Island Survey", player), logic.can_start_field_office) + MultiWorldRules.add_rule(multiworld.get_location("Purple Starfish Island Survey", player), logic.can_start_field_office) + MultiWorldRules.add_rule(multiworld.get_location("Protruding Tree Walnut", player), logic.combat.has_slingshot) + MultiWorldRules.add_rule(multiworld.get_location("Starfish Tide Pool", player), logic.tool.has_fishing_rod(1)) + MultiWorldRules.add_rule(multiworld.get_location("Mermaid Song", player), logic.has(Furniture.flute_block)) + + +def set_walnut_bushes_rules(logic, multiworld, player, world_options): + if OptionName.walnutsanity_bushes not in world_options.walnutsanity: + return + # I don't think any of the bushes require something special, but that might change with ER + return + + +def set_walnut_dig_spot_rules(logic, multiworld, player, world_options): + if OptionName.walnutsanity_dig_spots not in world_options.walnutsanity: return - harvest_prefix = "Harvest " - harvest_prefix_length = len(harvest_prefix) - for harvest_location in locations.locations_by_tag[LocationTags.CROPSANITY]: - if harvest_location.name in all_location_names and (harvest_location.mod_name is None or harvest_location.mod_name in world_options.mods): - crop_name = harvest_location.name[harvest_prefix_length:] - MultiWorldRules.set_rule(multiworld.get_location(harvest_location.name, player), - logic.has(crop_name)) + for dig_spot_walnut in locations.locations_by_tag[LocationTags.WALNUTSANITY_DIG]: + rule = logic.tool.has_tool(Tool.hoe) + if "Journal Scrap" in dig_spot_walnut.name: + rule = rule & logic.has(Forageable.journal_scrap) + if "Starfish Diamond" in dig_spot_walnut.name: + rule = rule & logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron) + MultiWorldRules.set_rule(multiworld.get_location(dig_spot_walnut.name, player), rule) + + +def set_walnut_repeatable_rules(logic, multiworld, player, world_options): + if OptionName.walnutsanity_repeatables not in world_options.walnutsanity: + return + for i in range(1, 6): + MultiWorldRules.set_rule(multiworld.get_location(f"Fishing Walnut {i}", player), logic.tool.has_fishing_rod(1)) + MultiWorldRules.set_rule(multiworld.get_location(f"Harvesting Walnut {i}", player), logic.skill.can_get_farming_xp) + MultiWorldRules.set_rule(multiworld.get_location(f"Mussel Node Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe)) + MultiWorldRules.set_rule(multiworld.get_location(f"Volcano Rocks Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe)) + MultiWorldRules.set_rule(multiworld.get_location(f"Volcano Monsters Walnut {i}", player), logic.combat.has_galaxy_weapon) + MultiWorldRules.set_rule(multiworld.get_location(f"Volcano Crates Walnut {i}", player), logic.combat.has_any_weapon) + MultiWorldRules.set_rule(multiworld.get_location(f"Tiger Slime Walnut", player), logic.monster.can_kill(Monster.tiger_slime)) + + +def set_cropsanity_rules(logic: StardewLogic, multiworld, player, world_content: StardewContent): + if not world_content.features.cropsanity.is_enabled: + return + + for item in world_content.find_tagged_items(ItemTag.CROPSANITY): + location = world_content.features.cropsanity.to_location_name(item.name) + harvest_sources = (source for source in item.sources if isinstance(source, (HarvestFruitTreeSource, HarvestCropSource))) + MultiWorldRules.set_rule(multiworld.get_location(location, player), logic.source.has_access_to_any(harvest_sources)) def set_story_quests_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): @@ -427,23 +550,21 @@ def set_story_quests_rules(all_location_names: Set[str], logic: StardewLogic, mu def set_special_order_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): - if world_options.special_order_locations == SpecialOrderLocations.option_disabled: - return - board_rule = logic.received("Special Order Board") & logic.time.has_lived_months(4) - for board_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: - if board_order.name in all_location_names: - order_rule = board_rule & logic.registry.special_order_rules[board_order.name] - MultiWorldRules.set_rule(multiworld.get_location(board_order.name, player), order_rule) + if world_options.special_order_locations & SpecialOrderLocations.option_board: + board_rule = logic.received("Special Order Board") & logic.time.has_lived_months(4) + for board_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: + if board_order.name in all_location_names: + order_rule = board_rule & logic.registry.special_order_rules[board_order.name] + MultiWorldRules.set_rule(multiworld.get_location(board_order.name, player), order_rule) if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true: return - if world_options.special_order_locations == SpecialOrderLocations.option_board_only: - return - qi_rule = logic.region.can_reach(Region.qi_walnut_room) & logic.time.has_lived_months(8) - for qi_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_QI]: - if qi_order.name in all_location_names: - order_rule = qi_rule & logic.registry.special_order_rules[qi_order.name] - MultiWorldRules.set_rule(multiworld.get_location(qi_order.name, player), order_rule) + if world_options.special_order_locations & SpecialOrderLocations.value_qi: + qi_rule = logic.region.can_reach(Region.qi_walnut_room) & logic.time.has_lived_months(8) + for qi_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_QI]: + if qi_order.name in all_location_names: + order_rule = qi_rule & logic.registry.special_order_rules[qi_order.name] + MultiWorldRules.set_rule(multiworld.get_location(qi_order.name, player), order_rule) help_wanted_prefix = "Help Wanted:" @@ -730,6 +851,21 @@ def set_craftsanity_rules(all_location_names: Set[str], logic: StardewLogic, mul MultiWorldRules.set_rule(multiworld.get_location(location.name, player), craft_rule) +def set_booksanity_rules(logic: StardewLogic, multiworld, player, content: StardewContent): + booksanity = content.features.booksanity + if not booksanity.is_enabled: + return + + for book in content.find_tagged_items(ItemTag.BOOK): + if booksanity.is_included(book): + MultiWorldRules.set_rule(multiworld.get_location(booksanity.to_location_name(book.name), player), logic.has(book.name)) + + for i, book in enumerate(booksanity.get_randomized_lost_books()): + if i <= 0: + continue + MultiWorldRules.set_rule(multiworld.get_location(booksanity.to_location_name(book), player), logic.received(booksanity.progressive_lost_book, i)) + + def set_traveling_merchant_day_rules(logic: StardewLogic, multiworld: MultiWorld, player: int): for day in Weekday.all_days: item_for_day = f"Traveling Merchant: {day}" @@ -761,28 +897,26 @@ def set_arcade_machine_rules(logic: StardewLogic, multiworld: MultiWorld, player logic.has("JotPK Max Buff")) -def set_friendsanity_rules(all_location_names: Set[str], logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): - if world_options.friendsanity == Friendsanity.option_none: +def set_friendsanity_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, content: StardewContent): + if not content.features.friendsanity.is_enabled: return MultiWorldRules.add_rule(multiworld.get_location("Spouse Stardrop", player), - logic.relationship.has_hearts(Generic.bachelor, 13)) + logic.relationship.has_hearts_with_any_bachelor(13)) MultiWorldRules.add_rule(multiworld.get_location("Have a Baby", player), logic.relationship.can_reproduce(1)) MultiWorldRules.add_rule(multiworld.get_location("Have Another Baby", player), logic.relationship.can_reproduce(2)) - friend_prefix = "Friendsanity: " - friend_suffix = " <3" - for friend_location in locations.locations_by_tag[LocationTags.FRIENDSANITY]: - if not friend_location.name in all_location_names: - continue - friend_location_without_prefix = friend_location.name[len(friend_prefix):] - friend_location_trimmed = friend_location_without_prefix[:friend_location_without_prefix.index(friend_suffix)] - split_index = friend_location_trimmed.rindex(" ") - friend_name = friend_location_trimmed[:split_index] - num_hearts = int(friend_location_trimmed[split_index + 1:]) - MultiWorldRules.set_rule(multiworld.get_location(friend_location.name, player), - logic.relationship.can_earn_relationship(friend_name, num_hearts)) + for villager in content.villagers.values(): + for heart in content.features.friendsanity.get_randomized_hearts(villager): + rule = logic.relationship.can_earn_relationship(villager.name, heart) + location_name = friendsanity.to_location_name(villager.name, heart) + MultiWorldRules.set_rule(multiworld.get_location(location_name, player), rule) + + for heart in content.features.friendsanity.get_pet_randomized_hearts(): + rule = logic.pet.can_befriend_pet(heart) + location_name = friendsanity.to_location_name(NPC.pet, heart) + MultiWorldRules.set_rule(multiworld.get_location(location_name, player), rule) def set_deepwoods_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): @@ -876,7 +1010,7 @@ def set_sve_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, worl set_entrance_rule(multiworld, player, SVEEntrance.outpost_warp_to_outpost, logic.received(SVERunes.nexus_outpost)) set_entrance_rule(multiworld, player, SVEEntrance.wizard_warp_to_wizard, logic.received(SVERunes.nexus_wizard)) set_entrance_rule(multiworld, player, SVEEntrance.use_purple_junimo, logic.relationship.has_hearts(ModNPC.apples, 10)) - set_entrance_rule(multiworld, player, SVEEntrance.grandpa_interior_to_upstairs, logic.received(SVEQuestItem.grandpa_shed)) + set_entrance_rule(multiworld, player, SVEEntrance.grandpa_interior_to_upstairs, logic.mod.sve.has_grandpa_shed_repaired()) set_entrance_rule(multiworld, player, SVEEntrance.use_bear_shop, (logic.mod.sve.can_buy_bear_recipe())) set_entrance_rule(multiworld, player, SVEEntrance.railroad_to_grampleton_station, logic.received(SVEQuestItem.scarlett_job_offer)) set_entrance_rule(multiworld, player, SVEEntrance.museum_to_gunther_bedroom, logic.relationship.has_hearts(ModNPC.gunther, 2)) @@ -891,7 +1025,7 @@ def set_sve_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, worl def set_sve_ginger_island_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true: return - set_entrance_rule(multiworld, player, SVEEntrance.summit_to_highlands, logic.received(SVEQuestItem.marlon_boat_paddle)) + set_entrance_rule(multiworld, player, SVEEntrance.summit_to_highlands, logic.mod.sve.has_marlon_boat()) set_entrance_rule(multiworld, player, SVEEntrance.wizard_to_fable_reef, logic.received(SVEQuestItem.fable_reef_portal)) set_entrance_rule(multiworld, player, SVEEntrance.highlands_to_cave, logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron) & logic.tool.has_tool(Tool.axe, ToolMaterial.iron)) @@ -904,12 +1038,16 @@ def set_boarding_house_rules(logic: StardewLogic, multiworld: MultiWorld, player def set_entrance_rule(multiworld, player, entrance: str, rule: StardewRule): - potentially_required_regions = look_for_indirect_connection(rule) - if potentially_required_regions: - for region in potentially_required_regions: - multiworld.register_indirect_condition(multiworld.get_region(region, player), multiworld.get_entrance(entrance, player)) - - MultiWorldRules.set_rule(multiworld.get_entrance(entrance, player), rule) + try: + potentially_required_regions = look_for_indirect_connection(rule) + if potentially_required_regions: + for region in potentially_required_regions: + multiworld.register_indirect_condition(multiworld.get_region(region, player), multiworld.get_entrance(entrance, player)) + + MultiWorldRules.set_rule(multiworld.get_entrance(entrance, player), rule) + except KeyError as ex: + logger.error(f"""Failed to evaluate indirect connection in: {explain(rule, CollectionState(multiworld))}""") + raise ex def set_island_entrance_rule(multiworld, player, entrance: str, rule: StardewRule, world_options: StardewValleyOptions): diff --git a/worlds/stardew_valley/scripts/export_locations.py b/worlds/stardew_valley/scripts/export_locations.py index 1dc60f79b14b..c181faec7b94 100644 --- a/worlds/stardew_valley/scripts/export_locations.py +++ b/worlds/stardew_valley/scripts/export_locations.py @@ -16,11 +16,17 @@ if __name__ == "__main__": with open("output/stardew_valley_location_table.json", "w+") as f: locations = { + "Cheat Console": + {"code": -1, "region": "Archipelago"}, + "Server": + {"code": -2, "region": "Archipelago"} + } + locations.update({ location.name: { "code": location.code, "region": location.region, } for location in location_table.values() if location.code is not None - } + }) json.dump({"locations": locations}, f) diff --git a/worlds/stardew_valley/stardew_rule/base.py b/worlds/stardew_valley/stardew_rule/base.py index 007d2b64dc41..576cd36851fb 100644 --- a/worlds/stardew_valley/stardew_rule/base.py +++ b/worlds/stardew_valley/stardew_rule/base.py @@ -1,7 +1,8 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections import deque +from collections import deque, Counter +from dataclasses import dataclass, field from functools import cached_property from itertools import chain from threading import Lock @@ -295,7 +296,10 @@ def __eq__(self, other): self.simplification_state.original_simplifiable_rules == self.simplification_state.original_simplifiable_rules) def __hash__(self): - return hash((id(self.combinable_rules), self.simplification_state.original_simplifiable_rules)) + if len(self.combinable_rules) + len(self.simplification_state.original_simplifiable_rules) > 5: + return id(self) + + return hash((*self.combinable_rules.values(), self.simplification_state.original_simplifiable_rules)) class Or(AggregatingStardewRule): @@ -323,9 +327,6 @@ def __or__(self, other): def combine(left: CombinableStardewRule, right: CombinableStardewRule) -> CombinableStardewRule: return min(left, right, key=lambda x: x.value) - def get_difficulty(self): - return min(rule.get_difficulty() for rule in self.original_rules) - class And(AggregatingStardewRule): identity = true_ @@ -352,19 +353,34 @@ def __and__(self, other): def combine(left: CombinableStardewRule, right: CombinableStardewRule) -> CombinableStardewRule: return max(left, right, key=lambda x: x.value) - def get_difficulty(self): - return max(rule.get_difficulty() for rule in self.original_rules) - class Count(BaseStardewRule): count: int rules: List[StardewRule] + counter: Counter[StardewRule] + evaluate: Callable[[CollectionState], bool] + + total: Optional[int] + rule_mapping: Optional[Dict[StardewRule, StardewRule]] def __init__(self, rules: List[StardewRule], count: int): - self.rules = rules self.count = count + self.counter = Counter(rules) + + if len(self.counter) / len(rules) < .66: + # Checking if it's worth using the count operation with shortcircuit or not. Value should be fine-tuned when Count has more usage. + self.total = sum(self.counter.values()) + self.rules = sorted(self.counter.keys(), key=lambda x: self.counter[x], reverse=True) + self.rule_mapping = {} + self.evaluate = self.evaluate_with_shortcircuit + else: + self.rules = rules + self.evaluate = self.evaluate_without_shortcircuit - def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: + def __call__(self, state: CollectionState) -> bool: + return self.evaluate(state) + + def evaluate_without_shortcircuit(self, state: CollectionState) -> bool: c = 0 for i in range(self.rules_count): self.rules[i], value = self.rules[i].evaluate_while_simplifying(state) @@ -372,37 +388,58 @@ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRul c += 1 if c >= self.count: - return self, True + return True if c + self.rules_count - i < self.count: break - return self, False + return False - def __call__(self, state: CollectionState) -> bool: - return self.evaluate_while_simplifying(state)[1] + def evaluate_with_shortcircuit(self, state: CollectionState) -> bool: + c = 0 + t = self.total + + for rule in self.rules: + evaluation_value = self.call_evaluate_while_simplifying_cached(rule, state) + rule_value = self.counter[rule] + + if evaluation_value: + c += rule_value + else: + t -= rule_value + + if c >= self.count: + return True + elif t < self.count: + break + + return False + + def call_evaluate_while_simplifying_cached(self, rule: StardewRule, state: CollectionState) -> bool: + try: + # A mapping table with the original rule is used here because two rules could resolve to the same rule. + # This would require to change the counter to merge both rules, and quickly become complicated. + return self.rule_mapping[rule](state) + except KeyError: + self.rule_mapping[rule], value = rule.evaluate_while_simplifying(state) + return value + + def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: + return self, self(state) @cached_property def rules_count(self): return len(self.rules) - def get_difficulty(self): - self.rules = sorted(self.rules, key=lambda x: x.get_difficulty()) - # In an optimal situation, all the simplest rules will be true. Since the rules are sorted, we know that the most difficult rule we might have to do - # is the one at the "self.count". - return self.rules[self.count - 1].get_difficulty() - def __repr__(self): return f"Received {self.count} {repr(self.rules)}" +@dataclass(frozen=True) class Has(BaseStardewRule): item: str # For sure there is a better way than just passing all the rules everytime - other_rules: Dict[str, StardewRule] - - def __init__(self, item: str, other_rules: Dict[str, StardewRule]): - self.item = item - self.other_rules = other_rules + other_rules: Dict[str, StardewRule] = field(repr=False, hash=False, compare=False) + group: str = "item" def __call__(self, state: CollectionState) -> bool: return self.evaluate_while_simplifying(state)[1] @@ -410,21 +447,15 @@ def __call__(self, state: CollectionState) -> bool: def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: return self.other_rules[self.item].evaluate_while_simplifying(state) - def get_difficulty(self): - return self.other_rules[self.item].get_difficulty() + 1 - def __str__(self): if self.item not in self.other_rules: - return f"Has {self.item} -> {MISSING_ITEM}" - return f"Has {self.item}" + return f"Has {self.item} ({self.group}) -> {MISSING_ITEM}" + return f"Has {self.item} ({self.group})" def __repr__(self): if self.item not in self.other_rules: - return f"Has {self.item} -> {MISSING_ITEM}" - return f"Has {self.item} -> {repr(self.other_rules[self.item])}" - - def __hash__(self): - return hash(self.item) + return f"Has {self.item} ({self.group}) -> {MISSING_ITEM}" + return f"Has {self.item} ({self.group}) -> {repr(self.other_rules[self.item])}" class RepeatableChain(Iterable, Sized): diff --git a/worlds/stardew_valley/stardew_rule/indirect_connection.py b/worlds/stardew_valley/stardew_rule/indirect_connection.py index 2bbddb16818f..17433f7df4a8 100644 --- a/worlds/stardew_valley/stardew_rule/indirect_connection.py +++ b/worlds/stardew_valley/stardew_rule/indirect_connection.py @@ -6,34 +6,38 @@ def look_for_indirect_connection(rule: StardewRule) -> Set[str]: required_regions = set() - _find(rule, required_regions) + _find(rule, required_regions, depth=0) return required_regions @singledispatch -def _find(rule: StardewRule, regions: Set[str]): +def _find(rule: StardewRule, regions: Set[str], depth: int): ... @_find.register -def _(rule: AggregatingStardewRule, regions: Set[str]): +def _(rule: AggregatingStardewRule, regions: Set[str], depth: int): + assert depth < 50, "Recursion depth exceeded" for r in rule.original_rules: - _find(r, regions) + _find(r, regions, depth + 1) @_find.register -def _(rule: Count, regions: Set[str]): +def _(rule: Count, regions: Set[str], depth: int): + assert depth < 50, "Recursion depth exceeded" for r in rule.rules: - _find(r, regions) + _find(r, regions, depth + 1) @_find.register -def _(rule: Has, regions: Set[str]): +def _(rule: Has, regions: Set[str], depth: int): + assert depth < 50, f"Recursion depth exceeded on {rule.item}" r = rule.other_rules[rule.item] - _find(r, regions) + _find(r, regions, depth + 1) @_find.register -def _(rule: Reach, regions: Set[str]): +def _(rule: Reach, regions: Set[str], depth: int): + assert depth < 50, "Recursion depth exceeded" if rule.resolution_hint == "Region": regions.add(rule.spot) diff --git a/worlds/stardew_valley/stardew_rule/literal.py b/worlds/stardew_valley/stardew_rule/literal.py index 58f7bae047fa..93a8503e8739 100644 --- a/worlds/stardew_valley/stardew_rule/literal.py +++ b/worlds/stardew_valley/stardew_rule/literal.py @@ -33,9 +33,6 @@ def __or__(self, other) -> StardewRule: def __and__(self, other) -> StardewRule: return other - def get_difficulty(self): - return 0 - class False_(LiteralStardewRule): # noqa value = False @@ -52,9 +49,6 @@ def __or__(self, other) -> StardewRule: def __and__(self, other) -> StardewRule: return self - def get_difficulty(self): - return 999999999 - false_ = False_() true_ = True_() diff --git a/worlds/stardew_valley/stardew_rule/protocol.py b/worlds/stardew_valley/stardew_rule/protocol.py index c20394d5b826..f69a3663c63a 100644 --- a/worlds/stardew_valley/stardew_rule/protocol.py +++ b/worlds/stardew_valley/stardew_rule/protocol.py @@ -24,7 +24,3 @@ def __or__(self, other: StardewRule): @abstractmethod def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: ... - - @abstractmethod - def get_difficulty(self): - ... diff --git a/worlds/stardew_valley/stardew_rule/rule_explain.py b/worlds/stardew_valley/stardew_rule/rule_explain.py new file mode 100644 index 000000000000..61a88ceb6996 --- /dev/null +++ b/worlds/stardew_valley/stardew_rule/rule_explain.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from functools import cached_property, singledispatch +from typing import Iterable, Set, Tuple, List, Optional + +from BaseClasses import CollectionState +from worlds.generic.Rules import CollectionRule +from . import StardewRule, AggregatingStardewRule, Count, Has, TotalReceived, Received, Reach, true_ + + +@dataclass +class RuleExplanation: + rule: StardewRule + state: CollectionState + expected: bool + sub_rules: Iterable[StardewRule] = field(default_factory=list) + explored_rules_key: Set[Tuple[str, str]] = field(default_factory=set) + current_rule_explored: bool = False + + def __post_init__(self): + checkpoint = _rule_key(self.rule) + if checkpoint is not None and checkpoint in self.explored_rules_key: + self.current_rule_explored = True + self.sub_rules = [] + + def summary(self, depth=0) -> str: + summary = " " * depth + f"{str(self.rule)} -> {self.result}" + if self.current_rule_explored: + summary += " [Already explained]" + return summary + + def __str__(self, depth=0): + if not self.sub_rules: + return self.summary(depth) + + return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__str__(i, depth + 1) + if i.result is not self.expected else i.summary(depth + 1) + for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) + + def __repr__(self, depth=0): + if not self.sub_rules: + return self.summary(depth) + + return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__repr__(i, depth + 1) + for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) + + @cached_property + def result(self) -> bool: + try: + return self.rule(self.state) + except KeyError: + return False + + @cached_property + def explained_sub_rules(self) -> List[RuleExplanation]: + rule_key = _rule_key(self.rule) + if rule_key is not None: + self.explored_rules_key.add(rule_key) + + return [_explain(i, self.state, self.expected, self.explored_rules_key) for i in self.sub_rules] + + +def explain(rule: CollectionRule, state: CollectionState, expected: bool = True) -> RuleExplanation: + if isinstance(rule, StardewRule): + return _explain(rule, state, expected, explored_spots=set()) + else: + return f"Value of rule {str(rule)} was not {str(expected)} in {str(state)}" # noqa + + +@singledispatch +def _explain(rule: StardewRule, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: AggregatingStardewRule, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + return RuleExplanation(rule, state, expected, rule.original_rules, explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: Count, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + return RuleExplanation(rule, state, expected, rule.rules, explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: Has, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + try: + return RuleExplanation(rule, state, expected, [rule.other_rules[rule.item]], explored_rules_key=explored_spots) + except KeyError: + return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: TotalReceived, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + return RuleExplanation(rule, state, expected, [Received(i, rule.player, 1) for i in rule.items], explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: Reach, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + access_rules = None + if rule.resolution_hint == 'Location': + spot = state.multiworld.get_location(rule.spot, rule.player) + + if isinstance(spot.access_rule, StardewRule): + if spot.access_rule is true_: + access_rules = [Reach(spot.parent_region.name, "Region", rule.player)] + else: + access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] + + elif rule.resolution_hint == 'Entrance': + spot = state.multiworld.get_entrance(rule.spot, rule.player) + + if isinstance(spot.access_rule, StardewRule): + if spot.access_rule is true_: + access_rules = [Reach(spot.parent_region.name, "Region", rule.player)] + else: + access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] + + else: + spot = state.multiworld.get_region(rule.spot, rule.player) + access_rules = [*(Reach(e.name, "Entrance", rule.player) for e in spot.entrances)] + + if not access_rules: + return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots) + + return RuleExplanation(rule, state, expected, access_rules, explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: Received, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + access_rules = None + if rule.event: + try: + spot = state.multiworld.get_location(rule.item, rule.player) + if spot.access_rule is true_: + access_rules = [Reach(spot.parent_region.name, "Region", rule.player)] + else: + access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] + except KeyError: + pass + + if not access_rules: + return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots) + + return RuleExplanation(rule, state, expected, access_rules, explored_rules_key=explored_spots) + + +@singledispatch +def _rule_key(_: StardewRule) -> Optional[Tuple[str, str]]: + return None + + +@_rule_key.register +def _(rule: Reach) -> Tuple[str, str]: + return rule.spot, rule.resolution_hint + + +@_rule_key.register +def _(rule: Received) -> Optional[Tuple[str, str]]: + if not rule.event: + return None + + return rule.item, "Logic Event" diff --git a/worlds/stardew_valley/stardew_rule/state.py b/worlds/stardew_valley/stardew_rule/state.py index a0fce7c7c19e..cf0996a63bbc 100644 --- a/worlds/stardew_valley/stardew_rule/state.py +++ b/worlds/stardew_valley/stardew_rule/state.py @@ -1,10 +1,9 @@ from dataclasses import dataclass from typing import Iterable, Union, List, Tuple, Hashable -from BaseClasses import ItemClassification, CollectionState +from BaseClasses import CollectionState from .base import BaseStardewRule, CombinableStardewRule from .protocol import StardewRule -from ..items import item_table class TotalReceived(BaseStardewRule): @@ -20,11 +19,6 @@ def __init__(self, count: int, items: Union[str, Iterable[str]], player: int): else: items_list = [items] - assert items_list, "Can't create a Total Received conditions without items" - for item in items_list: - assert item_table[item].classification & ItemClassification.progression, \ - f"Item [{item_table[item].name}] has to be progression to be used in logic" - self.player = player self.items = items_list self.count = count @@ -40,9 +34,6 @@ def __call__(self, state: CollectionState) -> bool: def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: return self, self(state) - def get_difficulty(self): - return self.count - def __repr__(self): return f"Received {self.count} {self.items}" @@ -52,10 +43,8 @@ class Received(CombinableStardewRule): item: str player: int count: int - - def __post_init__(self): - assert item_table[self.item].classification & ItemClassification.progression, \ - f"Item [{item_table[self.item].name}] has to be progression to be used in logic" + event: bool = False + """Helps `explain` to know it can dig into a location with the same name.""" @property def combination_key(self) -> Hashable: @@ -73,11 +62,8 @@ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRul def __repr__(self): if self.count == 1: - return f"Received {self.item}" - return f"Received {self.count} {self.item}" - - def get_difficulty(self): - return self.count + return f"Received {'event ' if self.event else ''}{self.item}" + return f"Received {'event ' if self.event else ''}{self.count} {self.item}" @dataclass(frozen=True) @@ -97,9 +83,6 @@ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRul def __repr__(self): return f"Reach {self.resolution_hint} {self.spot}" - def get_difficulty(self): - return 1 - @dataclass(frozen=True) class HasProgressionPercent(CombinableStardewRule): @@ -122,19 +105,21 @@ def __call__(self, state: CollectionState) -> bool: stardew_world = state.multiworld.worlds[self.player] total_count = stardew_world.total_progression_items needed_count = (total_count * self.percent) // 100 + player_state = state.prog_items[self.player] + + if needed_count <= len(player_state): + return True + total_count = 0 - for item in state.prog_items[self.player]: - item_count = state.prog_items[self.player][item] + for item, item_count in player_state.items(): total_count += item_count if total_count >= needed_count: return True + return False def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: return self, self(state) def __repr__(self): - return f"HasProgressionPercent {self.percent}" - - def get_difficulty(self): - return self.percent + return f"Received {self.percent}% progression items." diff --git a/worlds/stardew_valley/strings/ap_names/ap_option_names.py b/worlds/stardew_valley/strings/ap_names/ap_option_names.py new file mode 100644 index 000000000000..a5cc10f7d7b8 --- /dev/null +++ b/worlds/stardew_valley/strings/ap_names/ap_option_names.py @@ -0,0 +1,16 @@ +class OptionName: + walnutsanity_puzzles = "Puzzles" + walnutsanity_bushes = "Bushes" + walnutsanity_dig_spots = "Dig Spots" + walnutsanity_repeatables = "Repeatables" + buff_luck = "Luck" + buff_damage = "Damage" + buff_defense = "Defense" + buff_immunity = "Immunity" + buff_health = "Health" + buff_energy = "Energy" + buff_bite = "Bite Rate" + buff_fish_trap = "Fish Trap" + buff_fishing_bar = "Fishing Bar Size" + buff_quality = "Quality" + buff_glow = "Glow" diff --git a/worlds/stardew_valley/strings/ap_names/buff_names.py b/worlds/stardew_valley/strings/ap_names/buff_names.py index 4ddd6fb5034f..0f311869aa9a 100644 --- a/worlds/stardew_valley/strings/ap_names/buff_names.py +++ b/worlds/stardew_valley/strings/ap_names/buff_names.py @@ -1,3 +1,13 @@ class Buff: movement = "Movement Speed Bonus" - luck = "Luck Bonus" \ No newline at end of file + luck = "Luck Bonus" + damage = "Damage Bonus" + defense = "Defense Bonus" + immunity = "Immunity Bonus" + health = "Health Bonus" + energy = "Energy Bonus" + bite_rate = "Bite Rate Bonus" + fish_trap = "Fish Trap Bonus" + fishing_bar = "Fishing Bar Size Bonus" + quality = "Quality Bonus" + glow = "Glow Bonus" diff --git a/worlds/stardew_valley/strings/ap_names/community_upgrade_names.py b/worlds/stardew_valley/strings/ap_names/community_upgrade_names.py index 68dad8e75287..6826b9234a30 100644 --- a/worlds/stardew_valley/strings/ap_names/community_upgrade_names.py +++ b/worlds/stardew_valley/strings/ap_names/community_upgrade_names.py @@ -1,4 +1,6 @@ class CommunityUpgrade: + raccoon = "Progressive Raccoon" fruit_bats = "Fruit Bats" mushroom_boxes = "Mushroom Boxes" movie_theater = "Progressive Movie Theater" + mr_qi_plane_ride = "Mr Qi's Plane Ride" diff --git a/worlds/stardew_valley/strings/ap_names/event_names.py b/worlds/stardew_valley/strings/ap_names/event_names.py index 08b9d8f8131c..88f9715abc65 100644 --- a/worlds/stardew_valley/strings/ap_names/event_names.py +++ b/worlds/stardew_valley/strings/ap_names/event_names.py @@ -1,6 +1,20 @@ +all_events = set() + + +def event(name: str): + all_events.add(name) + return name + + class Event: - victory = "Victory" - can_construct_buildings = "Can Construct Buildings" - start_dark_talisman_quest = "Start Dark Talisman Quest" - can_ship_items = "Can Ship Items" - can_shop_at_pierre = "Can Shop At Pierre's" + victory = event("Victory") + can_construct_buildings = event("Can Construct Buildings") + start_dark_talisman_quest = event("Start Dark Talisman Quest") + can_ship_items = event("Can Ship Items") + can_shop_at_pierre = event("Can Shop At Pierre's") + spring_farming = event("Spring Farming") + summer_farming = event("Summer Farming") + fall_farming = event("Fall Farming") + winter_farming = event("Winter Farming") + + received_walnuts = event("Received Walnuts") diff --git a/worlds/stardew_valley/strings/ap_names/mods/mod_items.py b/worlds/stardew_valley/strings/ap_names/mods/mod_items.py index ccc2765544a6..58371aebe7ed 100644 --- a/worlds/stardew_valley/strings/ap_names/mods/mod_items.py +++ b/worlds/stardew_valley/strings/ap_names/mods/mod_items.py @@ -9,6 +9,10 @@ class DeepWoodsItem: class SkillLevel: + cooking = "Cooking Level" + binning = "Binning Level" + magic = "Magic Level" + socializing = "Socializing Level" luck = "Luck Level" archaeology = "Archaeology Level" @@ -25,8 +29,10 @@ class SVEQuestItem: fable_reef_portal = "Fable Reef Portal" grandpa_shed = "Grandpa's Shed" - sve_quest_items: List[str] = [aurora_vineyard_tablet, iridium_bomb, void_soul, kittyfish_spell, scarlett_job_offer, morgan_schooling, grandpa_shed] - sve_quest_items_ginger_island: List[str] = [marlon_boat_paddle, fable_reef_portal] + sve_always_quest_items: List[str] = [kittyfish_spell, scarlett_job_offer, morgan_schooling] + sve_always_quest_items_ginger_island: List[str] = [fable_reef_portal] + sve_quest_items: List[str] = [aurora_vineyard_tablet, iridium_bomb, void_soul, grandpa_shed] + sve_quest_items_ginger_island: List[str] = [marlon_boat_paddle] class SVELocation: diff --git a/worlds/stardew_valley/strings/artisan_good_names.py b/worlds/stardew_valley/strings/artisan_good_names.py index a017cff1f9dd..366189568cf7 100644 --- a/worlds/stardew_valley/strings/artisan_good_names.py +++ b/worlds/stardew_valley/strings/artisan_good_names.py @@ -21,6 +21,45 @@ class ArtisanGood: caviar = "Caviar" green_tea = "Green Tea" mead = "Mead" + mystic_syrup = "Mystic Syrup" + dried_fruit = "Dried Fruit" + dried_mushroom = "Dried Mushrooms" + raisins = "Raisins" + stardrop_tea = "Stardrop Tea" + smoked_fish = "Smoked Fish" + targeted_bait = "Targeted Bait" + + @classmethod + def specific_wine(cls, fruit: str) -> str: + return f"{cls.wine} [{fruit}]" + + @classmethod + def specific_juice(cls, vegetable: str) -> str: + return f"{cls.juice} [{vegetable}]" + + @classmethod + def specific_jelly(cls, fruit: str) -> str: + return f"{cls.jelly} [{fruit}]" + + @classmethod + def specific_pickles(cls, vegetable: str) -> str: + return f"{cls.pickles} [{vegetable}]" + + @classmethod + def specific_dried_fruit(cls, food: str) -> str: + return f"{cls.dried_fruit} [{food}]" + + @classmethod + def specific_dried_mushroom(cls, food: str) -> str: + return f"{cls.dried_mushroom} [{food}]" + + @classmethod + def specific_smoked_fish(cls, fish: str) -> str: + return f"{cls.smoked_fish} [{fish}]" + + @classmethod + def specific_bait(cls, fish: str) -> str: + return f"{cls.targeted_bait} [{fish}]" class ModArtisanGood: diff --git a/worlds/stardew_valley/strings/book_names.py b/worlds/stardew_valley/strings/book_names.py new file mode 100644 index 000000000000..3c32cd81b326 --- /dev/null +++ b/worlds/stardew_valley/strings/book_names.py @@ -0,0 +1,65 @@ +class Book: + animal_catalogue = "Animal Catalogue" + book_of_mysteries = "Book of Mysteries" + book_of_stars = "Book Of Stars" + stardew_valley_almanac = "Stardew Valley Almanac" + bait_and_bobber = "Bait And Bobber" + mining_monthly = "Mining Monthly" + combat_quarterly = "Combat Quarterly" + woodcutters_weekly = "Woodcutter's Weekly" + the_alleyway_buffet = "The Alleyway Buffet" + the_art_o_crabbing = "The Art O' Crabbing" + dwarvish_safety_manual = "Dwarvish Safety Manual" + jewels_of_the_sea = "Jewels Of The Sea" + raccoon_journal = "Raccoon Journal" + woodys_secret = "Woody's Secret" + jack_be_nimble_jack_be_thick = "Jack Be Nimble, Jack Be Thick" + friendship_101 = "Friendship 101" + monster_compendium = "Monster Compendium" + mapping_cave_systems = "Mapping Cave Systems" + treasure_appraisal_guide = "Treasure Appraisal Guide" + way_of_the_wind_pt_1 = "Way Of The Wind pt. 1" + way_of_the_wind_pt_2 = "Way Of The Wind pt. 2" + horse_the_book = "Horse: The Book" + ol_slitherlegs = "Ol' Slitherlegs" + queen_of_sauce_cookbook = "Queen Of Sauce Cookbook" + price_catalogue = "Price Catalogue" + the_diamond_hunter = "The Diamond Hunter" + + +class ModBook: + digging_like_worms = "Digging Like Worms" + + +ordered_lost_books = [] +all_lost_books = set() + + +def lost_book(book_name: str): + ordered_lost_books.append(book_name) + all_lost_books.add(book_name) + return book_name + + +class LostBook: + tips_on_farming = lost_book("Tips on Farming") + this_is_a_book_by_marnie = lost_book("This is a book by Marnie") + on_foraging = lost_book("On Foraging") + the_fisherman_act_1 = lost_book("The Fisherman, Act 1") + how_deep_do_the_mines_go = lost_book("How Deep do the mines go?") + an_old_farmers_journal = lost_book("An Old Farmer's Journal") + scarecrows = lost_book("Scarecrows") + the_secret_of_the_stardrop = lost_book("The Secret of the Stardrop") + journey_of_the_prairie_king_the_smash_hit_video_game = lost_book("Journey of the Prairie King -- The Smash Hit Video Game!") + a_study_on_diamond_yields = lost_book("A Study on Diamond Yields") + brewmasters_guide = lost_book("Brewmaster's Guide") + mysteries_of_the_dwarves = lost_book("Mysteries of the Dwarves") + highlights_from_the_book_of_yoba = lost_book("Highlights From The Book of Yoba") + marriage_guide_for_farmers = lost_book("Marriage Guide for Farmers") + the_fisherman_act_ii = lost_book("The Fisherman, Act II") + technology_report = lost_book("Technology Report!") + secrets_of_the_legendary_fish = lost_book("Secrets of the Legendary Fish") + gunther_tunnel_notice = lost_book("Gunther Tunnel Notice") + note_from_gunther = lost_book("Note From Gunther") + goblins_by_m_jasper = lost_book("Goblins by M. Jasper") + secret_statues_acrostics = lost_book("Secret Statues Acrostics") diff --git a/worlds/stardew_valley/strings/bundle_names.py b/worlds/stardew_valley/strings/bundle_names.py index de8d8af3877f..5f560a545434 100644 --- a/worlds/stardew_valley/strings/bundle_names.py +++ b/worlds/stardew_valley/strings/bundle_names.py @@ -6,75 +6,103 @@ class CCRoom: vault = "Vault" boiler_room = "Boiler Room" abandoned_joja_mart = "Abandoned Joja Mart" + raccoon_requests = "Raccoon Requests" + + +all_cc_bundle_names = [] + + +def cc_bundle(name: str) -> str: + all_cc_bundle_names.append(name) + return name class BundleName: - spring_foraging = "Spring Foraging Bundle" - summer_foraging = "Summer Foraging Bundle" - fall_foraging = "Fall Foraging Bundle" - winter_foraging = "Winter Foraging Bundle" - construction = "Construction Bundle" - exotic_foraging = "Exotic Foraging Bundle" - beach_foraging = "Beach Foraging Bundle" - mines_foraging = "Mines Foraging Bundle" - desert_foraging = "Desert Foraging Bundle" - island_foraging = "Island Foraging Bundle" - sticky = "Sticky Bundle" - wild_medicine = "Wild Medicine Bundle" - quality_foraging = "Quality Foraging Bundle" - spring_crops = "Spring Crops Bundle" - summer_crops = "Summer Crops Bundle" - fall_crops = "Fall Crops Bundle" - quality_crops = "Quality Crops Bundle" - animal = "Animal Bundle" - artisan = "Artisan Bundle" - rare_crops = "Rare Crops Bundle" - fish_farmer = "Fish Farmer's Bundle" - garden = "Garden Bundle" - brewer = "Brewer's Bundle" - orchard = "Orchard Bundle" - island_crops = "Island Crops Bundle" - agronomist = "Agronomist's Bundle" - slime_farmer = "Slime Farmer Bundle" - river_fish = "River Fish Bundle" - lake_fish = "Lake Fish Bundle" - ocean_fish = "Ocean Fish Bundle" - night_fish = "Night Fishing Bundle" - crab_pot = "Crab Pot Bundle" - trash = "Trash Bundle" - recycling = "Recycling Bundle" - specialty_fish = "Specialty Fish Bundle" - spring_fish = "Spring Fishing Bundle" - summer_fish = "Summer Fishing Bundle" - fall_fish = "Fall Fishing Bundle" - winter_fish = "Winter Fishing Bundle" - rain_fish = "Rain Fishing Bundle" - quality_fish = "Quality Fish Bundle" - master_fisher = "Master Fisher's Bundle" - legendary_fish = "Legendary Fish Bundle" - island_fish = "Island Fish Bundle" - deep_fishing = "Deep Fishing Bundle" - tackle = "Tackle Bundle" - bait = "Master Baiter Bundle" - blacksmith = "Blacksmith's Bundle" - geologist = "Geologist's Bundle" - adventurer = "Adventurer's Bundle" - treasure_hunter = "Treasure Hunter's Bundle" - engineer = "Engineer's Bundle" - demolition = "Demolition Bundle" - paleontologist = "Paleontologist's Bundle" - archaeologist = "Archaeologist's Bundle" - chef = "Chef's Bundle" - dye = "Dye Bundle" - field_research = "Field Research Bundle" - fodder = "Fodder Bundle" - enchanter = "Enchanter's Bundle" - children = "Children's Bundle" - forager = "Forager's Bundle" - home_cook = "Home Cook's Bundle" - bartender = "Bartender's Bundle" - gambler = "Gambler's Bundle" - carnival = "Carnival Bundle" - walnut_hunter = "Walnut Hunter Bundle" - qi_helper = "Qi's Helper Bundle" + spring_foraging = cc_bundle("Spring Foraging Bundle") + summer_foraging = cc_bundle("Summer Foraging Bundle") + fall_foraging = cc_bundle("Fall Foraging Bundle") + winter_foraging = cc_bundle("Winter Foraging Bundle") + construction = cc_bundle("Construction Bundle") + exotic_foraging = cc_bundle("Exotic Foraging Bundle") + beach_foraging = cc_bundle("Beach Foraging Bundle") + mines_foraging = cc_bundle("Mines Foraging Bundle") + desert_foraging = cc_bundle("Desert Foraging Bundle") + island_foraging = cc_bundle("Island Foraging Bundle") + sticky = cc_bundle("Sticky Bundle") + forest = cc_bundle("Forest Bundle") + green_rain = cc_bundle("Green Rain Bundle") + wild_medicine = cc_bundle("Wild Medicine Bundle") + quality_foraging = cc_bundle("Quality Foraging Bundle") + spring_crops = cc_bundle("Spring Crops Bundle") + summer_crops = cc_bundle("Summer Crops Bundle") + fall_crops = cc_bundle("Fall Crops Bundle") + quality_crops = cc_bundle("Quality Crops Bundle") + animal = cc_bundle("Animal Bundle") + artisan = cc_bundle("Artisan Bundle") + rare_crops = cc_bundle("Rare Crops Bundle") + fish_farmer = cc_bundle("Fish Farmer's Bundle") + garden = cc_bundle("Garden Bundle") + brewer = cc_bundle("Brewer's Bundle") + orchard = cc_bundle("Orchard Bundle") + island_crops = cc_bundle("Island Crops Bundle") + agronomist = cc_bundle("Agronomist's Bundle") + slime_farmer = cc_bundle("Slime Farmer Bundle") + sommelier = cc_bundle("Sommelier Bundle") + dry = cc_bundle("Dry Bundle") + river_fish = cc_bundle("River Fish Bundle") + lake_fish = cc_bundle("Lake Fish Bundle") + ocean_fish = cc_bundle("Ocean Fish Bundle") + night_fish = cc_bundle("Night Fishing Bundle") + crab_pot = cc_bundle("Crab Pot Bundle") + trash = cc_bundle("Trash Bundle") + recycling = cc_bundle("Recycling Bundle") + specialty_fish = cc_bundle("Specialty Fish Bundle") + spring_fish = cc_bundle("Spring Fishing Bundle") + summer_fish = cc_bundle("Summer Fishing Bundle") + fall_fish = cc_bundle("Fall Fishing Bundle") + winter_fish = cc_bundle("Winter Fishing Bundle") + rain_fish = cc_bundle("Rain Fishing Bundle") + quality_fish = cc_bundle("Quality Fish Bundle") + master_fisher = cc_bundle("Master Fisher's Bundle") + legendary_fish = cc_bundle("Legendary Fish Bundle") + island_fish = cc_bundle("Island Fish Bundle") + deep_fishing = cc_bundle("Deep Fishing Bundle") + tackle = cc_bundle("Tackle Bundle") + bait = cc_bundle("Master Baiter Bundle") + specific_bait = cc_bundle("Specific Fishing Bundle") + fish_smoker = cc_bundle("Fish Smoker Bundle") + blacksmith = cc_bundle("Blacksmith's Bundle") + geologist = cc_bundle("Geologist's Bundle") + adventurer = cc_bundle("Adventurer's Bundle") + treasure_hunter = cc_bundle("Treasure Hunter's Bundle") + engineer = cc_bundle("Engineer's Bundle") + demolition = cc_bundle("Demolition Bundle") + paleontologist = cc_bundle("Paleontologist's Bundle") + archaeologist = cc_bundle("Archaeologist's Bundle") + chef = cc_bundle("Chef's Bundle") + dye = cc_bundle("Dye Bundle") + field_research = cc_bundle("Field Research Bundle") + fodder = cc_bundle("Fodder Bundle") + enchanter = cc_bundle("Enchanter's Bundle") + children = cc_bundle("Children's Bundle") + forager = cc_bundle("Forager's Bundle") + home_cook = cc_bundle("Home Cook's Bundle") + helper = cc_bundle("Helper's Bundle") + spirit_eve = cc_bundle("Spirit's Eve Bundle") + winter_star = cc_bundle("Winter Star Bundle") + bartender = cc_bundle("Bartender's Bundle") + calico = cc_bundle("Calico Bundle") + raccoon = cc_bundle("Raccoon Bundle") + money_2500 = cc_bundle("2,500g Bundle") + money_5000 = cc_bundle("5,000g Bundle") + money_10000 = cc_bundle("10,000g Bundle") + money_25000 = cc_bundle("25,000g Bundle") + gambler = cc_bundle("Gambler's Bundle") + carnival = cc_bundle("Carnival Bundle") + walnut_hunter = cc_bundle("Walnut Hunter Bundle") + qi_helper = cc_bundle("Qi's Helper Bundle") missing_bundle = "The Missing Bundle" + raccoon_fish = "Raccoon Fish" + raccoon_artisan = "Raccoon Artisan" + raccoon_food = "Raccoon Food" + raccoon_foraging = "Raccoon Foraging" diff --git a/worlds/stardew_valley/strings/craftable_names.py b/worlds/stardew_valley/strings/craftable_names.py index 74a77a8e9467..83445c702c32 100644 --- a/worlds/stardew_valley/strings/craftable_names.py +++ b/worlds/stardew_valley/strings/craftable_names.py @@ -25,6 +25,7 @@ class WildSeeds: winter = "Winter Seeds" ancient = "Ancient Seeds" grass_starter = "Grass Starter" + blue_grass_starter = "Blue Grass Starter" tea_sapling = "Tea Sapling" fiber = "Fiber Seeds" @@ -48,6 +49,7 @@ class Floor: class Fishing: spinner = "Spinner" trap_bobber = "Trap Bobber" + sonar_bobber = "Sonar Bobber" cork_bobber = "Cork Bobber" quality_bobber = "Quality Bobber" treasure_hunter = "Treasure Hunter" @@ -59,6 +61,8 @@ class Fishing: magic_bait = "Magic Bait" lead_bobber = "Lead Bobber" curiosity_lure = "Curiosity Lure" + deluxe_bait = "Deluxe Bait" + challenge_bait = "Challenge Bait" class Ring: @@ -70,6 +74,7 @@ class Ring: glowstone_ring = "Glowstone Ring" iridium_band = "Iridium Band" wedding_ring = "Wedding Ring" + lucky_ring = "Lucky Ring" class Edible: @@ -88,6 +93,15 @@ class Consumable: warp_totem_desert = "Warp Totem: Desert" warp_totem_island = "Warp Totem: Island" rain_totem = "Rain Totem" + mystery_box = "Mystery Box" + gold_mystery_box = "Golden Mystery Box" + treasure_totem = "Treasure Totem" + fireworks_red = "Fireworks (Red)" + fireworks_purple = "Fireworks (Purple)" + fireworks_green = "Fireworks (Green)" + far_away_stone = "Far Away Stone" + golden_animal_cracker = "Golden Animal Cracker" + butterfly_powder = "Butterfly Powder" class Lighting: @@ -116,12 +130,20 @@ class Furniture: class Storage: chest = "Chest" stone_chest = "Stone Chest" + big_chest = "Big Chest" + big_stone_chest = "Big Stone Chest" class Sign: wood = "Wood Sign" stone = "Stone Sign" dark = "Dark Sign" + text = "Text Sign" + + +class Statue: + blessings = "Statue Of Blessings" + dwarf_king = "Statue Of The Dwarf King" class Craftable: @@ -137,6 +159,7 @@ class Craftable: farm_computer = "Farm Computer" hopper = "Hopper" cookout_kit = "Cookout Kit" + tent_kit = "Tent Kit" class ModEdible: @@ -152,9 +175,11 @@ class ModEdible: class ModCraftable: travel_core = "Travel Core" - glass_bazier = "Glass Bazier" + glass_brazier = "Glass Brazier" water_shifter = "Water Shifter" + rusty_brazier = "Rusty Brazier" glass_fence = "Glass Fence" + bone_fence = "Bone Fence" wooden_display = "Wooden Display" hardwood_display = "Hardwood Display" neanderthal_skeleton = "Neanderthal Skeleton" @@ -171,11 +196,17 @@ class ModMachine: hardwood_preservation_chamber = "Hardwood Preservation Chamber" grinder = "Grinder" ancient_battery = "Ancient Battery Production Station" + restoration_table = "Restoration Table" + trash_bin = "Trash Bin" + composter = "Composter" + recycling_bin = "Recycling Bin" + advanced_recycling_machine = "Advanced Recycling Machine" class ModFloor: glass_path = "Glass Path" bone_path = "Bone Path" + rusty_path = "Rusty Path" class ModConsumable: diff --git a/worlds/stardew_valley/strings/crop_names.py b/worlds/stardew_valley/strings/crop_names.py index 295e40005f75..fa7a77c834fc 100644 --- a/worlds/stardew_valley/strings/crop_names.py +++ b/worlds/stardew_valley/strings/crop_names.py @@ -1,64 +1,55 @@ -all_fruits = [] -all_vegetables = [] - - -def veggie(name: str) -> str: - all_vegetables.append(name) - return name - - -def fruity(name: str) -> str: - all_fruits.append(name) - return name - - class Fruit: - sweet_gem_berry = fruity("Sweet Gem Berry") + sweet_gem_berry = "Sweet Gem Berry" any = "Any Fruit" - blueberry = fruity("Blueberry") - melon = fruity("Melon") - apple = fruity("Apple") - apricot = fruity("Apricot") - cherry = fruity("Cherry") - orange = fruity("Orange") - peach = fruity("Peach") - pomegranate = fruity("Pomegranate") - banana = fruity("Banana") - mango = fruity("Mango") - pineapple = fruity("Pineapple") - ancient_fruit = fruity("Ancient Fruit") - strawberry = fruity("Strawberry") - starfruit = fruity("Starfruit") - rhubarb = fruity("Rhubarb") - grape = fruity("Grape") - cranberries = fruity("Cranberries") - hot_pepper = fruity("Hot Pepper") + blueberry = "Blueberry" + melon = "Melon" + apple = "Apple" + apricot = "Apricot" + cherry = "Cherry" + orange = "Orange" + peach = "Peach" + pomegranate = "Pomegranate" + banana = "Banana" + mango = "Mango" + pineapple = "Pineapple" + ancient_fruit = "Ancient Fruit" + strawberry = "Strawberry" + starfruit = "Starfruit" + rhubarb = "Rhubarb" + grape = "Grape" + cranberries = "Cranberries" + hot_pepper = "Hot Pepper" + powdermelon = "Powdermelon" + qi_fruit = "Qi Fruit" class Vegetable: any = "Any Vegetable" - parsnip = veggie("Parsnip") - garlic = veggie("Garlic") + parsnip = "Parsnip" + garlic = "Garlic" bok_choy = "Bok Choy" wheat = "Wheat" - potato = veggie("Potato") - corn = veggie("Corn") - tomato = veggie("Tomato") - pumpkin = veggie("Pumpkin") - unmilled_rice = veggie("Unmilled Rice") - beet = veggie("Beet") + potato = "Potato" + corn = "Corn" + tomato = "Tomato" + pumpkin = "Pumpkin" + unmilled_rice = "Unmilled Rice" + beet = "Beet" hops = "Hops" - cauliflower = veggie("Cauliflower") - amaranth = veggie("Amaranth") - kale = veggie("Kale") - artichoke = veggie("Artichoke") + cauliflower = "Cauliflower" + amaranth = "Amaranth" + kale = "Kale" + artichoke = "Artichoke" tea_leaves = "Tea Leaves" - eggplant = veggie("Eggplant") - green_bean = veggie("Green Bean") - red_cabbage = veggie("Red Cabbage") - yam = veggie("Yam") - radish = veggie("Radish") - taro_root = veggie("Taro Root") + eggplant = "Eggplant" + green_bean = "Green Bean" + red_cabbage = "Red Cabbage" + yam = "Yam" + radish = "Radish" + taro_root = "Taro Root" + carrot = "Carrot" + summer_squash = "Summer Squash" + broccoli = "Broccoli" class SVEFruit: @@ -76,7 +67,3 @@ class SVEVegetable: class DistantLandsCrop: void_mint = "Void Mint Leaves" vile_ancient_fruit = "Vile Ancient Fruit" - - -all_vegetables = tuple(all_vegetables) -all_fruits = tuple(all_fruits) diff --git a/worlds/stardew_valley/strings/currency_names.py b/worlds/stardew_valley/strings/currency_names.py index 5192466c9ca7..21ccb5b55c58 100644 --- a/worlds/stardew_valley/strings/currency_names.py +++ b/worlds/stardew_valley/strings/currency_names.py @@ -5,6 +5,9 @@ class Currency: star_token = "Star Token" money = "Money" cinder_shard = "Cinder Shard" + prize_ticket = "Prize Ticket" + calico_egg = "Calico Egg" + golden_tag = "Golden Tag" @staticmethod def is_currency(item: str) -> bool: diff --git a/worlds/stardew_valley/strings/entrance_names.py b/worlds/stardew_valley/strings/entrance_names.py index 00823d62ea07..9b651f42760a 100644 --- a/worlds/stardew_valley/strings/entrance_names.py +++ b/worlds/stardew_valley/strings/entrance_names.py @@ -42,14 +42,7 @@ class Entrance: forest_to_marnie_ranch = "Forest to Marnie's Ranch" forest_to_leah_cottage = "Forest to Leah's Cottage" forest_to_sewer = "Forest to Sewer" - buy_from_traveling_merchant = "Buy from Traveling Merchant" - buy_from_traveling_merchant_sunday = "Buy from Traveling Merchant Sunday" - buy_from_traveling_merchant_monday = "Buy from Traveling Merchant Monday" - buy_from_traveling_merchant_tuesday = "Buy from Traveling Merchant Tuesday" - buy_from_traveling_merchant_wednesday = "Buy from Traveling Merchant Wednesday" - buy_from_traveling_merchant_thursday = "Buy from Traveling Merchant Thursday" - buy_from_traveling_merchant_friday = "Buy from Traveling Merchant Friday" - buy_from_traveling_merchant_saturday = "Buy from Traveling Merchant Saturday" + forest_to_mastery_cave = "Forest to Mastery Cave" mountain_to_railroad = "Mountain to Railroad" mountain_to_tent = "Mountain to Tent" mountain_to_carpenter_shop = "Mountain to Carpenter Shop" @@ -57,6 +50,7 @@ class Entrance: mountain_to_the_mines = "Mountain to The Mines" enter_quarry = "Mountain to Quarry" mountain_to_adventurer_guild = "Mountain to Adventurer's Guild" + adventurer_guild_to_bedroom = "Adventurer's Guild to Marlon's Bedroom" mountain_to_town = "Mountain to Town" town_to_community_center = "Town to Community Center" access_crafts_room = "Access Crafts Room" @@ -120,7 +114,6 @@ class Entrance: mine_to_skull_cavern_floor_175 = dig_to_skull_floor(175) mine_to_skull_cavern_floor_200 = dig_to_skull_floor(200) enter_dangerous_skull_cavern = "Enter the Dangerous Skull Cavern" - talk_to_mines_dwarf = "Talk to Mines Dwarf" dig_to_mines_floor_5 = dig_to_mines_floor(5) dig_to_mines_floor_10 = dig_to_mines_floor(10) dig_to_mines_floor_15 = dig_to_mines_floor(15) @@ -183,6 +176,19 @@ class Entrance: parrot_express_jungle_to_docks = "Parrot Express Jungle to Docks" parrot_express_dig_site_to_docks = "Parrot Express Dig Site to Docks" parrot_express_volcano_to_docks = "Parrot Express Volcano to Docks" + + +class LogicEntrance: + talk_to_mines_dwarf = "Talk to Mines Dwarf" + + buy_from_traveling_merchant = "Buy from Traveling Merchant" + buy_from_traveling_merchant_sunday = "Buy from Traveling Merchant Sunday" + buy_from_traveling_merchant_monday = "Buy from Traveling Merchant Monday" + buy_from_traveling_merchant_tuesday = "Buy from Traveling Merchant Tuesday" + buy_from_traveling_merchant_wednesday = "Buy from Traveling Merchant Wednesday" + buy_from_traveling_merchant_thursday = "Buy from Traveling Merchant Thursday" + buy_from_traveling_merchant_friday = "Buy from Traveling Merchant Friday" + buy_from_traveling_merchant_saturday = "Buy from Traveling Merchant Saturday" farmhouse_cooking = "Farmhouse Cooking" island_cooking = "Island Cooking" shipping = "Use Shipping Bin" @@ -191,17 +197,43 @@ class Entrance: blacksmith_iron = "Upgrade Iron Tools" blacksmith_gold = "Upgrade Gold Tools" blacksmith_iridium = "Upgrade Iridium Tools" - farming = "Start Farming" + + grow_spring_crops = "Grow Spring Crops" + grow_summer_crops = "Grow Summer Crops" + grow_fall_crops = "Grow Fall Crops" + grow_winter_crops = "Grow Winter Crops" + grow_spring_crops_in_greenhouse = "Grow Spring Crops in Greenhouse" + grow_summer_crops_in_greenhouse = "Grow Summer Crops in Greenhouse" + grow_fall_crops_in_greenhouse = "Grow Fall Crops in Greenhouse" + grow_winter_crops_in_greenhouse = "Grow Winter Crops in Greenhouse" + grow_indoor_crops_in_greenhouse = "Grow Indoor Crops in Greenhouse" + grow_spring_crops_on_island = "Grow Spring Crops on Island" + grow_summer_crops_on_island = "Grow Summer Crops on Island" + grow_fall_crops_on_island = "Grow Fall Crops on Island" + grow_winter_crops_on_island = "Grow Winter Crops on Island" + grow_indoor_crops_on_island = "Grow Indoor Crops on Island" + grow_summer_fall_crops_in_summer = "Grow Summer Fall Crops in Summer" + grow_summer_fall_crops_in_fall = "Grow Summer Fall Crops in Fall" + fishing = "Start Fishing" attend_egg_festival = "Attend Egg Festival" + attend_desert_festival = "Attend Desert Festival" attend_flower_dance = "Attend Flower Dance" attend_luau = "Attend Luau" + attend_trout_derby = "Attend Trout Derby" attend_moonlight_jellies = "Attend Dance of the Moonlight Jellies" attend_fair = "Attend Stardew Valley Fair" attend_spirit_eve = "Attend Spirit's Eve" attend_festival_of_ice = "Attend Festival of Ice" attend_night_market = "Attend Night Market" attend_winter_star = "Attend Feast of the Winter Star" + attend_squidfest = "Attend SquidFest" + buy_experience_books = "Buy Experience Books from the bookseller" + buy_year1_books = "Buy Year 1 Books from the Bookseller" + buy_year3_books = "Buy Year 3 Books from the Bookseller" + complete_raccoon_requests = "Complete Raccoon Requests" + buy_from_raccoon = "Buy From Raccoon" + fish_in_waterfall = "Fish In Waterfall" # Skull Cavern Elevator @@ -356,4 +388,3 @@ class BoardingHouseEntrance: lost_valley_ruins_to_lost_valley_house_1 = "Lost Valley Ruins to Lost Valley Ruins - First House" lost_valley_ruins_to_lost_valley_house_2 = "Lost Valley Ruins to Lost Valley Ruins - Second House" boarding_house_plateau_to_buffalo_ranch = "Boarding House Outside to Buffalo's Ranch" - diff --git a/worlds/stardew_valley/strings/festival_check_names.py b/worlds/stardew_valley/strings/festival_check_names.py index 73a9d3978eab..b59b3cd03f17 100644 --- a/worlds/stardew_valley/strings/festival_check_names.py +++ b/worlds/stardew_valley/strings/festival_check_names.py @@ -35,3 +35,46 @@ class FestivalCheck: jack_o_lantern = "Jack-O-Lantern Recipe" moonlight_jellies_banner = "Moonlight Jellies Banner" starport_decal = "Starport Decal" + calico_race = "Calico Race" + mummy_mask = "Mummy Mask" + calico_statue = "Calico Statue" + emily_outfit_service = "Emily's Outfit Services" + earthy_mousse = "Earthy Mousse" + sweet_bean_cake = "Sweet Bean Cake" + skull_cave_casserole = "Skull Cave Casserole" + spicy_tacos = "Spicy Tacos" + mountain_chili = "Mountain Chili" + crystal_cake = "Crystal Cake" + cave_kebab = "Cave Kebab" + hot_log = "Hot Log" + sour_salad = "Sour Salad" + superfood_cake = "Superfood Cake" + warrior_smoothie = "Warrior Smoothie" + rumpled_fruit_skin = "Rumpled Fruit Skin" + calico_pizza = "Calico Pizza" + stuffed_mushrooms = "Stuffed Mushrooms" + elf_quesadilla = "Elf Quesadilla" + nachos_of_the_desert = "Nachos Of The Desert" + cloppino = "Cloppino" + rainforest_shrimp = "Rainforest Shrimp" + shrimp_donut = "Shrimp Donut" + smell_of_the_sea = "Smell Of The Sea" + desert_gumbo = "Desert Gumbo" + free_cactis = "Free Cactis" + monster_hunt = "Monster Hunt" + deep_dive = "Deep Dive" + treasure_hunt = "Treasure Hunt" + touch_calico_statue = "Touch A Calico Statue" + real_calico_egg_hunter = "Real Calico Egg Hunter" + willy_challenge = "Willy's Challenge" + desert_scholar = "Desert Scholar" + trout_derby_reward_pattern = "Trout Derby Reward " + squidfest_day_1_copper = "SquidFest Day 1 Copper" + squidfest_day_1_iron = "SquidFest Day 1 Iron" + squidfest_day_1_gold = "SquidFest Day 1 Gold" + squidfest_day_1_iridium = "SquidFest Day 1 Iridium" + squidfest_day_2_copper = "SquidFest Day 2 Copper" + squidfest_day_2_iron = "SquidFest Day 2 Iron" + squidfest_day_2_gold = "SquidFest Day 2 Gold" + squidfest_day_2_iridium = "SquidFest Day 2 Iridium" + diff --git a/worlds/stardew_valley/strings/fish_names.py b/worlds/stardew_valley/strings/fish_names.py index cd59d749ee01..d94f9e2fd403 100644 --- a/worlds/stardew_valley/strings/fish_names.py +++ b/worlds/stardew_valley/strings/fish_names.py @@ -1,81 +1,92 @@ +all_fish = [] + + +def fish(fish_name: str) -> str: + all_fish.append(fish_name) + return fish_name + + class Fish: - albacore = "Albacore" - anchovy = "Anchovy" - angler = "Angler" - any = "Any Fish" - blob_fish = "Blobfish" - blobfish = "Blobfish" - blue_discus = "Blue Discus" - bream = "Bream" - bullhead = "Bullhead" - carp = "Carp" - catfish = "Catfish" - chub = "Chub" - clam = "Clam" - cockle = "Cockle" - crab = "Crab" - crayfish = "Crayfish" - crimsonfish = "Crimsonfish" - dorado = "Dorado" - eel = "Eel" - flounder = "Flounder" - ghostfish = "Ghostfish" - glacierfish = "Glacierfish" - glacierfish_jr = "Glacierfish Jr." - halibut = "Halibut" - herring = "Herring" - ice_pip = "Ice Pip" - largemouth_bass = "Largemouth Bass" - lava_eel = "Lava Eel" - legend = "Legend" - legend_ii = "Legend II" - lingcod = "Lingcod" - lionfish = "Lionfish" - lobster = "Lobster" - midnight_carp = "Midnight Carp" - midnight_squid = "Midnight Squid" - ms_angler = "Ms. Angler" - mussel = "Mussel" - mussel_node = "Mussel Node" - mutant_carp = "Mutant Carp" - octopus = "Octopus" - oyster = "Oyster" - perch = "Perch" - periwinkle = "Periwinkle" - pike = "Pike" - pufferfish = "Pufferfish" - radioactive_carp = "Radioactive Carp" - rainbow_trout = "Rainbow Trout" - red_mullet = "Red Mullet" - red_snapper = "Red Snapper" - salmon = "Salmon" - sandfish = "Sandfish" - sardine = "Sardine" - scorpion_carp = "Scorpion Carp" - sea_cucumber = "Sea Cucumber" - shad = "Shad" - shrimp = "Shrimp" - slimejack = "Slimejack" - smallmouth_bass = "Smallmouth Bass" - snail = "Snail" - son_of_crimsonfish = "Son of Crimsonfish" - spook_fish = "Spook Fish" - spookfish = "Spook Fish" - squid = "Squid" - stingray = "Stingray" - stonefish = "Stonefish" - sturgeon = "Sturgeon" - sunfish = "Sunfish" - super_cucumber = "Super Cucumber" - tiger_trout = "Tiger Trout" - tilapia = "Tilapia" - tuna = "Tuna" - void_salmon = "Void Salmon" - walleye = "Walleye" - woodskip = "Woodskip" + albacore = fish("Albacore") + anchovy = fish("Anchovy") + angler = fish("Angler") + any = fish("Any Fish") + blobfish = fish("Blobfish") + blue_discus = fish("Blue Discus") + bream = fish("Bream") + bullhead = fish("Bullhead") + carp = fish("Carp") + catfish = fish("Catfish") + chub = fish("Chub") + clam = fish("Clam") + cockle = fish("Cockle") + crab = fish("Crab") + crayfish = fish("Crayfish") + crimsonfish = fish("Crimsonfish") + dorado = fish("Dorado") + eel = fish("Eel") + flounder = fish("Flounder") + ghostfish = fish("Ghostfish") + goby = fish("Goby") + glacierfish = fish("Glacierfish") + glacierfish_jr = fish("Glacierfish Jr.") + halibut = fish("Halibut") + herring = fish("Herring") + ice_pip = fish("Ice Pip") + largemouth_bass = fish("Largemouth Bass") + lava_eel = fish("Lava Eel") + legend = fish("Legend") + legend_ii = fish("Legend II") + lingcod = fish("Lingcod") + lionfish = fish("Lionfish") + lobster = fish("Lobster") + midnight_carp = fish("Midnight Carp") + midnight_squid = fish("Midnight Squid") + ms_angler = fish("Ms. Angler") + mussel = fish("Mussel") + mussel_node = fish("Mussel Node") + mutant_carp = fish("Mutant Carp") + octopus = fish("Octopus") + oyster = fish("Oyster") + perch = fish("Perch") + periwinkle = fish("Periwinkle") + pike = fish("Pike") + pufferfish = fish("Pufferfish") + radioactive_carp = fish("Radioactive Carp") + rainbow_trout = fish("Rainbow Trout") + red_mullet = fish("Red Mullet") + red_snapper = fish("Red Snapper") + salmon = fish("Salmon") + sandfish = fish("Sandfish") + sardine = fish("Sardine") + scorpion_carp = fish("Scorpion Carp") + sea_cucumber = fish("Sea Cucumber") + shad = fish("Shad") + shrimp = fish("Shrimp") + slimejack = fish("Slimejack") + smallmouth_bass = fish("Smallmouth Bass") + snail = fish("Snail") + son_of_crimsonfish = fish("Son of Crimsonfish") + spook_fish = fish("Spook Fish") + spookfish = fish("Spook Fish") + squid = fish("Squid") + stingray = fish("Stingray") + stonefish = fish("Stonefish") + sturgeon = fish("Sturgeon") + sunfish = fish("Sunfish") + super_cucumber = fish("Super Cucumber") + tiger_trout = fish("Tiger Trout") + tilapia = fish("Tilapia") + tuna = fish("Tuna") + void_salmon = fish("Void Salmon") + walleye = fish("Walleye") + woodskip = fish("Woodskip") class WaterItem: + sea_jelly = "Sea Jelly" + river_jelly = "River Jelly" + cave_jelly = "Cave Jelly" seaweed = "Seaweed" green_algae = "Green Algae" white_algae = "White Algae" @@ -95,6 +106,7 @@ class Trash: class WaterChest: fishing_chest = "Fishing Chest" + golden_fishing_chest = "Golden Fishing Chest" treasure = "Treasure Chest" @@ -134,3 +146,9 @@ class DistantLandsFish: purple_algae = "Purple Algae" giant_horsehoe_crab = "Giant Horsehoe Crab" + +class ModTrash: + rusty_scrap = "Scrap Rust" + + +all_fish = tuple(all_fish) \ No newline at end of file diff --git a/worlds/stardew_valley/strings/food_names.py b/worlds/stardew_valley/strings/food_names.py index 6e2f98fd581b..5555316f8314 100644 --- a/worlds/stardew_valley/strings/food_names.py +++ b/worlds/stardew_valley/strings/food_names.py @@ -42,6 +42,7 @@ class Meal: maki_roll = "Maki Roll" maple_bar = "Maple Bar" miners_treat = "Miner's Treat" + moss_soup = "Moss Soup" omelet = "Omelet" pale_broth = "Pale Broth" pancakes = "Pancakes" @@ -103,6 +104,17 @@ class SVEMeal: grampleton_orange_chicken = "Grampleton Orange Chicken" +class TrashyMeal: + grilled_cheese = "Grilled Cheese" + fish_casserole = "Fish Casserole" + + +class ArchaeologyMeal: + diggers_delight = "Digger's Delight" + rocky_root = "Rocky Root Coffee" + ancient_jello = "Ancient Jello" + + class SVEBeverage: sports_drink = "Sports Drink" diff --git a/worlds/stardew_valley/strings/forageable_names.py b/worlds/stardew_valley/strings/forageable_names.py index 24127beb9838..c7dae8af3ce0 100644 --- a/worlds/stardew_valley/strings/forageable_names.py +++ b/worlds/stardew_valley/strings/forageable_names.py @@ -1,10 +1,26 @@ +all_edible_mushrooms = [] + + +def mushroom(name: str) -> str: + all_edible_mushrooms.append(name) + return name + + +class Mushroom: + any_edible = "Any Edible Mushroom" + chanterelle = mushroom("Chanterelle") + common = mushroom("Common Mushroom") + morel = mushroom("Morel") + purple = mushroom("Purple Mushroom") + red = "Red Mushroom" # Not in all mushrooms, as it can't be dried + magma_cap = mushroom("Magma Cap") + + class Forageable: blackberry = "Blackberry" cactus_fruit = "Cactus Fruit" cave_carrot = "Cave Carrot" - chanterelle = "Chanterelle" coconut = "Coconut" - common_mushroom = "Common Mushroom" crocus = "Crocus" crystal_fruit = "Crystal Fruit" daffodil = "Daffodil" @@ -16,8 +32,6 @@ class Forageable: holly = "Holly" journal_scrap = "Journal Scrap" leek = "Leek" - magma_cap = "Magma Cap" - morel = "Morel" secret_note = "Secret Note" spice_berry = "Spice Berry" sweet_pea = "Sweet Pea" @@ -25,8 +39,6 @@ class Forageable: wild_plum = "Wild Plum" winter_root = "Winter Root" dragon_tooth = "Dragon Tooth" - red_mushroom = "Red Mushroom" - purple_mushroom = "Purple Mushroom" rainbow_shell = "Rainbow Shell" salmonberry = "Salmonberry" snow_yam = "Snow Yam" @@ -34,28 +46,26 @@ class Forageable: class SVEForage: - ornate_treasure_chest = "Ornate Treasure Chest" - swirl_stone = "Swirl Stone" - void_pebble = "Void Pebble" - void_soul = "Void Soul" ferngill_primrose = "Ferngill Primrose" goldenrod = "Goldenrod" winter_star_rose = "Winter Star Rose" - bearberrys = "Bearberrys" + bearberry = "Bearberry" poison_mushroom = "Poison Mushroom" red_baneberry = "Red Baneberry" - big_conch = "Big Conch" + conch = "Conch" dewdrop_berry = "Dewdrop Berry" - dried_sand_dollar = "Dried Sand Dollar" + sand_dollar = "Sand Dollar" golden_ocean_flower = "Golden Ocean Flower" - lucky_four_leaf_clover = "Lucky Four Leaf Clover" + four_leaf_clover = "Four Leaf Clover" mushroom_colony = "Mushroom Colony" - poison_mushroom = "Poison Mushroom" rusty_blade = "Rusty Blade" - smelly_rafflesia = "Smelly Rafflesia" + rafflesia = "Rafflesia" thistle = "Thistle" class DistantLandsForageable: brown_amanita = "Brown Amanita" swamp_herb = "Swamp Herb" + + +all_edible_mushrooms = tuple(all_edible_mushrooms) diff --git a/worlds/stardew_valley/strings/machine_names.py b/worlds/stardew_valley/strings/machine_names.py index f9be78c41a03..d9e249a33594 100644 --- a/worlds/stardew_valley/strings/machine_names.py +++ b/worlds/stardew_valley/strings/machine_names.py @@ -1,4 +1,6 @@ class Machine: + dehydrator = "Dehydrator" + fish_smoker = "Fish Smoker" bee_house = "Bee House" bone_mill = "Bone Mill" cask = "Cask" @@ -10,6 +12,7 @@ class Machine: enricher = "Enricher" furnace = "Furnace" geode_crusher = "Geode Crusher" + mushroom_log = "Mushroom Log" heavy_tapper = "Heavy Tapper" keg = "Keg" lightning_rod = "Lightning Rod" @@ -26,4 +29,9 @@ class Machine: solar_panel = "Solar Panel" tapper = "Tapper" worm_bin = "Worm Bin" + deluxe_worm_bin = "Deluxe Worm Bin" + heavy_furnace = "Heavy Furnace" + anvil = "Anvil" + mini_forge = "Mini-Forge" + bait_maker = "Bait Maker" diff --git a/worlds/stardew_valley/strings/material_names.py b/worlds/stardew_valley/strings/material_names.py index 16511a5bcb97..797a42b73756 100644 --- a/worlds/stardew_valley/strings/material_names.py +++ b/worlds/stardew_valley/strings/material_names.py @@ -1,4 +1,5 @@ class Material: + moss = "Moss" coal = "Coal" fiber = "Fiber" hardwood = "Hardwood" diff --git a/worlds/stardew_valley/strings/metal_names.py b/worlds/stardew_valley/strings/metal_names.py index bf15b9d01c8e..7798c06defeb 100644 --- a/worlds/stardew_valley/strings/metal_names.py +++ b/worlds/stardew_valley/strings/metal_names.py @@ -44,6 +44,7 @@ class Mineral: ruby = "Ruby" emerald = "Emerald" amethyst = "Amethyst" + tigerseye = "Tigerseye" class Artifact: diff --git a/worlds/stardew_valley/strings/monster_drop_names.py b/worlds/stardew_valley/strings/monster_drop_names.py index c42e7ad5ede0..df2cacf0c6aa 100644 --- a/worlds/stardew_valley/strings/monster_drop_names.py +++ b/worlds/stardew_valley/strings/monster_drop_names.py @@ -14,4 +14,8 @@ class Loot: class ModLoot: void_shard = "Void Shard" green_mushroom = "Green Mushroom" + ornate_treasure_chest = "Ornate Treasure Chest" + swirl_stone = "Swirl Stone" + void_pebble = "Void Pebble" + void_soul = "Void Soul" diff --git a/worlds/stardew_valley/strings/quest_names.py b/worlds/stardew_valley/strings/quest_names.py index 2c02381609ec..6370b8b56875 100644 --- a/worlds/stardew_valley/strings/quest_names.py +++ b/worlds/stardew_valley/strings/quest_names.py @@ -4,6 +4,7 @@ class Quest: getting_started = "Getting Started" to_the_beach = "To The Beach" raising_animals = "Raising Animals" + feeding_animals = "Feeding Animals" advancement = "Advancement" archaeology = "Archaeology" rat_problem = "Rat Problem" @@ -49,12 +50,13 @@ class Quest: dark_talisman = "Dark Talisman" goblin_problem = "Goblin Problem" magic_ink = "Magic Ink" + giant_stump = "The Giant Stump" class ModQuest: MrGinger = "Mr.Ginger's request" AyeishaEnvelope = "Missing Envelope" - AyeishaRing = "Lost Emerald Ring" + AyeishaRing = "Ayeisha's Lost Ring" JunaCola = "Juna's Drink Request" JunaSpaghetti = "Juna's BFF Request" RailroadBoulder = "The Railroad Boulder" diff --git a/worlds/stardew_valley/strings/region_names.py b/worlds/stardew_valley/strings/region_names.py index 0fdab64fef68..9cedb6b8ef32 100644 --- a/worlds/stardew_valley/strings/region_names.py +++ b/worlds/stardew_valley/strings/region_names.py @@ -14,6 +14,7 @@ class Region: forest = "Forest" bus_stop = "Bus Stop" backwoods = "Backwoods" + tunnel_entrance = "Tunnel Entrance" bus_tunnel = "Bus Tunnel" railroad = "Railroad" secret_woods = "Secret Woods" @@ -28,7 +29,6 @@ class Region: oasis = "Oasis" casino = "Casino" mines = "The Mines" - mines_dwarf_shop = "Mines Dwarf Shop" skull_cavern_entrance = "Skull Cavern Entrance" skull_cavern = "Skull Cavern" sewer = "Sewer" @@ -73,17 +73,9 @@ class Region: alex_house = "Alex's House" elliott_house = "Elliott's House" ranch = "Marnie's Ranch" - traveling_cart = "Traveling Cart" - traveling_cart_sunday = "Traveling Cart Sunday" - traveling_cart_monday = "Traveling Cart Monday" - traveling_cart_tuesday = "Traveling Cart Tuesday" - traveling_cart_wednesday = "Traveling Cart Wednesday" - traveling_cart_thursday = "Traveling Cart Thursday" - traveling_cart_friday = "Traveling Cart Friday" - traveling_cart_saturday = "Traveling Cart Saturday" + mastery_cave = "Mastery Cave" farm_cave = "Farmcave" greenhouse = "Greenhouse" - tunnel_entrance = "Tunnel Entrance" leah_house = "Leah's Cottage" wizard_tower = "Wizard Tower" wizard_basement = "Wizard Basement" @@ -91,6 +83,7 @@ class Region: maru_room = "Maru's Room" sebastian_room = "Sebastian's Room" adventurer_guild = "Adventurer's Guild" + adventurer_guild_bedroom = "Marlon's bedroom" quarry = "Quarry" quarry_mine_entrance = "Quarry Mine Entrance" quarry_mine = "Quarry Mine" @@ -148,6 +141,20 @@ class Region: dangerous_mines_20 = "Dangerous Mines - Floor 20" dangerous_mines_60 = "Dangerous Mines - Floor 60" dangerous_mines_100 = "Dangerous Mines - Floor 100" + + +class LogicRegion: + mines_dwarf_shop = "Mines Dwarf Shop" + + traveling_cart = "Traveling Cart" + traveling_cart_sunday = "Traveling Cart Sunday" + traveling_cart_monday = "Traveling Cart Monday" + traveling_cart_tuesday = "Traveling Cart Tuesday" + traveling_cart_wednesday = "Traveling Cart Wednesday" + traveling_cart_thursday = "Traveling Cart Thursday" + traveling_cart_friday = "Traveling Cart Friday" + traveling_cart_saturday = "Traveling Cart Saturday" + kitchen = "Kitchen" shipping = "Shipping" queen_of_sauce = "The Queen of Sauce" @@ -155,9 +162,18 @@ class Region: blacksmith_iron = "Blacksmith Iron Upgrades" blacksmith_gold = "Blacksmith Gold Upgrades" blacksmith_iridium = "Blacksmith Iridium Upgrades" - farming = "Farming" + + spring_farming = "Spring Farming" + summer_farming = "Summer Farming" + fall_farming = "Fall Farming" + winter_farming = "Winter Farming" + indoor_farming = "Indoor Farming" + summer_or_fall_farming = "Summer or Fall Farming" + fishing = "Fishing" egg_festival = "Egg Festival" + desert_festival = "Desert Festival" + trout_derby = "Trout Derby" flower_dance = "Flower Dance" luau = "Luau" moonlight_jellies = "Dance of the Moonlight Jellies" @@ -166,6 +182,13 @@ class Region: festival_of_ice = "Festival of Ice" night_market = "Night Market" winter_star = "Feast of the Winter Star" + squidfest = "SquidFest" + raccoon_daddy = "Raccoon Bundles" + raccoon_shop = "Raccoon Shop" + bookseller_1 = "Bookseller Experience Books" + bookseller_2 = "Bookseller Year 1 Books" + bookseller_3 = "Bookseller Year 3 Books" + forest_waterfall = "Waterfall" class DeepWoodsRegion: @@ -302,5 +325,3 @@ class BoardingHouseRegion: lost_valley_house_1 = "Lost Valley Ruins - First House" lost_valley_house_2 = "Lost Valley Ruins - Second House" buffalo_ranch = "Buffalo's Ranch" - - diff --git a/worlds/stardew_valley/strings/season_names.py b/worlds/stardew_valley/strings/season_names.py index f3659bc87fe0..1c4971c3f802 100644 --- a/worlds/stardew_valley/strings/season_names.py +++ b/worlds/stardew_valley/strings/season_names.py @@ -5,4 +5,5 @@ class Season: winter = "Winter" progressive = "Progressive Season" + all = (spring, summer, fall, winter) not_winter = (spring, summer, fall,) diff --git a/worlds/stardew_valley/strings/seed_names.py b/worlds/stardew_valley/strings/seed_names.py index 398b370f2745..f2799d4e449f 100644 --- a/worlds/stardew_valley/strings/seed_names.py +++ b/worlds/stardew_valley/strings/seed_names.py @@ -1,35 +1,72 @@ class Seed: + amaranth = "Amaranth Seeds" + artichoke = "Artichoke Seeds" + bean = "Bean Starter" + beet = "Beet Seeds" + blueberry = "Blueberry Seeds" + bok_choy = "Bok Choy Seeds" + broccoli = "Broccoli Seeds" + cactus = "Cactus Seeds" + carrot = "Carrot Seeds" + cauliflower = "Cauliflower Seeds" + coffee_starter = "Coffee Bean (Starter)" + """This item does not really exist and should never end up being displayed. + It's there to patch the loop in logic because "Coffee Bean" is both the seed and the crop.""" coffee = "Coffee Bean" + corn = "Corn Seeds" + cranberry = "Cranberry Seeds" + eggplant = "Eggplant Seeds" + fairy = "Fairy Seeds" garlic = "Garlic Seeds" + grape = "Grape Starter" + hops = "Hops Starter" jazz = "Jazz Seeds" + kale = "Kale Seeds" melon = "Melon Seeds" mixed = "Mixed Seeds" + mixed_flower = "Mixed Flower Seeds" + parsnip = "Parsnip Seeds" + pepper = "Pepper Seeds" pineapple = "Pineapple Seeds" poppy = "Poppy Seeds" + potato = "Potato Seeds" + powdermelon = "Powdermelon Seeds" + pumpkin = "Pumpkin Seeds" qi_bean = "Qi Bean" + radish = "Radish Seeds" + rare_seed = "Rare Seed" + red_cabbage = "Red Cabbage Seeds" + rhubarb = "Rhubarb Seeds" + rice = "Rice Shoot" spangle = "Spangle Seeds" + starfruit = "Starfruit Seeds" + strawberry = "Strawberry Seeds" + summer_squash = "Summer Squash Seeds" sunflower = "Sunflower Seeds" taro = "Taro Tuber" tomato = "Tomato Seeds" tulip = "Tulip Bulb" wheat = "Wheat Seeds" + yam = "Yam Seeds" class TreeSeed: acorn = "Acorn" maple = "Maple Seed" + mossy = "Mossy Seed" + mystic = "Mystic Tree Seed" pine = "Pine Cone" mahogany = "Mahogany Seed" mushroom = "Mushroom Tree Seed" class SVESeed: - stalk_seed = "Stalk Seed" - fungus_seed = "Fungus Seed" - slime_seed = "Slime Seed" - void_seed = "Void Seed" - shrub_seed = "Shrub Seed" - ancient_ferns_seed = "Ancient Ferns Seed" + stalk = "Stalk Seed" + fungus = "Fungus Seed" + slime = "Slime Seed" + void = "Void Seed" + shrub = "Shrub Seed" + ancient_fern = "Ancient Fern Seed" class DistantLandsSeed: diff --git a/worlds/stardew_valley/strings/skill_names.py b/worlds/stardew_valley/strings/skill_names.py index bae4c26fd716..7f3a61f2dfcd 100644 --- a/worlds/stardew_valley/strings/skill_names.py +++ b/worlds/stardew_valley/strings/skill_names.py @@ -15,4 +15,6 @@ class ModSkill: socializing = "Socializing" +all_vanilla_skills = {Skill.farming, Skill.foraging, Skill.fishing, Skill.mining, Skill.combat} all_mod_skills = {ModSkill.luck, ModSkill.binning, ModSkill.archaeology, ModSkill.cooking, ModSkill.magic, ModSkill.socializing} +all_skills = {*all_vanilla_skills, *all_mod_skills} diff --git a/worlds/stardew_valley/strings/tool_names.py b/worlds/stardew_valley/strings/tool_names.py index ea8c00b9bfd2..761f50e0a9bb 100644 --- a/worlds/stardew_valley/strings/tool_names.py +++ b/worlds/stardew_valley/strings/tool_names.py @@ -4,6 +4,7 @@ class Tool: hoe = "Hoe" watering_can = "Watering Can" trash_can = "Trash Can" + pan = "Pan" fishing_rod = "Fishing Rod" scythe = "Scythe" golden_scythe = "Golden Scythe" diff --git a/worlds/stardew_valley/strings/wallet_item_names.py b/worlds/stardew_valley/strings/wallet_item_names.py index 28f09b0558fc..32655efe88c2 100644 --- a/worlds/stardew_valley/strings/wallet_item_names.py +++ b/worlds/stardew_valley/strings/wallet_item_names.py @@ -8,3 +8,4 @@ class Wallet: skull_key = "Skull Key" dark_talisman = "Dark Talisman" club_card = "Club Card" + mastery_of_the_five_ways = "Mastery Of The Five Ways" diff --git a/worlds/stardew_valley/test/TestBooksanity.py b/worlds/stardew_valley/test/TestBooksanity.py new file mode 100644 index 000000000000..3ca52f5728c1 --- /dev/null +++ b/worlds/stardew_valley/test/TestBooksanity.py @@ -0,0 +1,207 @@ +from . import SVTestBase +from ..options import ExcludeGingerIsland, Booksanity, Shipsanity +from ..strings.ap_names.ap_option_names import OptionName +from ..strings.book_names import Book, LostBook + +power_books = [Book.animal_catalogue, Book.book_of_mysteries, + Book.the_alleyway_buffet, Book.the_art_o_crabbing, Book.dwarvish_safety_manual, + Book.jewels_of_the_sea, Book.raccoon_journal, Book.woodys_secret, Book.jack_be_nimble_jack_be_thick, Book.friendship_101, + Book.monster_compendium, Book.mapping_cave_systems, Book.treasure_appraisal_guide, Book.way_of_the_wind_pt_1, Book.way_of_the_wind_pt_2, + Book.horse_the_book, Book.ol_slitherlegs, Book.price_catalogue, Book.the_diamond_hunter, ] + +skill_books = [Book.combat_quarterly, Book.woodcutters_weekly, Book.book_of_stars, Book.stardew_valley_almanac, Book.bait_and_bobber, Book.mining_monthly, + Book.queen_of_sauce_cookbook, ] + +lost_books = [ + LostBook.tips_on_farming, LostBook.this_is_a_book_by_marnie, LostBook.on_foraging, LostBook.the_fisherman_act_1, + LostBook.how_deep_do_the_mines_go, LostBook.an_old_farmers_journal, LostBook.scarecrows, LostBook.the_secret_of_the_stardrop, + LostBook.journey_of_the_prairie_king_the_smash_hit_video_game, LostBook.a_study_on_diamond_yields, LostBook.brewmasters_guide, + LostBook.mysteries_of_the_dwarves, LostBook.highlights_from_the_book_of_yoba, LostBook.marriage_guide_for_farmers, LostBook.the_fisherman_act_ii, + LostBook.technology_report, LostBook.secrets_of_the_legendary_fish, LostBook.gunther_tunnel_notice, LostBook.note_from_gunther, + LostBook.goblins_by_m_jasper, LostBook.secret_statues_acrostics, ] + +lost_book = "Progressive Lost Book" + + +class TestBooksanityNone(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Shipsanity: Shipsanity.option_everything, + Booksanity: Booksanity.option_none, + } + + def test_no_power_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in power_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_no_skill_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in skill_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_no_lost_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in lost_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_no_power_items(self): + item_names = {location.name for location in self.multiworld.get_items()} + for book in power_books: + with self.subTest(book): + self.assertNotIn(f"Power: {book}", item_names) + with self.subTest(lost_book): + self.assertNotIn(lost_book, item_names) + + def test_can_ship_all_books(self): + self.collect_everything() + shipsanity_prefix = "Shipsanity: " + for location in self.multiworld.get_locations(): + if not location.name.startswith(shipsanity_prefix): + continue + item_to_ship = location.name[len(shipsanity_prefix):] + if item_to_ship not in power_books and item_to_ship not in skill_books: + continue + with self.subTest(location.name): + self.assert_reach_location_true(location, self.multiworld.state) + + +class TestBooksanityPowers(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Shipsanity: Shipsanity.option_everything, + Booksanity: Booksanity.option_power, + } + + def test_all_power_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_no_skill_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in skill_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_no_lost_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in lost_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_all_power_items(self): + item_names = {location.name for location in self.multiworld.get_items()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Power: {book}", item_names) + with self.subTest(lost_book): + self.assertNotIn(lost_book, item_names) + + def test_can_ship_all_books(self): + self.collect_everything() + shipsanity_prefix = "Shipsanity: " + for location in self.multiworld.get_locations(): + if not location.name.startswith(shipsanity_prefix): + continue + item_to_ship = location.name[len(shipsanity_prefix):] + if item_to_ship not in power_books and item_to_ship not in skill_books: + continue + with self.subTest(location.name): + self.assert_reach_location_true(location, self.multiworld.state) + + +class TestBooksanityPowersAndSkills(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Shipsanity: Shipsanity.option_everything, + Booksanity: Booksanity.option_power_skill, + } + + def test_all_power_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_all_skill_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in skill_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_no_lost_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in lost_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_all_power_items(self): + item_names = {location.name for location in self.multiworld.get_items()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Power: {book}", item_names) + with self.subTest(lost_book): + self.assertNotIn(lost_book, item_names) + + def test_can_ship_all_books(self): + self.collect_everything() + shipsanity_prefix = "Shipsanity: " + for location in self.multiworld.get_locations(): + if not location.name.startswith(shipsanity_prefix): + continue + item_to_ship = location.name[len(shipsanity_prefix):] + if item_to_ship not in power_books and item_to_ship not in skill_books: + continue + with self.subTest(location.name): + self.assert_reach_location_true(location, self.multiworld.state) + + +class TestBooksanityAll(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Shipsanity: Shipsanity.option_everything, + Booksanity: Booksanity.option_all, + } + + def test_all_power_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_all_skill_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in skill_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_all_lost_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in lost_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_all_power_items(self): + item_names = {location.name for location in self.multiworld.get_items()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Power: {book}", item_names) + with self.subTest(lost_book): + self.assertIn(lost_book, item_names) + + def test_can_ship_all_books(self): + self.collect_everything() + shipsanity_prefix = "Shipsanity: " + for location in self.multiworld.get_locations(): + if not location.name.startswith(shipsanity_prefix): + continue + item_to_ship = location.name[len(shipsanity_prefix):] + if item_to_ship not in power_books and item_to_ship not in skill_books: + continue + with self.subTest(location.name): + self.assert_reach_location_true(location, self.multiworld.state) diff --git a/worlds/stardew_valley/test/TestBundles.py b/worlds/stardew_valley/test/TestBundles.py index cd6828cd79e5..091f39b2568e 100644 --- a/worlds/stardew_valley/test/TestBundles.py +++ b/worlds/stardew_valley/test/TestBundles.py @@ -1,8 +1,12 @@ import unittest -from ..data.bundle_data import all_bundle_items_except_money, quality_crops_items_thematic +from . import SVTestBase +from .. import BundleRandomization +from ..data.bundle_data import all_bundle_items_except_money, quality_crops_items_thematic, quality_foraging_items, quality_fish_items +from ..options import BundlePlando +from ..strings.bundle_names import BundleName from ..strings.crop_names import Fruit -from ..strings.quality_names import CropQuality +from ..strings.quality_names import CropQuality, ForageQuality, FishQuality class TestBundles(unittest.TestCase): @@ -27,3 +31,60 @@ def test_quality_crops_have_correct_quality(self): with self.subTest(bundle_item.item_name): self.assertEqual(bundle_item.quality, CropQuality.gold) + def test_quality_foraging_have_correct_amounts(self): + for bundle_item in quality_foraging_items: + with self.subTest(bundle_item.item_name): + self.assertEqual(bundle_item.amount, 3) + + def test_quality_foraging_have_correct_quality(self): + for bundle_item in quality_foraging_items: + with self.subTest(bundle_item.item_name): + self.assertEqual(bundle_item.quality, ForageQuality.gold) + + def test_quality_fish_have_correct_amounts(self): + for bundle_item in quality_fish_items: + with self.subTest(bundle_item.item_name): + self.assertEqual(bundle_item.amount, 2) + + def test_quality_fish_have_correct_quality(self): + for bundle_item in quality_fish_items: + with self.subTest(bundle_item.item_name): + self.assertEqual(bundle_item.quality, FishQuality.gold) + + +class TestRemixedPlandoBundles(SVTestBase): + plando_bundles = {BundleName.money_2500, BundleName.money_5000, BundleName.money_10000, BundleName.gambler, BundleName.ocean_fish, + BundleName.lake_fish, BundleName.deep_fishing, BundleName.spring_fish, BundleName.legendary_fish, BundleName.bait} + options = { + BundleRandomization: BundleRandomization.option_remixed, + BundlePlando: frozenset(plando_bundles) + } + + def test_all_plando_bundles_are_there(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for bundle_name in self.plando_bundles: + with self.subTest(f"{bundle_name}"): + self.assertIn(bundle_name, location_names) + self.assertNotIn(BundleName.money_25000, location_names) + self.assertNotIn(BundleName.carnival, location_names) + self.assertNotIn(BundleName.night_fish, location_names) + self.assertNotIn(BundleName.specialty_fish, location_names) + self.assertNotIn(BundleName.specific_bait, location_names) + + +class TestRemixedAnywhereBundles(SVTestBase): + fish_bundle_names = {BundleName.spring_fish, BundleName.summer_fish, BundleName.fall_fish, BundleName.winter_fish, BundleName.ocean_fish, + BundleName.lake_fish, BundleName.river_fish, BundleName.night_fish, BundleName.legendary_fish, BundleName.specialty_fish, + BundleName.bait, BundleName.specific_bait, BundleName.crab_pot, BundleName.tackle, BundleName.quality_fish, + BundleName.rain_fish, BundleName.master_fisher} + options = { + BundleRandomization: BundleRandomization.option_remixed_anywhere, + BundlePlando: frozenset(fish_bundle_names) + } + + def test_all_plando_bundles_are_there(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for bundle_name in self.fish_bundle_names: + with self.subTest(f"{bundle_name}"): + self.assertIn(bundle_name, location_names) + diff --git a/worlds/stardew_valley/test/TestData.py b/worlds/stardew_valley/test/TestData.py index a77dc17319b7..86550705b917 100644 --- a/worlds/stardew_valley/test/TestData.py +++ b/worlds/stardew_valley/test/TestData.py @@ -2,19 +2,52 @@ from ..items import load_item_csv from ..locations import load_location_csv +from ..options import Mods class TestCsvIntegrity(unittest.TestCase): def test_items_integrity(self): items = load_item_csv() - for item in items: - self.assertIsNotNone(item.code_without_offset, "Some item do not have an id." - " Run the script `update_data.py` to generate them.") + with self.subTest("Test all items have an id"): + for item in items: + self.assertIsNotNone(item.code_without_offset, "Some item do not have an id." + " Run the script `update_data.py` to generate them.") + with self.subTest("Test all ids are unique"): + all_ids = [item.code_without_offset for item in items] + unique_ids = set(all_ids) + self.assertEqual(len(all_ids), len(unique_ids)) + + with self.subTest("Test all names are unique"): + all_names = [item.name for item in items] + unique_names = set(all_names) + self.assertEqual(len(all_names), len(unique_names)) + + with self.subTest("Test all mod names are valid"): + mod_names = {item.mod_name for item in items} + for mod_name in mod_names: + if mod_name: + self.assertIn(mod_name, Mods.valid_keys) def test_locations_integrity(self): locations = load_location_csv() - for location in locations: - self.assertIsNotNone(location.code_without_offset, "Some location do not have an id." - " Run the script `update_data.py` to generate them.") + with self.subTest("Test all locations have an id"): + for location in locations: + self.assertIsNotNone(location.code_without_offset, "Some location do not have an id." + " Run the script `update_data.py` to generate them.") + with self.subTest("Test all ids are unique"): + all_ids = [location.code_without_offset for location in locations] + unique_ids = set(all_ids) + self.assertEqual(len(all_ids), len(unique_ids)) + + with self.subTest("Test all names are unique"): + all_names = [location.name for location in locations] + unique_names = set(all_names) + self.assertEqual(len(all_names), len(unique_names)) + + with self.subTest("Test all mod names are valid"): + mod_names = {location.mod_name for location in locations} + for mod_name in mod_names: + if mod_name: + self.assertIn(mod_name, Mods.valid_keys) diff --git a/worlds/stardew_valley/test/TestFarmType.py b/worlds/stardew_valley/test/TestFarmType.py new file mode 100644 index 000000000000..f78edc3eece8 --- /dev/null +++ b/worlds/stardew_valley/test/TestFarmType.py @@ -0,0 +1,31 @@ +from . import SVTestBase +from .assertion import WorldAssertMixin +from .. import options + + +class TestStartInventoryStandardFarm(WorldAssertMixin, SVTestBase): + options = { + options.FarmType.internal_name: options.FarmType.option_standard, + } + + def test_start_inventory_progressive_coops(self): + start_items = dict(map(lambda x: (x.name, self.multiworld.precollected_items[self.player].count(x)), self.multiworld.precollected_items[self.player])) + items = dict(map(lambda x: (x.name, self.multiworld.itempool.count(x)), self.multiworld.itempool)) + self.assertIn("Progressive Coop", items) + self.assertEqual(items["Progressive Coop"], 3) + self.assertNotIn("Progressive Coop", start_items) + + +class TestStartInventoryMeadowLands(WorldAssertMixin, SVTestBase): + options = { + options.FarmType.internal_name: options.FarmType.option_meadowlands, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, + } + + def test_start_inventory_progressive_coops(self): + start_items = dict(map(lambda x: (x.name, self.multiworld.precollected_items[self.player].count(x)), self.multiworld.precollected_items[self.player])) + items = dict(map(lambda x: (x.name, self.multiworld.itempool.count(x)), self.multiworld.itempool)) + self.assertIn("Progressive Coop", items) + self.assertEqual(items["Progressive Coop"], 2) + self.assertIn("Progressive Coop", start_items) + self.assertEqual(start_items["Progressive Coop"], 1) diff --git a/worlds/stardew_valley/test/TestFill.py b/worlds/stardew_valley/test/TestFill.py new file mode 100644 index 000000000000..0bfacb6ef6f5 --- /dev/null +++ b/worlds/stardew_valley/test/TestFill.py @@ -0,0 +1,30 @@ +from . import SVTestBase, minimal_locations_maximal_items +from .assertion import WorldAssertMixin +from .. import options +from ..mods.mod_data import ModNames + + +class TestMinLocationsMaxItems(WorldAssertMixin, SVTestBase): + options = minimal_locations_maximal_items() + + def run_default_tests(self) -> bool: + return True + + def test_fill(self): + self.assert_basic_checks(self.multiworld) + + +class TestSpecificSeedForTroubleshooting(WorldAssertMixin, SVTestBase): + options = { + options.Fishsanity: options.Fishsanity.option_all, + options.Goal: options.Goal.option_master_angler, + options.QuestLocations: -1, + options.Mods: (ModNames.sve,), + } + seed = 65453499742665118161 + + def run_default_tests(self) -> bool: + return True + + def test_fill(self): + self.assert_basic_checks(self.multiworld) diff --git a/worlds/stardew_valley/test/TestFishsanity.py b/worlds/stardew_valley/test/TestFishsanity.py new file mode 100644 index 000000000000..c5d87c0f8dd7 --- /dev/null +++ b/worlds/stardew_valley/test/TestFishsanity.py @@ -0,0 +1,405 @@ +import unittest +from typing import ClassVar, Set + +from . import SVTestBase +from .assertion import WorldAssertMixin +from ..content.feature import fishsanity +from ..mods.mod_data import ModNames +from ..options import Fishsanity, ExcludeGingerIsland, Mods, SpecialOrderLocations, Goal, QuestLocations +from ..strings.fish_names import Fish, SVEFish, DistantLandsFish + +pelican_town_legendary_fishes = {Fish.angler, Fish.crimsonfish, Fish.glacierfish, Fish.legend, Fish.mutant_carp, } +pelican_town_hard_special_fishes = {Fish.lava_eel, Fish.octopus, Fish.scorpion_carp, Fish.ice_pip, Fish.super_cucumber, } +pelican_town_medium_special_fishes = {Fish.blobfish, Fish.dorado, } +pelican_town_hard_normal_fishes = {Fish.lingcod, Fish.pufferfish, Fish.void_salmon, } +pelican_town_medium_normal_fishes = { + Fish.albacore, Fish.catfish, Fish.eel, Fish.flounder, Fish.ghostfish, Fish.goby, Fish.halibut, Fish.largemouth_bass, Fish.midnight_carp, + Fish.midnight_squid, Fish.pike, Fish.red_mullet, Fish.salmon, Fish.sandfish, Fish.slimejack, Fish.stonefish, Fish.spook_fish, Fish.squid, Fish.sturgeon, + Fish.tiger_trout, Fish.tilapia, Fish.tuna, Fish.woodskip, +} +pelican_town_easy_normal_fishes = { + Fish.anchovy, Fish.bream, Fish.bullhead, Fish.carp, Fish.chub, Fish.herring, Fish.perch, Fish.rainbow_trout, Fish.red_snapper, Fish.sardine, Fish.shad, + Fish.sea_cucumber, Fish.shad, Fish.smallmouth_bass, Fish.sunfish, Fish.walleye, +} +pelican_town_crab_pot_fishes = { + Fish.clam, Fish.cockle, Fish.crab, Fish.crayfish, Fish.lobster, Fish.mussel, Fish.oyster, Fish.periwinkle, Fish.shrimp, Fish.snail, +} + +ginger_island_hard_fishes = {Fish.pufferfish, Fish.stingray, Fish.super_cucumber, } +ginger_island_medium_fishes = {Fish.blue_discus, Fish.lionfish, Fish.tilapia, Fish.tuna, } +qi_board_legendary_fishes = {Fish.ms_angler, Fish.son_of_crimsonfish, Fish.glacierfish_jr, Fish.legend_ii, Fish.radioactive_carp, } + +sve_pelican_town_hard_fishes = { + SVEFish.grass_carp, SVEFish.king_salmon, SVEFish.kittyfish, SVEFish.meteor_carp, SVEFish.puppyfish, SVEFish.radioactive_bass, SVEFish.undeadfish, + SVEFish.void_eel, +} +sve_pelican_town_medium_fishes = { + SVEFish.bonefish, SVEFish.butterfish, SVEFish.frog, SVEFish.goldenfish, SVEFish.snatcher_worm, SVEFish.water_grub, +} +sve_pelican_town_easy_fishes = {SVEFish.bull_trout, SVEFish.minnow, } +sve_ginger_island_hard_fishes = {SVEFish.gemfish, SVEFish.shiny_lunaloo, } +sve_ginger_island_medium_fishes = {SVEFish.daggerfish, SVEFish.lunaloo, SVEFish.starfish, SVEFish.torpedo_trout, } +sve_ginger_island_easy_fishes = {SVEFish.baby_lunaloo, SVEFish.clownfish, SVEFish.seahorse, SVEFish.sea_sponge, } + +distant_lands_hard_fishes = {DistantLandsFish.giant_horsehoe_crab, } +distant_lands_easy_fishes = {DistantLandsFish.void_minnow, DistantLandsFish.purple_algae, DistantLandsFish.swamp_leech, } + + +def complete_options_with_default(options): + return { + **{ + ExcludeGingerIsland: ExcludeGingerIsland.default, + Mods: Mods.default, + SpecialOrderLocations: SpecialOrderLocations.default, + }, + **options + } + + +class SVFishsanityTestBase(SVTestBase): + expected_fishes: ClassVar[Set[str]] = set() + + @classmethod + def setUpClass(cls) -> None: + if cls is SVFishsanityTestBase: + raise unittest.SkipTest("Base tests disabled") + + super().setUpClass() + + def test_fishsanity(self): + with self.subTest("Locations are valid"): + self.check_all_locations_match_expected_fishes() + + def check_all_locations_match_expected_fishes(self): + location_fishes = { + name + for location_name in self.get_real_location_names() + if (name := fishsanity.extract_fish_from_location_name(location_name)) is not None + } + + self.assertEqual(location_fishes, self.expected_fishes) + + +class TestFishsanityNoneVanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_none, + }) + + @property + def run_default_tests(self) -> bool: + # None is default + return False + + +class TestFishsanityLegendaries_Vanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_legendaries, + }) + expected_fishes = pelican_town_legendary_fishes + + +class TestFishsanityLegendaries_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_legendaries, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + ExcludeGingerIsland: ExcludeGingerIsland.option_false + }) + expected_fishes = pelican_town_legendary_fishes | qi_board_legendary_fishes + + +class TestFishsanitySpecial(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_special, + }) + expected_fishes = pelican_town_legendary_fishes | pelican_town_hard_special_fishes | pelican_town_medium_special_fishes + + +class TestFishsanityAll_Vanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes + ) + + +class TestFishsanityAll_ExcludeGingerIsland(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + ExcludeGingerIsland: ExcludeGingerIsland.option_true, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes + ) + + +class TestFishsanityAll_SVE(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + Mods: ModNames.sve, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes | + sve_pelican_town_hard_fishes | + sve_pelican_town_medium_fishes | + sve_pelican_town_easy_fishes | + sve_ginger_island_hard_fishes | + sve_ginger_island_medium_fishes | + sve_ginger_island_easy_fishes + ) + + +class TestFishsanityAll_ExcludeGingerIsland_SVE(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + ExcludeGingerIsland: ExcludeGingerIsland.option_true, + Mods: ModNames.sve, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + sve_pelican_town_hard_fishes | + sve_pelican_town_medium_fishes | + sve_pelican_town_easy_fishes + ) + + +class TestFishsanityAll_DistantLands(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + Mods: ModNames.distant_lands, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes | + distant_lands_hard_fishes | + distant_lands_easy_fishes + ) + + +class TestFishsanityAll_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + ExcludeGingerIsland: ExcludeGingerIsland.option_false + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes | + qi_board_legendary_fishes + ) + + +class TestFishsanityAll_ExcludeGingerIsland_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + ExcludeGingerIsland: ExcludeGingerIsland.option_true, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes + ) + + +class TestFishsanityExcludeLegendaries_Vanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_legendaries, + }) + expected_fishes = ( + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes + ) + + +class TestFishsanityExcludeLegendaries_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_legendaries, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + ExcludeGingerIsland: ExcludeGingerIsland.option_false + }) + expected_fishes = ( + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes + ) + + +class TestFishsanityExcludeHardFishes_Vanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_hard_fish, + }) + expected_fishes = ( + pelican_town_medium_special_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_medium_fishes + ) + + +class TestFishsanityExcludeHardFishes_SVE(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_hard_fish, + Mods: ModNames.sve, + }) + expected_fishes = ( + pelican_town_medium_special_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_medium_fishes | + sve_pelican_town_medium_fishes | + sve_pelican_town_easy_fishes | + sve_ginger_island_medium_fishes | + sve_ginger_island_easy_fishes + ) + + +class TestFishsanityExcludeHardFishes_DistantLands(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_hard_fish, + Mods: ModNames.distant_lands, + }) + expected_fishes = ( + pelican_town_medium_special_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_medium_fishes | + distant_lands_easy_fishes + ) + + +class TestFishsanityExcludeHardFishes_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_hard_fish, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + ExcludeGingerIsland: ExcludeGingerIsland.option_false + }) + expected_fishes = ( + pelican_town_medium_special_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_medium_fishes + ) + + +class TestFishsanityOnlyEasyFishes_Vanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_only_easy_fish, + }) + expected_fishes = ( + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes + ) + + +class TestFishsanityOnlyEasyFishes_SVE(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_only_easy_fish, + Mods: ModNames.sve, + }) + expected_fishes = ( + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + sve_pelican_town_easy_fishes | + sve_ginger_island_easy_fishes + ) + + +class TestFishsanityOnlyEasyFishes_DistantLands(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_only_easy_fish, + Mods: ModNames.distant_lands, + }) + expected_fishes = ( + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + distant_lands_easy_fishes + ) + + +class TestFishsanityOnlyEasyFishes_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_only_easy_fish, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + ExcludeGingerIsland: ExcludeGingerIsland.option_false + }) + expected_fishes = ( + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes + ) + + +class TestFishsanityMasterAnglerSVEWithoutQuests(WorldAssertMixin, SVTestBase): + options = { + Fishsanity: Fishsanity.option_all, + Goal: Goal.option_master_angler, + QuestLocations: -1, + Mods: (ModNames.sve,), + } + + def run_default_tests(self) -> bool: + return True + + def test_fill(self): + self.assert_basic_checks(self.multiworld) diff --git a/worlds/stardew_valley/test/TestFriendsanity.py b/worlds/stardew_valley/test/TestFriendsanity.py new file mode 100644 index 000000000000..842c0edd0980 --- /dev/null +++ b/worlds/stardew_valley/test/TestFriendsanity.py @@ -0,0 +1,159 @@ +import unittest +from collections import Counter +from typing import ClassVar, Set + +from . import SVTestBase +from ..content.feature import friendsanity +from ..options import Friendsanity, FriendsanityHeartSize + +all_vanilla_bachelor = { + "Harvey", "Elliott", "Sam", "Alex", "Shane", "Sebastian", "Emily", "Haley", "Leah", "Abigail", "Penny", "Maru" +} + +all_vanilla_starting_npc = { + "Alex", "Elliott", "Harvey", "Sam", "Sebastian", "Shane", "Abigail", "Emily", "Haley", "Leah", "Maru", "Penny", "Caroline", "Clint", "Demetrius", "Evelyn", + "George", "Gus", "Jas", "Jodi", "Lewis", "Linus", "Marnie", "Pam", "Pierre", "Robin", "Vincent", "Willy", "Wizard", "Pet", +} + +all_vanilla_npc = { + "Alex", "Elliott", "Harvey", "Sam", "Sebastian", "Shane", "Abigail", "Emily", "Haley", "Leah", "Maru", "Penny", "Caroline", "Clint", "Demetrius", "Evelyn", + "George", "Gus", "Jas", "Jodi", "Lewis", "Linus", "Marnie", "Pam", "Pierre", "Robin", "Vincent", "Willy", "Wizard", "Pet", "Sandy", "Dwarf", "Kent", "Leo", + "Krobus" +} + + +class SVFriendsanityTestBase(SVTestBase): + expected_npcs: ClassVar[Set[str]] = set() + expected_pet_heart_size: ClassVar[Set[str]] = set() + expected_bachelor_heart_size: ClassVar[Set[str]] = set() + expected_other_heart_size: ClassVar[Set[str]] = set() + + @classmethod + def setUpClass(cls) -> None: + if cls is SVFriendsanityTestBase: + raise unittest.SkipTest("Base tests disabled") + + super().setUpClass() + + def test_friendsanity(self): + with self.subTest("Items are valid"): + self.check_all_items_match_expected_npcs() + with self.subTest("Correct number of items"): + self.check_correct_number_of_items() + with self.subTest("Locations are valid"): + self.check_all_locations_match_expected_npcs() + with self.subTest("Locations heart size are valid"): + self.check_all_locations_match_heart_size() + + def check_all_items_match_expected_npcs(self): + npc_names = { + name + for item in self.multiworld.itempool + if (name := friendsanity.extract_npc_from_item_name(item.name)) is not None + } + + self.assertEqual(npc_names, self.expected_npcs) + + def check_correct_number_of_items(self): + item_by_npc = Counter() + for item in self.multiworld.itempool: + name = friendsanity.extract_npc_from_item_name(item.name) + if name is None: + continue + + item_by_npc[name] += 1 + + for name, count in item_by_npc.items(): + + if name == "Pet": + self.assertEqual(count, len(self.expected_pet_heart_size)) + elif self.world.content.villagers[name].bachelor: + self.assertEqual(count, len(self.expected_bachelor_heart_size)) + else: + self.assertEqual(count, len(self.expected_other_heart_size)) + + def check_all_locations_match_expected_npcs(self): + npc_names = { + name_and_heart[0] + for location_name in self.get_real_location_names() + if (name_and_heart := friendsanity.extract_npc_from_location_name(location_name))[0] is not None + } + + self.assertEqual(npc_names, self.expected_npcs) + + def check_all_locations_match_heart_size(self): + for location_name in self.get_real_location_names(): + name, heart_size = friendsanity.extract_npc_from_location_name(location_name) + if name is None: + continue + + if name == "Pet": + self.assertIn(heart_size, self.expected_pet_heart_size) + elif self.world.content.villagers[name].bachelor: + self.assertIn(heart_size, self.expected_bachelor_heart_size) + else: + self.assertIn(heart_size, self.expected_other_heart_size) + + +class TestFriendsanityNone(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_none, + } + + @property + def run_default_tests(self) -> bool: + # None is default + return False + + +class TestFriendsanityBachelors(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_bachelors, + FriendsanityHeartSize: 1, + } + expected_npcs = all_vanilla_bachelor + expected_bachelor_heart_size = {1, 2, 3, 4, 5, 6, 7, 8} + + +class TestFriendsanityStartingNpcs(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_starting_npcs, + FriendsanityHeartSize: 1, + } + expected_npcs = all_vanilla_starting_npc + expected_pet_heart_size = {1, 2, 3, 4, 5} + expected_bachelor_heart_size = {1, 2, 3, 4, 5, 6, 7, 8} + expected_other_heart_size = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + + +class TestFriendsanityAllNpcs(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_all, + FriendsanityHeartSize: 4, + } + expected_npcs = all_vanilla_npc + expected_pet_heart_size = {4, 5} + expected_bachelor_heart_size = {4, 8} + expected_other_heart_size = {4, 8, 10} + + +class TestFriendsanityHeartSize3(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_all_with_marriage, + FriendsanityHeartSize: 3, + } + expected_npcs = all_vanilla_npc + expected_pet_heart_size = {3, 5} + expected_bachelor_heart_size = {3, 6, 9, 12, 14} + expected_other_heart_size = {3, 6, 9, 10} + + +class TestFriendsanityHeartSize5(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_all_with_marriage, + FriendsanityHeartSize: 5, + } + expected_npcs = all_vanilla_npc + expected_pet_heart_size = {5} + expected_bachelor_heart_size = {5, 10, 14} + expected_other_heart_size = {5, 10} diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py index 1b4d1476b900..8431e6857eaf 100644 --- a/worlds/stardew_valley/test/TestGeneration.py +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -1,26 +1,27 @@ from typing import List from BaseClasses import ItemClassification, Item -from . import SVTestBase, allsanity_options_without_mods, \ - allsanity_options_with_mods, minimal_locations_maximal_items, minimal_locations_maximal_items_with_island, get_minsanity_options, default_options +from . import SVTestBase from .. import items, location_table, options -from ..data.villagers_data import all_villagers_by_name, all_villagers_by_mod_by_name -from ..items import Group, item_table +from ..items import Group from ..locations import LocationTags -from ..mods.mod_data import ModNames from ..options import Friendsanity, SpecialOrderLocations, Shipsanity, Chefsanity, SeasonRandomization, Craftsanity, ExcludeGingerIsland, ToolProgression, \ - FriendsanityHeartSize + SkillProgression, Booksanity, Walnutsanity from ..strings.region_names import Region class TestBaseItemGeneration(SVTestBase): options = { - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, SeasonRandomization.internal_name: SeasonRandomization.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, Shipsanity.internal_name: Shipsanity.option_everything, Chefsanity.internal_name: Chefsanity.option_all, Craftsanity.internal_name: Craftsanity.option_all, + Booksanity.internal_name: Booksanity.option_all, + Walnutsanity.internal_name: Walnutsanity.preset_all, } def test_all_progression_items_are_added_to_the_pool(self): @@ -65,12 +66,14 @@ def test_does_not_create_exactly_two_items(self): class TestNoGingerIslandItemGeneration(SVTestBase): options = { - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, SeasonRandomization.internal_name: SeasonRandomization.option_progressive, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, Shipsanity.internal_name: Shipsanity.option_everything, Chefsanity.internal_name: Chefsanity.option_all, Craftsanity.internal_name: Craftsanity.option_all, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + Booksanity.internal_name: Booksanity.option_all, } def test_all_progression_items_except_island_are_added_to_the_pool(self): @@ -117,7 +120,16 @@ def test_does_not_create_exactly_two_items(self): class TestMonstersanityNone(SVTestBase): - options = {options.Monstersanity.internal_name: options.Monstersanity.option_none} + options = { + options.Monstersanity.internal_name: options.Monstersanity.option_none, + # Not really necessary, but it adds more locations, so we don't have to remove useful items. + options.Fishsanity.internal_name: options.Fishsanity.option_all + } + + @property + def run_default_tests(self) -> bool: + # None is default + return False def test_when_generate_world_then_5_generic_weapons_in_the_pool(self): item_pool = [item.name for item in self.multiworld.itempool] @@ -367,408 +379,15 @@ def generate_items_for_skull_100(self) -> List[Item]: return [*combat_levels, *mining_levels, *pickaxes, *swords, bus, skull_key] -class TestLocationGeneration(SVTestBase): - - def test_all_location_created_are_in_location_table(self): - for location in self.get_real_locations(): - self.assertIn(location.name, location_table) - - -class TestMinLocationAndMaxItem(SVTestBase): - options = minimal_locations_maximal_items() - - # They do not pass and I don't know why. - skip_base_tests = True - - def test_minimal_location_maximal_items_still_valid(self): - valid_locations = self.get_real_locations() - number_locations = len(valid_locations) - number_items = len([item for item in self.multiworld.itempool - if Group.RESOURCE_PACK not in item_table[item.name].groups and Group.TRAP not in item_table[item.name].groups]) - self.assertGreaterEqual(number_locations, number_items) - print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND EXCLUDED]") - - -class TestMinLocationAndMaxItemWithIsland(SVTestBase): - options = minimal_locations_maximal_items_with_island() - - def test_minimal_location_maximal_items_with_island_still_valid(self): - valid_locations = self.get_real_locations() - number_locations = len(valid_locations) - number_items = len([item for item in self.multiworld.itempool - if Group.RESOURCE_PACK not in item_table[item.name].groups and Group.TRAP not in item_table[item.name].groups]) - self.assertGreaterEqual(number_locations, number_items) - print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND INCLUDED]") - - -class TestMinSanityHasAllExpectedLocations(SVTestBase): - options = get_minsanity_options() - - def test_minsanity_has_fewer_than_locations(self): - expected_locations = 76 - real_locations = self.get_real_locations() - number_locations = len(real_locations) - self.assertLessEqual(number_locations, expected_locations) - print(f"Stardew Valley - Minsanity Locations: {number_locations}") - if number_locations != expected_locations: - print(f"\tDisappeared Locations Detected!" - f"\n\tPlease update test_minsanity_has_fewer_than_locations" - f"\n\t\tExpected: {expected_locations}" - f"\n\t\tActual: {number_locations}") - - -class TestDefaultSettingsHasAllExpectedLocations(SVTestBase): - options = default_options() - - def test_default_settings_has_exactly_locations(self): - expected_locations = 422 - real_locations = self.get_real_locations() - number_locations = len(real_locations) - print(f"Stardew Valley - Default options locations: {number_locations}") - if number_locations != expected_locations: - print(f"\tNew locations detected!" - f"\n\tPlease update test_default_settings_has_exactly_locations" - f"\n\t\tExpected: {expected_locations}" - f"\n\t\tActual: {number_locations}") - - -class TestAllSanitySettingsHasAllExpectedLocations(SVTestBase): - options = allsanity_options_without_mods() - - def test_allsanity_without_mods_has_at_least_locations(self): - expected_locations = 1956 - real_locations = self.get_real_locations() - number_locations = len(real_locations) - self.assertGreaterEqual(number_locations, expected_locations) - print(f"Stardew Valley - Allsanity Locations without mods: {number_locations}") - if number_locations != expected_locations: - print(f"\tNew locations detected!" - f"\n\tPlease update test_allsanity_without_mods_has_at_least_locations" - f"\n\t\tExpected: {expected_locations}" - f"\n\t\tActual: {number_locations}") - - -class TestAllSanityWithModsSettingsHasAllExpectedLocations(SVTestBase): - options = allsanity_options_with_mods() - - def test_allsanity_with_mods_has_at_least_locations(self): - expected_locations = 2804 - real_locations = self.get_real_locations() - number_locations = len(real_locations) - self.assertGreaterEqual(number_locations, expected_locations) - print(f"\nStardew Valley - Allsanity Locations with all mods: {number_locations}") - if number_locations != expected_locations: - print(f"\tNew locations detected!" - f"\n\tPlease update test_allsanity_with_mods_has_at_least_locations" - f"\n\t\tExpected: {expected_locations}" - f"\n\t\tActual: {number_locations}") - - -class TestFriendsanityNone(SVTestBase): +class TestShipsanityNone(SVTestBase): options = { - Friendsanity.internal_name: Friendsanity.option_none, + Shipsanity.internal_name: Shipsanity.option_none } - @property def run_default_tests(self) -> bool: # None is default return False - def test_friendsanity_none(self): - with self.subTest("No Items"): - self.check_no_friendsanity_items() - with self.subTest("No Locations"): - self.check_no_friendsanity_locations() - - def check_no_friendsanity_items(self): - for item in self.multiworld.itempool: - self.assertFalse(item.name.endswith(" <3")) - - def check_no_friendsanity_locations(self): - for location_name in self.get_real_location_names(): - self.assertFalse(location_name.startswith("Friendsanity")) - - -class TestFriendsanityBachelors(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_bachelors, - FriendsanityHeartSize.internal_name: 1, - } - bachelors = {"Harvey", "Elliott", "Sam", "Alex", "Shane", "Sebastian", "Emily", "Haley", "Leah", "Abigail", "Penny", - "Maru"} - - def test_friendsanity_only_bachelors(self): - with self.subTest("Items are valid"): - self.check_only_bachelors_items() - with self.subTest("Locations are valid"): - self.check_only_bachelors_locations() - - def check_only_bachelors_items(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertIn(villager_name, self.bachelors) - - def check_only_bachelors_locations(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if location_name.startswith(prefix): - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = parts[1] - self.assertIn(name, self.bachelors) - self.assertLessEqual(int(hearts), 8) - - -class TestFriendsanityStartingNpcs(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_starting_npcs, - FriendsanityHeartSize.internal_name: 1, - } - excluded_npcs = {"Leo", "Krobus", "Dwarf", "Sandy", "Kent"} - - def test_friendsanity_only_starting_npcs(self): - with self.subTest("Items are valid"): - self.check_only_starting_npcs_items() - with self.subTest("Locations are valid"): - self.check_only_starting_npcs_locations() - - def check_only_starting_npcs_items(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertNotIn(villager_name, self.excluded_npcs) - - def check_only_starting_npcs_locations(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if location_name.startswith(prefix): - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = parts[1] - self.assertNotIn(name, self.excluded_npcs) - self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet") - if name == "Pet": - self.assertLessEqual(int(hearts), 5) - elif all_villagers_by_name[name].bachelor: - self.assertLessEqual(int(hearts), 8) - else: - self.assertLessEqual(int(hearts), 10) - - -class TestFriendsanityAllNpcs(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_all, - FriendsanityHeartSize.internal_name: 4, - } - - def test_friendsanity_all_npcs(self): - with self.subTest("Items are valid"): - self.check_items_are_valid() - with self.subTest("Correct number of items"): - self.check_correct_number_of_items() - with self.subTest("Locations are valid"): - self.check_locations_are_valid() - - def check_items_are_valid(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertTrue(villager_name in all_villagers_by_mod_by_name[ModNames.vanilla] or villager_name == "Pet") - - def check_correct_number_of_items(self): - suffix = " <3" - item_names = [item.name for item in self.multiworld.itempool] - for villager_name in all_villagers_by_mod_by_name[ModNames.vanilla]: - heart_item_name = f"{villager_name}{suffix}" - number_heart_items = item_names.count(heart_item_name) - if all_villagers_by_name[villager_name].bachelor: - self.assertEqual(number_heart_items, 2) - else: - self.assertEqual(number_heart_items, 3) - self.assertEqual(item_names.count("Pet <3"), 2) - - def check_locations_are_valid(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if not location_name.startswith(prefix): - continue - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = int(parts[1]) - self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet") - if name == "Pet": - self.assertTrue(hearts == 4 or hearts == 5) - elif all_villagers_by_name[name].bachelor: - self.assertTrue(hearts == 4 or hearts == 8 or hearts == 12 or hearts == 14) - else: - self.assertTrue(hearts == 4 or hearts == 8 or hearts == 10) - - -class TestFriendsanityAllNpcsExcludingGingerIsland(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_all, - FriendsanityHeartSize.internal_name: 4, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true - } - - def test_friendsanity_all_npcs_exclude_island(self): - with self.subTest("Items"): - self.check_items() - with self.subTest("Locations"): - self.check_locations() - - def check_items(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertNotEqual(villager_name, "Leo") - self.assertTrue(villager_name in all_villagers_by_mod_by_name[ModNames.vanilla] or villager_name == "Pet") - - def check_locations(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if location_name.startswith(prefix): - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = parts[1] - self.assertNotEqual(name, "Leo") - self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet") - if name == "Pet": - self.assertLessEqual(int(hearts), 5) - elif all_villagers_by_name[name].bachelor: - self.assertLessEqual(int(hearts), 8) - else: - self.assertLessEqual(int(hearts), 10) - - -class TestFriendsanityHeartSize3(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, - FriendsanityHeartSize.internal_name: 3, - } - - def test_friendsanity_all_npcs_with_marriage(self): - with self.subTest("Items are valid"): - self.check_items_are_valid() - with self.subTest("Correct number of items"): - self.check_correct_number_of_items() - with self.subTest("Locations are valid"): - self.check_locations_are_valid() - - def check_items_are_valid(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertTrue(villager_name in all_villagers_by_mod_by_name[ModNames.vanilla] or villager_name == "Pet") - - def check_correct_number_of_items(self): - suffix = " <3" - item_names = [item.name for item in self.multiworld.itempool] - for villager_name in all_villagers_by_mod_by_name[ModNames.vanilla]: - heart_item_name = f"{villager_name}{suffix}" - number_heart_items = item_names.count(heart_item_name) - if all_villagers_by_name[villager_name].bachelor: - self.assertEqual(number_heart_items, 5) - else: - self.assertEqual(number_heart_items, 4) - self.assertEqual(item_names.count("Pet <3"), 2) - - def check_locations_are_valid(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if not location_name.startswith(prefix): - continue - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = int(parts[1]) - self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet") - if name == "Pet": - self.assertTrue(hearts == 3 or hearts == 5) - elif all_villagers_by_name[name].bachelor: - self.assertTrue(hearts == 3 or hearts == 6 or hearts == 9 or hearts == 12 or hearts == 14) - else: - self.assertTrue(hearts == 3 or hearts == 6 or hearts == 9 or hearts == 10) - - -class TestFriendsanityHeartSize5(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, - FriendsanityHeartSize.internal_name: 5, - } - - def test_friendsanity_all_npcs_with_marriage(self): - with self.subTest("Items are valid"): - self.check_items_are_valid() - with self.subTest("Correct number of items"): - self.check_correct_number_of_items() - with self.subTest("Locations are valid"): - self.check_locations_are_valid() - - def check_items_are_valid(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertTrue(villager_name in all_villagers_by_mod_by_name[ModNames.vanilla] or villager_name == "Pet") - - def check_correct_number_of_items(self): - suffix = " <3" - item_names = [item.name for item in self.multiworld.itempool] - for villager_name in all_villagers_by_mod_by_name[ModNames.vanilla]: - heart_item_name = f"{villager_name}{suffix}" - number_heart_items = item_names.count(heart_item_name) - if all_villagers_by_name[villager_name].bachelor: - self.assertEqual(number_heart_items, 3) - else: - self.assertEqual(number_heart_items, 2) - self.assertEqual(item_names.count("Pet <3"), 1) - - def check_locations_are_valid(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if not location_name.startswith(prefix): - continue - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = int(parts[1]) - self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet") - if name == "Pet": - self.assertTrue(hearts == 5) - elif all_villagers_by_name[name].bachelor: - self.assertTrue(hearts == 5 or hearts == 10 or hearts == 14) - else: - self.assertTrue(hearts == 5 or hearts == 10) - - -class TestShipsanityNone(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_none - } - def test_no_shipsanity_locations(self): for location in self.get_real_locations(): with self.subTest(location.name): @@ -779,6 +398,7 @@ def test_no_shipsanity_locations(self): class TestShipsanityCrops(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_crops, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi } @@ -825,7 +445,7 @@ def test_only_mainland_crop_shipsanity_locations(self): class TestShipsanityCropsNoQiCropWithoutSpecialOrders(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_crops, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board } def test_only_crop_shipsanity_locations(self): @@ -848,6 +468,7 @@ def test_island_crops_without_qi_fruit_shipsanity_locations(self): class TestShipsanityFish(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_fish, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi } @@ -896,7 +517,7 @@ def test_exclude_island_fish_shipsanity_locations(self): class TestShipsanityFishExcludeQiOrders(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_fish, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board } def test_only_fish_shipsanity_locations(self): @@ -920,6 +541,7 @@ def test_include_island_fish_no_extended_family_shipsanity_locations(self): class TestShipsanityFullShipment(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_full_shipment, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi } @@ -973,7 +595,7 @@ def test_exclude_island_items_shipsanity_locations(self): class TestShipsanityFullShipmentExcludeQiBoard(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_full_shipment, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla } def test_only_full_shipment_shipsanity_locations(self): @@ -1000,6 +622,7 @@ def test_exclude_qi_board_items_shipsanity_locations(self): class TestShipsanityFullShipmentWithFish(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi } @@ -1069,7 +692,7 @@ def test_exclude_island_items_shipsanity_locations(self): class TestShipsanityFullShipmentWithFishExcludeQiBoard(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board } def test_only_full_shipment_and_fish_shipsanity_locations(self): diff --git a/worlds/stardew_valley/test/TestItems.py b/worlds/stardew_valley/test/TestItems.py index 48bc1b152138..671fe6387258 100644 --- a/worlds/stardew_valley/test/TestItems.py +++ b/worlds/stardew_valley/test/TestItems.py @@ -1,16 +1,11 @@ -import sys -import random -import sys - from BaseClasses import MultiWorld, get_seed -from . import setup_solo_multiworld, SVTestCase, allsanity_options_without_mods, get_minsanity_options +from . import setup_solo_multiworld, SVTestCase, allsanity_no_mods_6_x_x, get_minsanity_options, solo_multiworld from .. import StardewValleyWorld from ..items import Group, item_table from ..options import Friendsanity, SeasonRandomization, Museumsanity, Shipsanity, Goal from ..strings.wallet_item_names import Wallet all_seasons = ["Spring", "Summer", "Fall", "Winter"] -all_farms = ["Standard Farm", "Riverland Farm", "Forest Farm", "Hill-top Farm", "Wilderness Farm", "Four Corners Farm", "Beach Farm"] class TestItems(SVTestCase): @@ -48,16 +43,16 @@ def test_babies_come_in_all_shapes_and_sizes(self): self.assertEqual(len(baby_permutations), 4) def test_correct_number_of_stardrops(self): - allsanity_options = allsanity_options_without_mods() - multiworld = setup_solo_multiworld(allsanity_options) - stardrop_items = [item for item in multiworld.get_items() if "Stardrop" in item.name] - self.assertEqual(len(stardrop_items), 7) + allsanity_options = allsanity_no_mods_6_x_x() + with solo_multiworld(allsanity_options) as (multiworld, _): + stardrop_items = [item for item in multiworld.get_items() if item.name == "Stardrop"] + self.assertEqual(len(stardrop_items), 7) def test_no_duplicate_rings(self): - allsanity_options = allsanity_options_without_mods() - multiworld = setup_solo_multiworld(allsanity_options) - ring_items = [item.name for item in multiworld.get_items() if Group.RING in item_table[item.name].groups] - self.assertEqual(len(ring_items), len(set(ring_items))) + allsanity_options = allsanity_no_mods_6_x_x() + with solo_multiworld(allsanity_options) as (multiworld, _): + ring_items = [item.name for item in multiworld.get_items() if Group.RING in item_table[item.name].groups] + self.assertEqual(len(ring_items), len(set(ring_items))) def test_can_start_in_any_season(self): starting_seasons_rolled = set() @@ -75,66 +70,54 @@ def test_can_start_in_any_season(self): starting_seasons_rolled.add(f"{starting_season_items[0]}") self.assertEqual(len(starting_seasons_rolled), 4) - def test_can_start_on_any_farm(self): - starting_farms_rolled = set() - for attempt_number in range(60): - if len(starting_farms_rolled) >= 7: - print(f"Already got all 7 farm types, breaking early [{attempt_number} generations]") - break - seed = random.randrange(sys.maxsize) - multiworld = setup_solo_multiworld(seed=seed, _cache={}) - starting_farm = multiworld.worlds[1].fill_slot_data()["farm_type"] - starting_farms_rolled.add(starting_farm) - self.assertEqual(len(starting_farms_rolled), 7) - class TestMetalDetectors(SVTestCase): def test_minsanity_1_metal_detector(self): options = get_minsanity_options() - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 1) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 1) def test_museumsanity_2_metal_detector(self): options = get_minsanity_options().copy() options[Museumsanity.internal_name] = Museumsanity.option_all - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 2) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 2) def test_shipsanity_full_shipment_1_metal_detector(self): options = get_minsanity_options().copy() options[Shipsanity.internal_name] = Shipsanity.option_full_shipment - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 1) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 1) def test_shipsanity_everything_2_metal_detector(self): options = get_minsanity_options().copy() options[Shipsanity.internal_name] = Shipsanity.option_everything - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 2) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 2) def test_complete_collection_2_metal_detector(self): options = get_minsanity_options().copy() options[Goal.internal_name] = Goal.option_complete_collection - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 2) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 2) def test_perfection_2_metal_detector(self): options = get_minsanity_options().copy() options[Goal.internal_name] = Goal.option_perfection - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 2) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 2) def test_maxsanity_4_metal_detector(self): options = get_minsanity_options().copy() options[Museumsanity.internal_name] = Museumsanity.option_all options[Shipsanity.internal_name] = Shipsanity.option_everything options[Goal.internal_name] = Goal.option_perfection - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 4) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 4) diff --git a/worlds/stardew_valley/test/TestLogic.py b/worlds/stardew_valley/test/TestLogic.py index 84d38ffeb449..65f7352a5e36 100644 --- a/worlds/stardew_valley/test/TestLogic.py +++ b/worlds/stardew_valley/test/TestLogic.py @@ -1,12 +1,13 @@ -from unittest import TestCase +import typing +import unittest +from unittest import TestCase, SkipTest -from . import setup_solo_multiworld, allsanity_options_with_mods -from .assertion import RuleAssertMixin +from BaseClasses import MultiWorld +from . import RuleAssertMixin, setup_solo_multiworld, allsanity_mods_6_x_x, minimal_locations_maximal_items +from .. import StardewValleyWorld from ..data.bundle_data import all_bundle_items_except_money - -multi_world = setup_solo_multiworld(allsanity_options_with_mods(), _cache={}) -world = multi_world.worlds[1] -logic = world.logic +from ..logic.logic import StardewLogic +from ..options import BundleRandomization def collect_all(mw): @@ -14,77 +15,91 @@ def collect_all(mw): mw.state.collect(item, event=True) -collect_all(multi_world) +class LogicTestBase(RuleAssertMixin, TestCase): + options: typing.Dict[str, typing.Any] = {} + multiworld: MultiWorld + logic: StardewLogic + world: StardewValleyWorld + + @classmethod + def setUpClass(cls) -> None: + if cls is LogicTestBase: + raise SkipTest("Not running test on base class.") + def setUp(self) -> None: + self.multiworld = setup_solo_multiworld(self.options, _cache={}) + collect_all(self.multiworld) + self.world = typing.cast(StardewValleyWorld, self.multiworld.worlds[1]) + self.logic = self.world.logic -class TestLogic(RuleAssertMixin, TestCase): def test_given_bundle_item_then_is_available_in_logic(self): for bundle_item in all_bundle_items_except_money: + if not bundle_item.can_appear(self.world.content, self.world.options): + continue + with self.subTest(msg=bundle_item.item_name): - self.assertIn(bundle_item.item_name, logic.registry.item_rules) + self.assertIn(bundle_item.get_item(), self.logic.registry.item_rules) def test_given_item_rule_then_can_be_resolved(self): - for item in logic.registry.item_rules.keys(): + for item in self.logic.registry.item_rules.keys(): with self.subTest(msg=item): - rule = logic.registry.item_rules[item] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.item_rules[item] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_building_rule_then_can_be_resolved(self): - for building in logic.registry.building_rules.keys(): + for building in self.logic.registry.building_rules.keys(): with self.subTest(msg=building): - rule = logic.registry.building_rules[building] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.building_rules[building] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_quest_rule_then_can_be_resolved(self): - for quest in logic.registry.quest_rules.keys(): + for quest in self.logic.registry.quest_rules.keys(): with self.subTest(msg=quest): - rule = logic.registry.quest_rules[quest] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.quest_rules[quest] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_special_order_rule_then_can_be_resolved(self): - for special_order in logic.registry.special_order_rules.keys(): + for special_order in self.logic.registry.special_order_rules.keys(): with self.subTest(msg=special_order): - rule = logic.registry.special_order_rules[special_order] - self.assert_rule_can_be_resolved(rule, multi_world.state) - - def test_given_tree_fruit_rule_then_can_be_resolved(self): - for tree_fruit in logic.registry.tree_fruit_rules.keys(): - with self.subTest(msg=tree_fruit): - rule = logic.registry.tree_fruit_rules[tree_fruit] - self.assert_rule_can_be_resolved(rule, multi_world.state) - - def test_given_seed_rule_then_can_be_resolved(self): - for seed in logic.registry.seed_rules.keys(): - with self.subTest(msg=seed): - rule = logic.registry.seed_rules[seed] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.special_order_rules[special_order] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_crop_rule_then_can_be_resolved(self): - for crop in logic.registry.crop_rules.keys(): + for crop in self.logic.registry.crop_rules.keys(): with self.subTest(msg=crop): - rule = logic.registry.crop_rules[crop] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.crop_rules[crop] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_fish_rule_then_can_be_resolved(self): - for fish in logic.registry.fish_rules.keys(): + for fish in self.logic.registry.fish_rules.keys(): with self.subTest(msg=fish): - rule = logic.registry.fish_rules[fish] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.fish_rules[fish] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_museum_rule_then_can_be_resolved(self): - for donation in logic.registry.museum_rules.keys(): + for donation in self.logic.registry.museum_rules.keys(): with self.subTest(msg=donation): - rule = logic.registry.museum_rules[donation] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.museum_rules[donation] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_cooking_rule_then_can_be_resolved(self): - for cooking_rule in logic.registry.cooking_rules.keys(): + for cooking_rule in self.logic.registry.cooking_rules.keys(): with self.subTest(msg=cooking_rule): - rule = logic.registry.cooking_rules[cooking_rule] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.cooking_rules[cooking_rule] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_location_rule_then_can_be_resolved(self): - for location in multi_world.get_locations(1): + for location in self.multiworld.get_locations(1): with self.subTest(msg=location.name): rule = location.access_rule - self.assert_rule_can_be_resolved(rule, multi_world.state) + self.assert_rule_can_be_resolved(rule, self.multiworld.state) + + +class TestAllSanityLogic(LogicTestBase): + options = allsanity_mods_6_x_x() + + +@unittest.skip("This test does not pass because some content is still not in content packs.") +class TestMinLocationsMaxItemsLogic(LogicTestBase): + options = minimal_locations_maximal_items() + options[BundleRandomization.internal_name] = BundleRandomization.default diff --git a/worlds/stardew_valley/test/TestMultiplePlayers.py b/worlds/stardew_valley/test/TestMultiplePlayers.py index 39be7d6f7ab2..2f2092fdf7b6 100644 --- a/worlds/stardew_valley/test/TestMultiplePlayers.py +++ b/worlds/stardew_valley/test/TestMultiplePlayers.py @@ -19,7 +19,7 @@ def test_different_festival_settings(self): multiworld = setup_multiworld(multiplayer_options) self.check_location_rule(multiworld, 1, FestivalCheck.egg_hunt, False) - self.check_location_rule(multiworld, 2, FestivalCheck.egg_hunt, True, False) + self.check_location_rule(multiworld, 2, FestivalCheck.egg_hunt, True, True) self.check_location_rule(multiworld, 3, FestivalCheck.egg_hunt, True, True) def test_different_money_settings(self): diff --git a/worlds/stardew_valley/test/TestNumberLocations.py b/worlds/stardew_valley/test/TestNumberLocations.py new file mode 100644 index 000000000000..ef552c10e8d5 --- /dev/null +++ b/worlds/stardew_valley/test/TestNumberLocations.py @@ -0,0 +1,98 @@ +from . import SVTestBase, allsanity_no_mods_6_x_x, \ + allsanity_mods_6_x_x, minimal_locations_maximal_items, minimal_locations_maximal_items_with_island, get_minsanity_options, default_6_x_x +from .. import location_table +from ..items import Group, item_table + + +class TestLocationGeneration(SVTestBase): + + def test_all_location_created_are_in_location_table(self): + for location in self.get_real_locations(): + self.assertIn(location.name, location_table) + + +class TestMinLocationAndMaxItem(SVTestBase): + options = minimal_locations_maximal_items() + + def test_minimal_location_maximal_items_still_valid(self): + valid_locations = self.get_real_locations() + number_locations = len(valid_locations) + number_items = len([item for item in self.multiworld.itempool + if Group.RESOURCE_PACK not in item_table[item.name].groups and Group.TRAP not in item_table[item.name].groups]) + print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND EXCLUDED]") + self.assertGreaterEqual(number_locations, number_items) + + +class TestMinLocationAndMaxItemWithIsland(SVTestBase): + options = minimal_locations_maximal_items_with_island() + + def test_minimal_location_maximal_items_with_island_still_valid(self): + valid_locations = self.get_real_locations() + number_locations = len(valid_locations) + number_items = len([item for item in self.multiworld.itempool + if Group.RESOURCE_PACK not in item_table[item.name].groups and Group.TRAP not in item_table[item.name].groups]) + print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND INCLUDED]") + self.assertGreaterEqual(number_locations, number_items) + + +class TestMinSanityHasAllExpectedLocations(SVTestBase): + options = get_minsanity_options() + + def test_minsanity_has_fewer_than_locations(self): + expected_locations = 85 + real_locations = self.get_real_locations() + number_locations = len(real_locations) + print(f"Stardew Valley - Minsanity Locations: {number_locations}") + self.assertLessEqual(number_locations, expected_locations) + if number_locations != expected_locations: + print(f"\tDisappeared Locations Detected!" + f"\n\tPlease update test_minsanity_has_fewer_than_locations" + f"\n\t\tExpected: {expected_locations}" + f"\n\t\tActual: {number_locations}") + + +class TestDefaultSettingsHasAllExpectedLocations(SVTestBase): + options = default_6_x_x() + + def test_default_settings_has_exactly_locations(self): + expected_locations = 491 + real_locations = self.get_real_locations() + number_locations = len(real_locations) + print(f"Stardew Valley - Default options locations: {number_locations}") + if number_locations != expected_locations: + print(f"\tNew locations detected!" + f"\n\tPlease update test_default_settings_has_exactly_locations" + f"\n\t\tExpected: {expected_locations}" + f"\n\t\tActual: {number_locations}") + + +class TestAllSanitySettingsHasAllExpectedLocations(SVTestBase): + options = allsanity_no_mods_6_x_x() + + def test_allsanity_without_mods_has_at_least_locations(self): + expected_locations = 2238 + real_locations = self.get_real_locations() + number_locations = len(real_locations) + print(f"Stardew Valley - Allsanity Locations without mods: {number_locations}") + self.assertGreaterEqual(number_locations, expected_locations) + if number_locations != expected_locations: + print(f"\tNew locations detected!" + f"\n\tPlease update test_allsanity_without_mods_has_at_least_locations" + f"\n\t\tExpected: {expected_locations}" + f"\n\t\tActual: {number_locations}") + + +class TestAllSanityWithModsSettingsHasAllExpectedLocations(SVTestBase): + options = allsanity_mods_6_x_x() + + def test_allsanity_with_mods_has_at_least_locations(self): + expected_locations = 3096 + real_locations = self.get_real_locations() + number_locations = len(real_locations) + print(f"Stardew Valley - Allsanity Locations with all mods: {number_locations}") + self.assertGreaterEqual(number_locations, expected_locations) + if number_locations != expected_locations: + print(f"\tNew locations detected!" + f"\n\tPlease update test_allsanity_with_mods_has_at_least_locations" + f"\n\t\tExpected: {expected_locations}" + f"\n\t\tActual: {number_locations}") diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index d13f9b8a051a..2824a10c38af 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -1,12 +1,13 @@ import itertools from Options import NamedRange -from . import setup_solo_multiworld, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods +from . import SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, solo_multiworld from .assertion import WorldAssertMixin from .long.option_names import all_option_choices from .. import items_by_group, Group, StardewValleyWorld from ..locations import locations_by_tag, LocationTags, location_table -from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations +from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations, \ + SkillProgression from ..strings.goal_names import Goal as GoalName from ..strings.season_names import Season from ..strings.special_order_names import SpecialOrder @@ -24,7 +25,7 @@ def test_given_special_range_when_generate_then_basic_checks(self): continue for value in option.special_range_names: world_options = {option_name: option.special_range_names[value]} - with self.solo_world_sub_test(f"{option_name}: {value}", world_options, dirty_state=True) as (multiworld, _): + with self.solo_world_sub_test(f"{option_name}: {value}", world_options) as (multiworld, _): self.assert_basic_checks(multiworld) def test_given_choice_when_generate_then_basic_checks(self): @@ -34,7 +35,7 @@ def test_given_choice_when_generate_then_basic_checks(self): continue for value in option.options: world_options = {option_name: option.options[value]} - with self.solo_world_sub_test(f"{option_name}: {value}", world_options, dirty_state=True) as (multiworld, _): + with self.solo_world_sub_test(f"{option_name}: {value}", world_options) as (multiworld, _): self.assert_basic_checks(multiworld) @@ -57,58 +58,71 @@ def test_given_goal_when_generate_then_victory_is_in_correct_location(self): class TestSeasonRandomization(SVTestCase): def test_given_disabled_when_generate_then_all_seasons_are_precollected(self): world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_disabled} - multi_world = setup_solo_multiworld(world_options) - - precollected_items = {item.name for item in multi_world.precollected_items[1]} - self.assertTrue(all([season in precollected_items for season in SEASONS])) + with solo_multiworld(world_options) as (multi_world, _): + precollected_items = {item.name for item in multi_world.precollected_items[1]} + self.assertTrue(all([season in precollected_items for season in SEASONS])) def test_given_randomized_when_generate_then_all_seasons_are_in_the_pool_or_precollected(self): world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_randomized} - multi_world = setup_solo_multiworld(world_options) - precollected_items = {item.name for item in multi_world.precollected_items[1]} - items = {item.name for item in multi_world.get_items()} | precollected_items - self.assertTrue(all([season in items for season in SEASONS])) - self.assertEqual(len(SEASONS.intersection(precollected_items)), 1) + with solo_multiworld(world_options) as (multi_world, _): + precollected_items = {item.name for item in multi_world.precollected_items[1]} + items = {item.name for item in multi_world.get_items()} | precollected_items + self.assertTrue(all([season in items for season in SEASONS])) + self.assertEqual(len(SEASONS.intersection(precollected_items)), 1) def test_given_progressive_when_generate_then_3_progressive_seasons_are_in_the_pool(self): world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_progressive} - multi_world = setup_solo_multiworld(world_options) - - items = [item.name for item in multi_world.get_items()] - self.assertEqual(items.count(Season.progressive), 3) + with solo_multiworld(world_options) as (multi_world, _): + items = [item.name for item in multi_world.get_items()] + self.assertEqual(items.count(Season.progressive), 3) class TestToolProgression(SVTestCase): def test_given_vanilla_when_generate_then_no_tool_in_pool(self): world_options = {ToolProgression.internal_name: ToolProgression.option_vanilla} - multi_world = setup_solo_multiworld(world_options) - - items = {item.name for item in multi_world.get_items()} - for tool in TOOLS: - self.assertNotIn(tool, items) - - def test_given_progressive_when_generate_then_progressive_tool_of_each_is_in_pool_four_times(self): - world_options = {ToolProgression.internal_name: ToolProgression.option_progressive} - multi_world = setup_solo_multiworld(world_options) - - items = [item.name for item in multi_world.get_items()] - for tool in TOOLS: - self.assertEqual(items.count("Progressive " + tool), 4) + with solo_multiworld(world_options) as (multi_world, _): + items = {item.name for item in multi_world.get_items()} + for tool in TOOLS: + self.assertNotIn(tool, items) + + def test_given_progressive_when_generate_then_each_tool_is_in_pool_4_times(self): + world_options = {ToolProgression.internal_name: ToolProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive} + with solo_multiworld(world_options) as (multi_world, _): + items = [item.name for item in multi_world.get_items()] + for tool in TOOLS: + count = items.count("Progressive " + tool) + self.assertEqual(count, 4, f"Progressive {tool} was there {count} times") + scythe_count = items.count("Progressive Scythe") + self.assertEqual(scythe_count, 1, f"Progressive Scythe was there {scythe_count} times") + self.assertEqual(items.count("Golden Scythe"), 0, f"Golden Scythe is deprecated") + + def test_given_progressive_with_masteries_when_generate_then_fishing_rod_is_in_the_pool_5_times(self): + world_options = {ToolProgression.internal_name: ToolProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries} + with solo_multiworld(world_options) as (multi_world, _): + items = [item.name for item in multi_world.get_items()] + for tool in TOOLS: + count = items.count("Progressive " + tool) + expected_count = 5 if tool == "Fishing Rod" else 4 + self.assertEqual(count, expected_count, f"Progressive {tool} was there {count} times") + scythe_count = items.count("Progressive Scythe") + self.assertEqual(scythe_count, 2, f"Progressive Scythe was there {scythe_count} times") + self.assertEqual(items.count("Golden Scythe"), 0, f"Golden Scythe is deprecated") def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): world_options = {ToolProgression.internal_name: ToolProgression.option_progressive} - multi_world = setup_solo_multiworld(world_options) - - locations = {locations.name for locations in multi_world.get_locations(1)} - for material, tool in itertools.product(ToolMaterial.tiers.values(), - [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can]): - if material == ToolMaterial.basic: - continue - self.assertIn(f"{material} {tool} Upgrade", locations) - self.assertIn("Purchase Training Rod", locations) - self.assertIn("Bamboo Pole Cutscene", locations) - self.assertIn("Purchase Fiberglass Rod", locations) - self.assertIn("Purchase Iridium Rod", locations) + with solo_multiworld(world_options) as (multi_world, _): + locations = {locations.name for locations in multi_world.get_locations(1)} + for material, tool in itertools.product(ToolMaterial.tiers.values(), + [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can]): + if material == ToolMaterial.basic: + continue + self.assertIn(f"{material} {tool} Upgrade", locations) + self.assertIn("Purchase Training Rod", locations) + self.assertIn("Bamboo Pole Cutscene", locations) + self.assertIn("Purchase Fiberglass Rod", locations) + self.assertIn("Purchase Iridium Rod", locations) class TestGenerateAllOptionsWithExcludeGingerIsland(WorldAssertMixin, SVTestCase): @@ -123,7 +137,7 @@ def test_given_choice_when_generate_exclude_ginger_island(self): option: option_choice } - with self.solo_world_sub_test(f"{option.internal_name}: {option_choice}", world_options, dirty_state=True) as (multiworld, stardew_world): + with self.solo_world_sub_test(f"{option.internal_name}: {option_choice}", world_options) as (multiworld, stardew_world): # Some options, like goals, will force Ginger island back in the game. We want to skip testing those. if stardew_world.options.exclude_ginger_island != ExcludeGingerIsland.option_true: @@ -140,7 +154,7 @@ def test_given_island_related_goal_then_override_exclude_ginger_island(self): ExcludeGingerIsland: exclude_island } - with self.solo_world_sub_test(f"Goal: {goal}, {ExcludeGingerIsland.internal_name}: {exclude_island}", world_options, dirty_state=True) \ + with self.solo_world_sub_test(f"Goal: {goal}, {ExcludeGingerIsland.internal_name}: {exclude_island}", world_options) \ as (multiworld, stardew_world): self.assertEqual(stardew_world.options.exclude_ginger_island, ExcludeGingerIsland.option_false) self.assert_basic_checks(multiworld) @@ -148,77 +162,77 @@ def test_given_island_related_goal_then_override_exclude_ginger_island(self): class TestTraps(SVTestCase): def test_given_no_traps_when_generate_then_no_trap_in_pool(self): - world_options = allsanity_options_without_mods().copy() + world_options = allsanity_no_mods_6_x_x().copy() world_options[TrapItems.internal_name] = TrapItems.option_no_traps - multi_world = setup_solo_multiworld(world_options) - - trap_items = [item_data.name for item_data in items_by_group[Group.TRAP]] - multiworld_items = [item.name for item in multi_world.get_items()] + with solo_multiworld(world_options) as (multi_world, _): + trap_items = [item_data.name for item_data in items_by_group[Group.TRAP]] + multiworld_items = [item.name for item in multi_world.get_items()] - for item in trap_items: - with self.subTest(f"{item}"): - self.assertNotIn(item, multiworld_items) + for item in trap_items: + with self.subTest(f"{item}"): + self.assertNotIn(item, multiworld_items) def test_given_traps_when_generate_then_all_traps_in_pool(self): trap_option = TrapItems for value in trap_option.options: if value == "no_traps": continue - world_options = allsanity_options_with_mods() + world_options = allsanity_mods_6_x_x() world_options.update({TrapItems.internal_name: trap_option.options[value]}) - multi_world = setup_solo_multiworld(world_options) - trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups and item_data.mod_name is None] - multiworld_items = [item.name for item in multi_world.get_items()] - for item in trap_items: - with self.subTest(f"Option: {value}, Item: {item}"): - self.assertIn(item, multiworld_items) + with solo_multiworld(world_options) as (multi_world, _): + trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if + Group.DEPRECATED not in item_data.groups and item_data.mod_name is None] + multiworld_items = [item.name for item in multi_world.get_items()] + for item in trap_items: + with self.subTest(f"Option: {value}, Item: {item}"): + self.assertIn(item, multiworld_items) class TestSpecialOrders(SVTestCase): def test_given_disabled_then_no_order_in_pool(self): - world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled} - multi_world = setup_solo_multiworld(world_options) - - locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table} - for location_name in locations_in_pool: - location = location_table[location_name] - self.assertNotIn(LocationTags.SPECIAL_ORDER_BOARD, location.tags) - self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags) + world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla} + with solo_multiworld(world_options) as (multi_world, _): + locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table} + for location_name in locations_in_pool: + location = location_table[location_name] + self.assertNotIn(LocationTags.SPECIAL_ORDER_BOARD, location.tags) + self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags) def test_given_board_only_then_no_qi_order_in_pool(self): - world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only} - multi_world = setup_solo_multiworld(world_options) + world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board} + with solo_multiworld(world_options) as (multi_world, _): - locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table} - for location_name in locations_in_pool: - location = location_table[location_name] - self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags) + locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table} + for location_name in locations_in_pool: + location = location_table[location_name] + self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags) - for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: - if board_location.mod_name: - continue - self.assertIn(board_location.name, locations_in_pool) + for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: + if board_location.mod_name: + continue + self.assertIn(board_location.name, locations_in_pool) def test_given_board_and_qi_then_all_orders_in_pool(self): world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories} - multi_world = setup_solo_multiworld(world_options) + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false} + with solo_multiworld(world_options) as (multi_world, _): - locations_in_pool = {location.name for location in multi_world.get_locations()} - for qi_location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI]: - if qi_location.mod_name: - continue - self.assertIn(qi_location.name, locations_in_pool) + locations_in_pool = {location.name for location in multi_world.get_locations()} + for qi_location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI]: + if qi_location.mod_name: + continue + self.assertIn(qi_location.name, locations_in_pool) - for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: - if board_location.mod_name: - continue - self.assertIn(board_location.name, locations_in_pool) + for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: + if board_location.mod_name: + continue + self.assertIn(board_location.name, locations_in_pool) def test_given_board_and_qi_without_arcade_machines_then_lets_play_a_game_not_in_pool(self): world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled} - multi_world = setup_solo_multiworld(world_options) - - locations_in_pool = {location.name for location in multi_world.get_locations()} - self.assertNotIn(SpecialOrder.lets_play_a_game, locations_in_pool) + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false} + with solo_multiworld(world_options) as (multi_world, _): + locations_in_pool = {location.name for location in multi_world.get_locations()} + self.assertNotIn(SpecialOrder.lets_play_a_game, locations_in_pool) diff --git a/worlds/stardew_valley/test/TestOptionsPairs.py b/worlds/stardew_valley/test/TestOptionsPairs.py index 9109c39562ee..d953696e887d 100644 --- a/worlds/stardew_valley/test/TestOptionsPairs.py +++ b/worlds/stardew_valley/test/TestOptionsPairs.py @@ -47,7 +47,7 @@ def test_given_option_pair_then_basic_checks(self): class TestCraftMasterNoSpecialOrder(WorldAssertMixin, SVTestBase): options = { options.Goal.internal_name: Goal.option_craft_master, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.alias_disabled, options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, options.Craftsanity.internal_name: options.Craftsanity.option_none } diff --git a/worlds/stardew_valley/test/TestRegions.py b/worlds/stardew_valley/test/TestRegions.py index 0137bab9148b..a25feea22085 100644 --- a/worlds/stardew_valley/test/TestRegions.py +++ b/worlds/stardew_valley/test/TestRegions.py @@ -4,7 +4,7 @@ from BaseClasses import get_seed from . import SVTestCase, complete_options_with_default -from ..options import EntranceRandomization, ExcludeGingerIsland +from ..options import EntranceRandomization, ExcludeGingerIsland, SkillProgression from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag, create_final_connections_and_regions from ..strings.entrance_names import Entrance as EntranceName from ..strings.region_names import Region as RegionName @@ -56,10 +56,12 @@ class TestEntranceRando(SVTestCase): def test_entrance_randomization(self): for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), (EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION), + (EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS), (EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]: sv_options = complete_options_with_default({ EntranceRandomization.internal_name: option, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, }) seed = get_seed() rand = random.Random(seed) @@ -80,11 +82,13 @@ def test_entrance_randomization(self): def test_entrance_randomization_without_island(self): for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), (EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION), + (EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS), (EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]: sv_options = complete_options_with_default({ EntranceRandomization.internal_name: option, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, }) seed = get_seed() rand = random.Random(seed) @@ -111,7 +115,8 @@ def test_entrance_randomization_without_island(self): def test_cannot_put_island_access_on_island(self): sv_options = complete_options_with_default({ EntranceRandomization.internal_name: EntranceRandomization.option_buildings, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, }) for i in range(0, 100 if self.skip_long_tests else 10000): diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py deleted file mode 100644 index 3ee921bd2bc2..000000000000 --- a/worlds/stardew_valley/test/TestRules.py +++ /dev/null @@ -1,797 +0,0 @@ -from collections import Counter - -from . import SVTestBase -from .. import options, HasProgressionPercent -from ..data.craftable_data import all_crafting_recipes_by_name -from ..locations import locations_by_tag, LocationTags, location_table -from ..options import ToolProgression, BuildingProgression, ExcludeGingerIsland, Chefsanity, Craftsanity, Shipsanity, SeasonRandomization, Friendsanity, \ - FriendsanityHeartSize, BundleRandomization, SkillProgression -from ..strings.entrance_names import Entrance -from ..strings.region_names import Region -from ..strings.tool_names import Tool, ToolMaterial - - -class TestProgressiveToolsLogic(SVTestBase): - options = { - ToolProgression.internal_name: ToolProgression.option_progressive, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - } - - def test_sturgeon(self): - self.multiworld.state.prog_items = {1: Counter()} - - sturgeon_rule = self.world.logic.has("Sturgeon") - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - summer = self.world.create_item("Summer") - self.multiworld.state.collect(summer, event=False) - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - fishing_rod = self.world.create_item("Progressive Fishing Rod") - self.multiworld.state.collect(fishing_rod, event=False) - self.multiworld.state.collect(fishing_rod, event=False) - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - fishing_level = self.world.create_item("Fishing Level") - self.multiworld.state.collect(fishing_level, event=False) - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - self.multiworld.state.collect(fishing_level, event=False) - self.multiworld.state.collect(fishing_level, event=False) - self.multiworld.state.collect(fishing_level, event=False) - self.multiworld.state.collect(fishing_level, event=False) - self.multiworld.state.collect(fishing_level, event=False) - self.assert_rule_true(sturgeon_rule, self.multiworld.state) - - self.remove(summer) - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - winter = self.world.create_item("Winter") - self.multiworld.state.collect(winter, event=False) - self.assert_rule_true(sturgeon_rule, self.multiworld.state) - - self.remove(fishing_rod) - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - def test_old_master_cannoli(self): - self.multiworld.state.prog_items = {1: Counter()} - - self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=False) - self.multiworld.state.collect(self.world.create_item("Summer"), event=False) - self.collect_lots_of_money() - - rule = self.world.logic.region.can_reach_location("Old Master Cannoli") - self.assert_rule_false(rule, self.multiworld.state) - - fall = self.world.create_item("Fall") - self.multiworld.state.collect(fall, event=False) - self.assert_rule_false(rule, self.multiworld.state) - - tuesday = self.world.create_item("Traveling Merchant: Tuesday") - self.multiworld.state.collect(tuesday, event=False) - self.assert_rule_false(rule, self.multiworld.state) - - rare_seed = self.world.create_item("Rare Seed") - self.multiworld.state.collect(rare_seed, event=False) - self.assert_rule_true(rule, self.multiworld.state) - - self.remove(fall) - self.assert_rule_false(rule, self.multiworld.state) - self.remove(tuesday) - - green_house = self.world.create_item("Greenhouse") - self.multiworld.state.collect(green_house, event=False) - self.assert_rule_false(rule, self.multiworld.state) - - friday = self.world.create_item("Traveling Merchant: Friday") - self.multiworld.state.collect(friday, event=False) - self.assertTrue(self.multiworld.get_location("Old Master Cannoli", 1).access_rule(self.multiworld.state)) - - self.remove(green_house) - self.assert_rule_false(rule, self.multiworld.state) - self.remove(friday) - - -class TestBundlesLogic(SVTestBase): - options = { - BundleRandomization.internal_name: BundleRandomization.option_vanilla - } - - def test_vault_2500g_bundle(self): - self.assertFalse(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state)) - - self.collect_lots_of_money() - self.assertTrue(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state)) - - -class TestBuildingLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive - } - - def test_coop_blueprint(self): - self.assertFalse(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state)) - - self.collect_lots_of_money() - self.assertTrue(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state)) - - def test_big_coop_blueprint(self): - big_coop_blueprint_rule = self.world.logic.region.can_reach_location("Big Coop Blueprint") - self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") - - self.collect_lots_of_money() - self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") - - self.multiworld.state.collect(self.world.create_item("Can Construct Buildings"), event=True) - self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") - - self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=False) - self.assertTrue(big_coop_blueprint_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") - - def test_deluxe_coop_blueprint(self): - self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) - - self.collect_lots_of_money() - self.multiworld.state.collect(self.world.create_item("Can Construct Buildings"), event=True) - self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) - - self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=True) - self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) - - self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=True) - self.assertTrue(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) - - def test_big_shed_blueprint(self): - big_shed_rule = self.world.logic.region.can_reach_location("Big Shed Blueprint") - self.assertFalse(big_shed_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") - - self.collect_lots_of_money() - self.assertFalse(big_shed_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") - - self.multiworld.state.collect(self.world.create_item("Can Construct Buildings"), event=True) - self.assertFalse(big_shed_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") - - self.multiworld.state.collect(self.world.create_item("Progressive Shed"), event=True) - self.assertTrue(big_shed_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") - - -class TestArcadeMachinesLogic(SVTestBase): - options = { - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, - } - - def test_prairie_king(self): - self.assertFalse(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - - boots = self.world.create_item("JotPK: Progressive Boots") - gun = self.world.create_item("JotPK: Progressive Gun") - ammo = self.world.create_item("JotPK: Progressive Ammo") - life = self.world.create_item("JotPK: Extra Life") - drop = self.world.create_item("JotPK: Increased Drop Rate") - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - self.remove(boots) - self.remove(gun) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(boots, event=True) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - self.remove(boots) - self.remove(boots) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(life, event=True) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - self.remove(boots) - self.remove(gun) - self.remove(ammo) - self.remove(life) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(life, event=True) - self.multiworld.state.collect(drop, event=True) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - self.remove(boots) - self.remove(gun) - self.remove(gun) - self.remove(ammo) - self.remove(ammo) - self.remove(life) - self.remove(drop) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(life, event=True) - self.multiworld.state.collect(drop, event=True) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - self.remove(boots) - self.remove(boots) - self.remove(gun) - self.remove(gun) - self.remove(gun) - self.remove(gun) - self.remove(ammo) - self.remove(ammo) - self.remove(ammo) - self.remove(life) - self.remove(drop) - - -class TestWeaponsLogic(SVTestBase): - options = { - ToolProgression.internal_name: ToolProgression.option_progressive, - options.SkillProgression.internal_name: options.SkillProgression.option_progressive, - } - - def test_mine(self): - self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) - self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) - self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) - self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) - self.collect([self.world.create_item("Combat Level")] * 10) - self.collect([self.world.create_item("Mining Level")] * 10) - self.collect([self.world.create_item("Progressive Mine Elevator")] * 24) - self.multiworld.state.collect(self.world.create_item("Bus Repair"), event=True) - self.multiworld.state.collect(self.world.create_item("Skull Key"), event=True) - - self.GiveItemAndCheckReachableMine("Progressive Sword", 1) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 1) - self.GiveItemAndCheckReachableMine("Progressive Club", 1) - - self.GiveItemAndCheckReachableMine("Progressive Sword", 2) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 2) - self.GiveItemAndCheckReachableMine("Progressive Club", 2) - - self.GiveItemAndCheckReachableMine("Progressive Sword", 3) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 3) - self.GiveItemAndCheckReachableMine("Progressive Club", 3) - - self.GiveItemAndCheckReachableMine("Progressive Sword", 4) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 4) - self.GiveItemAndCheckReachableMine("Progressive Club", 4) - - self.GiveItemAndCheckReachableMine("Progressive Sword", 5) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 5) - self.GiveItemAndCheckReachableMine("Progressive Club", 5) - - def GiveItemAndCheckReachableMine(self, item_name: str, reachable_level: int): - item = self.multiworld.create_item(item_name, self.player) - self.multiworld.state.collect(item, event=True) - rule = self.world.logic.mine.can_mine_in_the_mines_floor_1_40() - if reachable_level > 0: - self.assert_rule_true(rule, self.multiworld.state) - else: - self.assert_rule_false(rule, self.multiworld.state) - - rule = self.world.logic.mine.can_mine_in_the_mines_floor_41_80() - if reachable_level > 1: - self.assert_rule_true(rule, self.multiworld.state) - else: - self.assert_rule_false(rule, self.multiworld.state) - - rule = self.world.logic.mine.can_mine_in_the_mines_floor_81_120() - if reachable_level > 2: - self.assert_rule_true(rule, self.multiworld.state) - else: - self.assert_rule_false(rule, self.multiworld.state) - - rule = self.world.logic.mine.can_mine_in_the_skull_cavern() - if reachable_level > 3: - self.assert_rule_true(rule, self.multiworld.state) - else: - self.assert_rule_false(rule, self.multiworld.state) - - rule = self.world.logic.ability.can_mine_perfectly_in_the_skull_cavern() - if reachable_level > 4: - self.assert_rule_true(rule, self.multiworld.state) - else: - self.assert_rule_false(rule, self.multiworld.state) - - -class TestRecipeLearnLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.Cooksanity.internal_name: options.Cooksanity.option_all, - Chefsanity.internal_name: Chefsanity.option_none, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_learn_qos_recipe(self): - location = "Cook Radish Salad" - rule = self.world.logic.region.can_reach_location(location) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Progressive House"), event=False) - self.multiworld.state.collect(self.world.create_item("Radish Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Spring"), event=False) - self.multiworld.state.collect(self.world.create_item("Summer"), event=False) - self.collect_lots_of_money() - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("The Queen of Sauce"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestRecipeReceiveLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.Cooksanity.internal_name: options.Cooksanity.option_all, - Chefsanity.internal_name: Chefsanity.option_all, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_learn_qos_recipe(self): - location = "Cook Radish Salad" - rule = self.world.logic.region.can_reach_location(location) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Progressive House"), event=False) - self.multiworld.state.collect(self.world.create_item("Radish Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Summer"), event=False) - self.collect_lots_of_money() - self.assert_rule_false(rule, self.multiworld.state) - - spring = self.world.create_item("Spring") - qos = self.world.create_item("The Queen of Sauce") - self.multiworld.state.collect(spring, event=False) - self.multiworld.state.collect(qos, event=False) - self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.remove(spring) - self.multiworld.state.remove(qos) - - self.multiworld.state.collect(self.world.create_item("Radish Salad Recipe"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - def test_get_chefsanity_check_recipe(self): - location = "Radish Salad Recipe" - rule = self.world.logic.region.can_reach_location(location) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Spring"), event=False) - self.collect_lots_of_money() - self.assert_rule_false(rule, self.multiworld.state) - - seeds = self.world.create_item("Radish Seeds") - summer = self.world.create_item("Summer") - house = self.world.create_item("Progressive House") - self.multiworld.state.collect(seeds, event=False) - self.multiworld.state.collect(summer, event=False) - self.multiworld.state.collect(house, event=False) - self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.remove(seeds) - self.multiworld.state.remove(summer) - self.multiworld.state.remove(house) - - self.multiworld.state.collect(self.world.create_item("The Queen of Sauce"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestCraftsanityLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - Craftsanity.internal_name: Craftsanity.option_all, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_craft_recipe(self): - location = "Craft Marble Brazier" - rule = self.world.logic.region.can_reach_location(location) - self.collect([self.world.create_item("Progressive Pickaxe")] * 4) - self.collect([self.world.create_item("Progressive Fishing Rod")] * 4) - self.collect([self.world.create_item("Progressive Sword")] * 4) - self.collect([self.world.create_item("Progressive Mine Elevator")] * 24) - self.collect([self.world.create_item("Mining Level")] * 10) - self.collect([self.world.create_item("Combat Level")] * 10) - self.collect([self.world.create_item("Fishing Level")] * 10) - self.collect_all_the_money() - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Marble Brazier Recipe"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - def test_can_learn_crafting_recipe(self): - location = "Marble Brazier Recipe" - rule = self.world.logic.region.can_reach_location(location) - self.assert_rule_false(rule, self.multiworld.state) - - self.collect_lots_of_money() - self.assert_rule_true(rule, self.multiworld.state) - - def test_can_craft_festival_recipe(self): - recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.world.create_item("Pumpkin Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Torch Recipe"), event=False) - self.collect_lots_of_money() - rule = self.world.logic.crafting.can_craft(recipe) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Fall"), event=False) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Jack-O-Lantern Recipe"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestCraftsanityWithFestivalsLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, - Craftsanity.internal_name: Craftsanity.option_all, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_craft_festival_recipe(self): - recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.world.create_item("Pumpkin Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Fall"), event=False) - self.collect_lots_of_money() - rule = self.world.logic.crafting.can_craft(recipe) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Jack-O-Lantern Recipe"), event=False) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Torch Recipe"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestNoCraftsanityLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - SeasonRandomization.internal_name: SeasonRandomization.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, - Craftsanity.internal_name: Craftsanity.option_none, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_craft_recipe(self): - recipe = all_crafting_recipes_by_name["Wood Floor"] - rule = self.world.logic.crafting.can_craft(recipe) - self.assert_rule_true(rule, self.multiworld.state) - - def test_can_craft_festival_recipe(self): - recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.world.create_item("Pumpkin Seeds"), event=False) - self.collect_lots_of_money() - rule = self.world.logic.crafting.can_craft(recipe) - result = rule(self.multiworld.state) - self.assertFalse(result) - - self.collect([self.world.create_item("Progressive Season")] * 2) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestNoCraftsanityWithFestivalsLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, - Craftsanity.internal_name: Craftsanity.option_none, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_craft_festival_recipe(self): - recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.world.create_item("Pumpkin Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Fall"), event=False) - self.collect_lots_of_money() - rule = self.world.logic.crafting.can_craft(recipe) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Jack-O-Lantern Recipe"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestDonationLogicAll(SVTestBase): - options = { - options.Museumsanity.internal_name: options.Museumsanity.option_all - } - - def test_cannot_make_any_donation_without_museum_access(self): - railroad_item = "Railroad Boulder Removed" - swap_museum_and_bathhouse(self.multiworld, self.player) - collect_all_except(self.multiworld, railroad_item) - - for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: - self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - - for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: - self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - -class TestDonationLogicRandomized(SVTestBase): - options = { - options.Museumsanity.internal_name: options.Museumsanity.option_randomized - } - - def test_cannot_make_any_donation_without_museum_access(self): - railroad_item = "Railroad Boulder Removed" - swap_museum_and_bathhouse(self.multiworld, self.player) - collect_all_except(self.multiworld, railroad_item) - donation_locations = [location for location in self.get_real_locations() if - LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags] - - for donation in donation_locations: - self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - - for donation in donation_locations: - self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - -class TestDonationLogicMilestones(SVTestBase): - options = { - options.Museumsanity.internal_name: options.Museumsanity.option_milestones - } - - def test_cannot_make_any_donation_without_museum_access(self): - railroad_item = "Railroad Boulder Removed" - swap_museum_and_bathhouse(self.multiworld, self.player) - collect_all_except(self.multiworld, railroad_item) - - for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: - self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - - for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: - self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - -def swap_museum_and_bathhouse(multiworld, player): - museum_region = multiworld.get_region(Region.museum, player) - bathhouse_region = multiworld.get_region(Region.bathhouse_entrance, player) - museum_entrance = multiworld.get_entrance(Entrance.town_to_museum, player) - bathhouse_entrance = multiworld.get_entrance(Entrance.enter_bathhouse_entrance, player) - museum_entrance.connect(bathhouse_region) - bathhouse_entrance.connect(museum_region) - - -class TestToolVanillaRequiresBlacksmith(SVTestBase): - options = { - options.EntranceRandomization: options.EntranceRandomization.option_buildings, - options.ToolProgression: options.ToolProgression.option_vanilla, - } - seed = 4111845104987680262 - - # Seed is hardcoded to make sure the ER is a valid roll that actually lock the blacksmith behind the Railroad Boulder Removed. - - def test_cannot_get_any_tool_without_blacksmith_access(self): - railroad_item = "Railroad Boulder Removed" - place_region_at_entrance(self.multiworld, self.player, Region.blacksmith, Entrance.enter_bathhouse_entrance) - collect_all_except(self.multiworld, railroad_item) - - for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: - for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: - self.assert_rule_false(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - - for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: - for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: - self.assert_rule_true(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) - - def test_cannot_get_fishing_rod_without_willy_access(self): - railroad_item = "Railroad Boulder Removed" - place_region_at_entrance(self.multiworld, self.player, Region.fish_shop, Entrance.enter_bathhouse_entrance) - collect_all_except(self.multiworld, railroad_item) - - for fishing_rod_level in [3, 4]: - self.assert_rule_false(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - - for fishing_rod_level in [3, 4]: - self.assert_rule_true(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) - - -def place_region_at_entrance(multiworld, player, region, entrance): - region_to_place = multiworld.get_region(region, player) - entrance_to_place_region = multiworld.get_entrance(entrance, player) - - entrance_to_switch = region_to_place.entrances[0] - region_to_switch = entrance_to_place_region.connected_region - entrance_to_switch.connect(region_to_switch) - entrance_to_place_region.connect(region_to_place) - - -def collect_all_except(multiworld, item_to_not_collect: str): - for item in multiworld.get_items(): - if item.name != item_to_not_collect: - multiworld.state.collect(item) - - -class TestFriendsanityDatingRules(SVTestBase): - options = { - SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, - FriendsanityHeartSize.internal_name: 3 - } - - def test_earning_dating_heart_requires_dating(self): - self.collect_all_the_money() - self.multiworld.state.collect(self.world.create_item("Fall"), event=False) - self.multiworld.state.collect(self.world.create_item("Beach Bridge"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive House"), event=False) - for i in range(3): - self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Weapon"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Barn"), event=False) - for i in range(10): - self.multiworld.state.collect(self.world.create_item("Foraging Level"), event=False) - self.multiworld.state.collect(self.world.create_item("Farming Level"), event=False) - self.multiworld.state.collect(self.world.create_item("Mining Level"), event=False) - self.multiworld.state.collect(self.world.create_item("Combat Level"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Mine Elevator"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Mine Elevator"), event=False) - - npc = "Abigail" - heart_name = f"{npc} <3" - step = 3 - - self.assert_can_reach_heart_up_to(npc, 3, step) - self.multiworld.state.collect(self.world.create_item(heart_name), event=False) - self.assert_can_reach_heart_up_to(npc, 6, step) - self.multiworld.state.collect(self.world.create_item(heart_name), event=False) - self.assert_can_reach_heart_up_to(npc, 8, step) - self.multiworld.state.collect(self.world.create_item(heart_name), event=False) - self.assert_can_reach_heart_up_to(npc, 10, step) - self.multiworld.state.collect(self.world.create_item(heart_name), event=False) - self.assert_can_reach_heart_up_to(npc, 14, step) - - def assert_can_reach_heart_up_to(self, npc: str, max_reachable: int, step: int): - prefix = "Friendsanity: " - suffix = " <3" - for i in range(1, max_reachable + 1): - if i % step != 0 and i != 14: - continue - location = f"{prefix}{npc} {i}{suffix}" - can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state) - self.assertTrue(can_reach, f"Should be able to earn relationship up to {i} hearts") - for i in range(max_reachable + 1, 14 + 1): - if i % step != 0 and i != 14: - continue - location = f"{prefix}{npc} {i}{suffix}" - can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state) - self.assertFalse(can_reach, f"Should not be able to earn relationship up to {i} hearts") - - -class TestShipsanityNone(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_none - } - - def test_no_shipsanity_locations(self): - for location in self.get_real_locations(): - self.assertFalse("Shipsanity" in location.name) - self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags) - - -class TestShipsanityCrops(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_crops - } - - def test_only_crop_shipsanity_locations(self): - for location in self.get_real_locations(): - if LocationTags.SHIPSANITY in location_table[location.name].tags: - self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags) - - -class TestShipsanityFish(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_fish - } - - def test_only_fish_shipsanity_locations(self): - for location in self.get_real_locations(): - if LocationTags.SHIPSANITY in location_table[location.name].tags: - self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) - - -class TestShipsanityFullShipment(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_full_shipment - } - - def test_only_full_shipment_shipsanity_locations(self): - for location in self.get_real_locations(): - if LocationTags.SHIPSANITY in location_table[location.name].tags: - self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags) - self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) - - -class TestShipsanityFullShipmentWithFish(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish - } - - def test_only_full_shipment_and_fish_shipsanity_locations(self): - for location in self.get_real_locations(): - if LocationTags.SHIPSANITY in location_table[location.name].tags: - self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or - LocationTags.SHIPSANITY_FISH in location_table[location.name].tags) - - -class TestShipsanityEverything(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_everything, - BuildingProgression.internal_name: BuildingProgression.option_progressive - } - - def test_all_shipsanity_locations_require_shipping_bin(self): - bin_name = "Shipping Bin" - collect_all_except(self.multiworld, bin_name) - shipsanity_locations = [location for location in self.get_real_locations() if - LocationTags.SHIPSANITY in location_table[location.name].tags] - bin_item = self.world.create_item(bin_name) - for location in shipsanity_locations: - with self.subTest(location.name): - self.remove(bin_item) - self.assertFalse(self.world.logic.region.can_reach_location(location.name)(self.multiworld.state)) - self.multiworld.state.collect(bin_item, event=False) - shipsanity_rule = self.world.logic.region.can_reach_location(location.name) - self.assert_rule_true(shipsanity_rule, self.multiworld.state) - self.remove(bin_item) - - -class TestVanillaSkillLogicSimplification(SVTestBase): - options = { - SkillProgression.internal_name: SkillProgression.option_vanilla, - ToolProgression.internal_name: ToolProgression.option_progressive, - } - - def test_skill_logic_has_level_only_uses_one_has_progression_percent(self): - rule = self.multiworld.worlds[1].logic.skill.has_level("Farming", 8) - self.assertEqual(1, sum(1 for i in rule.current_rules if type(i) == HasProgressionPercent)) diff --git a/worlds/stardew_valley/test/TestStardewRule.py b/worlds/stardew_valley/test/TestStardewRule.py index 89317d90e4e2..93b32b0d8ab4 100644 --- a/worlds/stardew_valley/test/TestStardewRule.py +++ b/worlds/stardew_valley/test/TestStardewRule.py @@ -1,7 +1,9 @@ import unittest +from typing import cast from unittest.mock import MagicMock, Mock -from ..stardew_rule import Received, And, Or, HasProgressionPercent, false_, true_ +from .. import StardewRule +from ..stardew_rule import Received, And, Or, HasProgressionPercent, false_, true_, Count class TestSimplification(unittest.TestCase): @@ -72,7 +74,7 @@ def test_propagate_evaluate_while_simplifying(self): collection_state = MagicMock() other_rule = MagicMock() other_rule.evaluate_while_simplifying = Mock(return_value=(other_rule, expected_result)) - rule = And(Or(other_rule)) + rule = And(Or(cast(StardewRule, other_rule))) _, actual_result = rule.evaluate_while_simplifying(collection_state) @@ -101,8 +103,9 @@ def test_short_circuit_when_complement_found(self): def test_short_circuit_when_combinable_rules_is_false(self): collection_state = MagicMock() + collection_state.has = Mock(return_value=False) other_rule = MagicMock() - rule = And(HasProgressionPercent(1, 10), other_rule) + rule = And(Received("Potato", 1, 10), cast(StardewRule, other_rule)) rule.evaluate_while_simplifying(collection_state) @@ -110,16 +113,16 @@ def test_short_circuit_when_combinable_rules_is_false(self): def test_identity_is_removed_from_other_rules(self): collection_state = MagicMock() - rule = Or(false_, HasProgressionPercent(1, 10)) + rule = Or(false_, Received("Potato", 1, 10)) rule.evaluate_while_simplifying(collection_state) self.assertEqual(1, len(rule.current_rules)) - self.assertIn(HasProgressionPercent(1, 10), rule.current_rules) + self.assertIn(Received("Potato", 1, 10), rule.current_rules) def test_complement_replaces_combinable_rules(self): collection_state = MagicMock() - rule = Or(HasProgressionPercent(1, 10), true_) + rule = Or(Received("Potato", 1, 10), true_) rule.evaluate_while_simplifying(collection_state) @@ -129,7 +132,7 @@ def test_simplifying_to_complement_propagates_complement(self): expected_simplified = true_ expected_result = True collection_state = MagicMock() - rule = Or(Or(expected_simplified), HasProgressionPercent(1, 10)) + rule = Or(Or(expected_simplified), Received("Potato", 1, 10)) actual_simplified, actual_result = rule.evaluate_while_simplifying(collection_state) @@ -141,7 +144,7 @@ def test_already_simplified_rules_are_not_simplified_again(self): collection_state = MagicMock() other_rule = MagicMock() other_rule.evaluate_while_simplifying = Mock(return_value=(other_rule, False)) - rule = Or(other_rule, HasProgressionPercent(1, 10)) + rule = Or(cast(StardewRule, other_rule), Received("Potato", 1, 10)) rule.evaluate_while_simplifying(collection_state) other_rule.assert_not_called() @@ -157,7 +160,7 @@ def test_continue_simplification_after_short_circuited(self): a_rule.evaluate_while_simplifying = Mock(return_value=(a_rule, False)) another_rule = MagicMock() another_rule.evaluate_while_simplifying = Mock(return_value=(another_rule, False)) - rule = And(a_rule, another_rule) + rule = And(cast(StardewRule, a_rule), cast(StardewRule, another_rule)) rule.evaluate_while_simplifying(collection_state) # This test is completely messed up because sets are used internally and order of the rules cannot be ensured. @@ -183,7 +186,7 @@ class TestEvaluateWhileSimplifyingDoubleCalls(unittest.TestCase): def test_nested_call_in_the_internal_rule_being_evaluated_does_check_the_internal_rule(self): collection_state = MagicMock() internal_rule = MagicMock() - rule = Or(internal_rule) + rule = Or(cast(StardewRule, internal_rule)) called_once = False internal_call_result = None @@ -212,7 +215,7 @@ def test_nested_call_to_already_simplified_rule_does_not_steal_rule_to_simplify_ an_internal_rule.evaluate_while_simplifying = Mock(return_value=(an_internal_rule, True)) another_internal_rule = MagicMock() another_internal_rule.evaluate_while_simplifying = Mock(return_value=(another_internal_rule, True)) - rule = Or(an_internal_rule, another_internal_rule) + rule = Or(cast(StardewRule, an_internal_rule), cast(StardewRule, another_internal_rule)) rule.evaluate_while_simplifying(collection_state) # This test is completely messed up because sets are used internally and order of the rules cannot be ensured. @@ -242,3 +245,61 @@ def call_to_already_simplified(state): self.assertTrue(called_once) self.assertTrue(internal_call_result) self.assertTrue(actual_result) + + +class TestCount(unittest.TestCase): + + def test_duplicate_rule_count_double(self): + expected_result = True + collection_state = MagicMock() + simplified_rule = Mock() + other_rule = Mock(spec=StardewRule) + other_rule.evaluate_while_simplifying = Mock(return_value=(simplified_rule, expected_result)) + rule = Count([cast(StardewRule, other_rule), other_rule, other_rule], 2) + + actual_result = rule(collection_state) + + other_rule.evaluate_while_simplifying.assert_called_once_with(collection_state) + self.assertEqual(expected_result, actual_result) + + def test_simplified_rule_is_reused(self): + expected_result = False + collection_state = MagicMock() + simplified_rule = Mock(return_value=expected_result) + other_rule = Mock(spec=StardewRule) + other_rule.evaluate_while_simplifying = Mock(return_value=(simplified_rule, expected_result)) + rule = Count([cast(StardewRule, other_rule), cast(StardewRule, other_rule), cast(StardewRule, other_rule)], 2) + + actual_result = rule(collection_state) + + other_rule.evaluate_while_simplifying.assert_called_once_with(collection_state) + self.assertEqual(expected_result, actual_result) + + other_rule.evaluate_while_simplifying.reset_mock() + + actual_result = rule(collection_state) + + other_rule.evaluate_while_simplifying.assert_not_called() + simplified_rule.assert_called() + self.assertEqual(expected_result, actual_result) + + def test_break_if_not_enough_rule_to_complete(self): + expected_result = False + collection_state = MagicMock() + simplified_rule = Mock() + never_called_rule = Mock() + other_rule = Mock(spec=StardewRule) + other_rule.evaluate_while_simplifying = Mock(return_value=(simplified_rule, expected_result)) + rule = Count([cast(StardewRule, other_rule)] * 4, 2) + + actual_result = rule(collection_state) + + other_rule.evaluate_while_simplifying.assert_called_once_with(collection_state) + never_called_rule.assert_not_called() + never_called_rule.evaluate_while_simplifying.assert_not_called() + self.assertEqual(expected_result, actual_result) + + def test_evaluate_without_shortcircuit_when_rules_are_all_different(self): + rule = Count([cast(StardewRule, Mock()) for i in range(5)], 2) + + self.assertEqual(rule.evaluate, rule.evaluate_without_shortcircuit) diff --git a/worlds/stardew_valley/test/TestStartInventory.py b/worlds/stardew_valley/test/TestStartInventory.py index 826f49b1ac83..dc44a1bb4598 100644 --- a/worlds/stardew_valley/test/TestStartInventory.py +++ b/worlds/stardew_valley/test/TestStartInventory.py @@ -17,7 +17,7 @@ class TestStartInventoryAllsanity(WorldAssertMixin, SVTestBase): options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_very_cheap, options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_only, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board, options.QuestLocations.internal_name: -1, options.Fishsanity.internal_name: options.Fishsanity.option_only_easy_fish, options.Museumsanity.internal_name: options.Museumsanity.option_randomized, @@ -29,13 +29,13 @@ class TestStartInventoryAllsanity(WorldAssertMixin, SVTestBase): options.Friendsanity.internal_name: options.Friendsanity.option_bachelors, options.FriendsanityHeartSize.internal_name: 3, options.NumberOfMovementBuffs.internal_name: 10, - options.NumberOfLuckBuffs.internal_name: 12, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, options.Mods.internal_name: ["Tractor Mod", "Bigger Backpack", "Luck Skill", "Magic", "Socializing Skill", "Archaeology", "Cooking Skill", "Binning Skill"], - "start_inventory": {"Movement Speed Bonus": 2} + "start_inventory": {"Progressive Pickaxe": 2} } - def test_start_inventory_movement_speed(self): + def test_start_inventory_progression_items_does_not_break_progression_percent(self): self.assert_basic_checks_with_subtests(self.multiworld) self.assert_can_win(self.multiworld) diff --git a/worlds/stardew_valley/test/TestWalnutsanity.py b/worlds/stardew_valley/test/TestWalnutsanity.py new file mode 100644 index 000000000000..e1ab348def41 --- /dev/null +++ b/worlds/stardew_valley/test/TestWalnutsanity.py @@ -0,0 +1,209 @@ +from . import SVTestBase +from ..options import ExcludeGingerIsland, Walnutsanity +from ..strings.ap_names.ap_option_names import OptionName + + +class TestWalnutsanityNone(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: Walnutsanity.preset_none, + } + + def test_no_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Open Golden Coconut", location_names) + self.assertNotIn("Fishing Walnut 4", location_names) + self.assertNotIn("Journal Scrap #6", location_names) + self.assertNotIn("Starfish Triangle", location_names) + self.assertNotIn("Bush Behind Coconut Tree", location_names) + self.assertNotIn("Purple Starfish Island Survey", location_names) + self.assertNotIn("Volcano Monsters Walnut 3", location_names) + self.assertNotIn("Cliff Over Island South Bush", location_names) + + def test_logic_received_walnuts(self): + # You need to receive 0, and collect 40 + self.collect("Island Obelisk") + self.collect("Island West Turtle") + self.collect("Progressive House") + items = self.collect("5 Golden Walnuts", 10) + + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.collect("Island North Turtle") + self.collect("Island Resort") + self.collect("Open Professor Snail Cave") + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.collect("Dig Site Bridge") + self.collect("Island Farmhouse") + self.collect("Qi Walnut Room") + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.collect("Combat Level", 10) + self.collect("Mining Level", 10) + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.collect("Progressive Slingshot") + self.collect("Progressive Weapon", 5) + self.collect("Progressive Pickaxe", 4) + self.collect("Progressive Watering Can", 4) + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + + +class TestWalnutsanityPuzzles(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: frozenset({OptionName.walnutsanity_puzzles}), + } + + def test_only_puzzle_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertIn("Open Golden Coconut", location_names) + self.assertNotIn("Fishing Walnut 4", location_names) + self.assertNotIn("Journal Scrap #6", location_names) + self.assertNotIn("Starfish Triangle", location_names) + self.assertNotIn("Bush Behind Coconut Tree", location_names) + self.assertIn("Purple Starfish Island Survey", location_names) + self.assertNotIn("Volcano Monsters Walnut 3", location_names) + self.assertNotIn("Cliff Over Island South Bush", location_names) + + def test_field_office_locations_require_professor_snail(self): + location_names = ["Complete Large Animal Collection", "Complete Snake Collection", "Complete Mummified Frog Collection", + "Complete Mummified Bat Collection", "Purple Flowers Island Survey", "Purple Starfish Island Survey", ] + locations = [location for location in self.multiworld.get_locations() if location.name in location_names] + self.collect("Island Obelisk") + self.collect("Island North Turtle") + self.collect("Island West Turtle") + self.collect("Island Resort") + self.collect("Dig Site Bridge") + self.collect("Progressive House") + self.collect("Progressive Pan") + self.collect("Progressive Fishing Rod") + self.collect("Progressive Watering Can") + self.collect("Progressive Pickaxe", 4) + self.collect("Progressive Sword", 5) + self.collect("Combat Level", 10) + self.collect("Mining Level", 10) + for location in locations: + self.assert_reach_location_false(location, self.multiworld.state) + self.collect("Open Professor Snail Cave") + for location in locations: + self.assert_reach_location_true(location, self.multiworld.state) + + +class TestWalnutsanityBushes(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: frozenset({OptionName.walnutsanity_bushes}), + } + + def test_only_bush_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Open Golden Coconut", location_names) + self.assertNotIn("Fishing Walnut 4", location_names) + self.assertNotIn("Journal Scrap #6", location_names) + self.assertNotIn("Starfish Triangle", location_names) + self.assertIn("Bush Behind Coconut Tree", location_names) + self.assertNotIn("Purple Starfish Island Survey", location_names) + self.assertNotIn("Volcano Monsters Walnut 3", location_names) + self.assertIn("Cliff Over Island South Bush", location_names) + + +class TestWalnutsanityPuzzlesAndBushes(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: frozenset({OptionName.walnutsanity_puzzles, OptionName.walnutsanity_bushes}), + } + + def test_only_bush_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertIn("Open Golden Coconut", location_names) + self.assertNotIn("Fishing Walnut 4", location_names) + self.assertNotIn("Journal Scrap #6", location_names) + self.assertNotIn("Starfish Triangle", location_names) + self.assertIn("Bush Behind Coconut Tree", location_names) + self.assertIn("Purple Starfish Island Survey", location_names) + self.assertNotIn("Volcano Monsters Walnut 3", location_names) + self.assertIn("Cliff Over Island South Bush", location_names) + + def test_logic_received_walnuts(self): + # You need to receive 25, and collect 15 + self.collect("Island Obelisk") + self.collect("Island West Turtle") + items = self.collect("5 Golden Walnuts", 5) + + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + items = self.collect("Island North Turtle") + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + + +class TestWalnutsanityDigSpots(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: frozenset({OptionName.walnutsanity_dig_spots}), + } + + def test_only_dig_spots_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Open Golden Coconut", location_names) + self.assertNotIn("Fishing Walnut 4", location_names) + self.assertIn("Journal Scrap #6", location_names) + self.assertIn("Starfish Triangle", location_names) + self.assertNotIn("Bush Behind Coconut Tree", location_names) + self.assertNotIn("Purple Starfish Island Survey", location_names) + self.assertNotIn("Volcano Monsters Walnut 3", location_names) + self.assertNotIn("Cliff Over Island South Bush", location_names) + + +class TestWalnutsanityRepeatables(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: frozenset({OptionName.walnutsanity_repeatables}), + } + + def test_only_repeatable_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Open Golden Coconut", location_names) + self.assertIn("Fishing Walnut 4", location_names) + self.assertNotIn("Journal Scrap #6", location_names) + self.assertNotIn("Starfish Triangle", location_names) + self.assertNotIn("Bush Behind Coconut Tree", location_names) + self.assertNotIn("Purple Starfish Island Survey", location_names) + self.assertIn("Volcano Monsters Walnut 3", location_names) + self.assertNotIn("Cliff Over Island South Bush", location_names) + + +class TestWalnutsanityAll(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: Walnutsanity.preset_all, + } + + def test_all_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertIn("Open Golden Coconut", location_names) + self.assertIn("Fishing Walnut 4", location_names) + self.assertIn("Journal Scrap #6", location_names) + self.assertIn("Starfish Triangle", location_names) + self.assertIn("Bush Behind Coconut Tree", location_names) + self.assertIn("Purple Starfish Island Survey", location_names) + self.assertIn("Volcano Monsters Walnut 3", location_names) + self.assertIn("Cliff Over Island South Bush", location_names) + + def test_logic_received_walnuts(self): + # You need to receive 40, and collect 4 + self.collect("Island Obelisk") + self.collect("Island West Turtle") + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + items = self.collect("5 Golden Walnuts", 8) + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.remove(items) + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + items = self.collect("3 Golden Walnuts", 14) + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.remove(items) + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + items = self.collect("Golden Walnut", 40) + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.remove(items) + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + items = self.collect("5 Golden Walnuts", 4) + items = self.collect("3 Golden Walnuts", 6) + items = self.collect("Golden Walnut", 2) + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 1a463d9fc280..bee02f3c3d68 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -1,201 +1,183 @@ +import logging import os +import threading import unittest from argparse import Namespace from contextlib import contextmanager -from typing import Dict, ClassVar, Iterable, Hashable, Tuple, Optional, List, Union, Any +from typing import Dict, ClassVar, Iterable, Tuple, Optional, List, Union, Any -from BaseClasses import MultiWorld, CollectionState, get_seed, Location +from BaseClasses import MultiWorld, CollectionState, get_seed, Location, Item, ItemClassification from Options import VerifyKeys -from Utils import cache_argsless from test.bases import WorldTestBase from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld from worlds.AutoWorld import call_all from .assertion import RuleAssertMixin -from .. import StardewValleyWorld, options -from ..mods.mod_data import all_mods +from .. import StardewValleyWorld, options, StardewItem from ..options import StardewValleyOptions, StardewValleyOption +logger = logging.getLogger(__name__) + DEFAULT_TEST_SEED = get_seed() +logger.info(f"Default Test Seed: {DEFAULT_TEST_SEED}") -# TODO is this caching really changing anything? -@cache_argsless -def disable_5_x_x_options(): +def default_6_x_x(): return { - options.Monstersanity.internal_name: options.Monstersanity.option_none, - options.Shipsanity.internal_name: options.Shipsanity.option_none, - options.Cooksanity.internal_name: options.Cooksanity.option_none, - options.Chefsanity.internal_name: options.Chefsanity.option_none, - options.Craftsanity.internal_name: options.Craftsanity.option_none + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.default, + options.BackpackProgression.internal_name: options.BackpackProgression.default, + options.Booksanity.internal_name: options.Booksanity.default, + options.BuildingProgression.internal_name: options.BuildingProgression.default, + options.BundlePrice.internal_name: options.BundlePrice.default, + options.BundleRandomization.internal_name: options.BundleRandomization.default, + options.Chefsanity.internal_name: options.Chefsanity.default, + options.Cooksanity.internal_name: options.Cooksanity.default, + options.Craftsanity.internal_name: options.Craftsanity.default, + options.Cropsanity.internal_name: options.Cropsanity.default, + options.ElevatorProgression.internal_name: options.ElevatorProgression.default, + options.EntranceRandomization.internal_name: options.EntranceRandomization.default, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.default, + options.FestivalLocations.internal_name: options.FestivalLocations.default, + options.Fishsanity.internal_name: options.Fishsanity.default, + options.Friendsanity.internal_name: options.Friendsanity.default, + options.FriendsanityHeartSize.internal_name: options.FriendsanityHeartSize.default, + options.Goal.internal_name: options.Goal.default, + options.Mods.internal_name: options.Mods.default, + options.Monstersanity.internal_name: options.Monstersanity.default, + options.Museumsanity.internal_name: options.Museumsanity.default, + options.NumberOfMovementBuffs.internal_name: options.NumberOfMovementBuffs.default, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.default, + options.QuestLocations.internal_name: options.QuestLocations.default, + options.SeasonRandomization.internal_name: options.SeasonRandomization.default, + options.Shipsanity.internal_name: options.Shipsanity.default, + options.SkillProgression.internal_name: options.SkillProgression.default, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.default, + options.ToolProgression.internal_name: options.ToolProgression.default, + options.TrapItems.internal_name: options.TrapItems.default, + options.Walnutsanity.internal_name: options.Walnutsanity.default } -@cache_argsless -def default_4_x_x_options(): - option_dict = default_options().copy() - option_dict.update(disable_5_x_x_options()) - option_dict.update({ - options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled, - }) - return option_dict +def allsanity_no_mods_6_x_x(): + return { + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, + options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, + options.Booksanity.internal_name: options.Booksanity.option_all, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, + options.BundlePrice.internal_name: options.BundlePrice.option_expensive, + options.BundleRandomization.internal_name: options.BundleRandomization.option_thematic, + options.Chefsanity.internal_name: options.Chefsanity.option_all, + options.Cooksanity.internal_name: options.Cooksanity.option_all, + options.Craftsanity.internal_name: options.Craftsanity.option_all, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, + options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, + options.Fishsanity.internal_name: options.Fishsanity.option_all, + options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, + options.FriendsanityHeartSize.internal_name: 1, + options.Goal.internal_name: options.Goal.option_perfection, + options.Mods.internal_name: frozenset(), + options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals, + options.Museumsanity.internal_name: options.Museumsanity.option_all, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, + options.NumberOfMovementBuffs.internal_name: 12, + options.QuestLocations.internal_name: 56, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + options.Shipsanity.internal_name: options.Shipsanity.option_everything, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + options.TrapItems.internal_name: options.TrapItems.option_nightmare, + options.Walnutsanity.internal_name: options.Walnutsanity.preset_all + } -@cache_argsless -def default_options(): - return {} +def allsanity_mods_6_x_x(): + allsanity = allsanity_no_mods_6_x_x() + allsanity.update({options.Mods.internal_name: frozenset(options.Mods.valid_keys)}) + return allsanity -@cache_argsless def get_minsanity_options(): return { - options.Goal.internal_name: options.Goal.option_bottom_of_the_mines, - options.BundleRandomization.internal_name: options.BundleRandomization.option_vanilla, - options.BundlePrice.internal_name: options.BundlePrice.option_very_cheap, - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_disabled, - options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, - options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, - options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, + options.Booksanity.internal_name: options.Booksanity.option_none, options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, - options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, - options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled, - options.QuestLocations.internal_name: -1, - options.Fishsanity.internal_name: options.Fishsanity.option_none, - options.Museumsanity.internal_name: options.Museumsanity.option_none, - options.Monstersanity.internal_name: options.Monstersanity.option_none, - options.Shipsanity.internal_name: options.Shipsanity.option_none, - options.Cooksanity.internal_name: options.Cooksanity.option_none, + options.BundlePrice.internal_name: options.BundlePrice.option_very_cheap, + options.BundleRandomization.internal_name: options.BundleRandomization.option_vanilla, options.Chefsanity.internal_name: options.Chefsanity.option_none, + options.Cooksanity.internal_name: options.Cooksanity.option_none, options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, + options.Fishsanity.internal_name: options.Fishsanity.option_none, options.Friendsanity.internal_name: options.Friendsanity.option_none, options.FriendsanityHeartSize.internal_name: 8, + options.Goal.internal_name: options.Goal.option_bottom_of_the_mines, + options.Mods.internal_name: frozenset(), + options.Monstersanity.internal_name: options.Monstersanity.option_none, + options.Museumsanity.internal_name: options.Museumsanity.option_none, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_none, options.NumberOfMovementBuffs.internal_name: 0, - options.NumberOfLuckBuffs.internal_name: 0, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.QuestLocations.internal_name: -1, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_disabled, + options.Shipsanity.internal_name: options.Shipsanity.option_none, + options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla, + options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, options.TrapItems.internal_name: options.TrapItems.option_no_traps, - options.Mods.internal_name: frozenset(), + options.Walnutsanity.internal_name: options.Walnutsanity.preset_none } -@cache_argsless def minimal_locations_maximal_items(): min_max_options = { - options.Goal.internal_name: options.Goal.option_craft_master, - options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled, - options.BundlePrice.internal_name: options.BundlePrice.option_expensive, - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, - options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, - options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, - options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, + options.Booksanity.internal_name: options.Booksanity.option_none, options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, - options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, - options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled, - options.QuestLocations.internal_name: -1, - options.Fishsanity.internal_name: options.Fishsanity.option_none, - options.Museumsanity.internal_name: options.Museumsanity.option_none, - options.Monstersanity.internal_name: options.Monstersanity.option_none, - options.Shipsanity.internal_name: options.Shipsanity.option_none, - options.Cooksanity.internal_name: options.Cooksanity.option_none, + options.BundlePrice.internal_name: options.BundlePrice.option_expensive, + options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled, options.Chefsanity.internal_name: options.Chefsanity.option_none, + options.Cooksanity.internal_name: options.Cooksanity.option_none, options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, + options.Fishsanity.internal_name: options.Fishsanity.option_none, options.Friendsanity.internal_name: options.Friendsanity.option_none, options.FriendsanityHeartSize.internal_name: 8, + options.Goal.internal_name: options.Goal.option_craft_master, + options.Mods.internal_name: frozenset(), + options.Monstersanity.internal_name: options.Monstersanity.option_none, + options.Museumsanity.internal_name: options.Museumsanity.option_none, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, options.NumberOfMovementBuffs.internal_name: 12, - options.NumberOfLuckBuffs.internal_name: 12, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.QuestLocations.internal_name: -1, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + options.Shipsanity.internal_name: options.Shipsanity.option_none, + options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla, + options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, options.TrapItems.internal_name: options.TrapItems.option_nightmare, - options.Mods.internal_name: (), + options.Walnutsanity.internal_name: options.Walnutsanity.preset_none } return min_max_options -@cache_argsless def minimal_locations_maximal_items_with_island(): - min_max_options = minimal_locations_maximal_items().copy() + min_max_options = minimal_locations_maximal_items() min_max_options.update({options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false}) return min_max_options -@cache_argsless -def allsanity_4_x_x_options_without_mods(): - option_dict = { - options.Goal.internal_name: options.Goal.option_perfection, - options.BundleRandomization.internal_name: options.BundleRandomization.option_thematic, - options.BundlePrice.internal_name: options.BundlePrice.option_expensive, - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, - options.ToolProgression.internal_name: options.ToolProgression.option_progressive, - options.SkillProgression.internal_name: options.SkillProgression.option_progressive, - options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, - options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, - options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, - options.QuestLocations.internal_name: 56, - options.Fishsanity.internal_name: options.Fishsanity.option_all, - options.Museumsanity.internal_name: options.Museumsanity.option_all, - options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals, - options.Shipsanity.internal_name: options.Shipsanity.option_everything, - options.Cooksanity.internal_name: options.Cooksanity.option_all, - options.Chefsanity.internal_name: options.Chefsanity.option_all, - options.Craftsanity.internal_name: options.Craftsanity.option_all, - options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, - options.FriendsanityHeartSize.internal_name: 1, - options.NumberOfMovementBuffs.internal_name: 12, - options.NumberOfLuckBuffs.internal_name: 12, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, - options.TrapItems.internal_name: options.TrapItems.option_nightmare, - } - option_dict.update(disable_5_x_x_options()) - return option_dict - - -@cache_argsless -def allsanity_options_without_mods(): - return { - options.Goal.internal_name: options.Goal.option_perfection, - options.BundleRandomization.internal_name: options.BundleRandomization.option_thematic, - options.BundlePrice.internal_name: options.BundlePrice.option_expensive, - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, - options.ToolProgression.internal_name: options.ToolProgression.option_progressive, - options.SkillProgression.internal_name: options.SkillProgression.option_progressive, - options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, - options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, - options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, - options.QuestLocations.internal_name: 56, - options.Fishsanity.internal_name: options.Fishsanity.option_all, - options.Museumsanity.internal_name: options.Museumsanity.option_all, - options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals, - options.Shipsanity.internal_name: options.Shipsanity.option_everything, - options.Cooksanity.internal_name: options.Cooksanity.option_all, - options.Chefsanity.internal_name: options.Chefsanity.option_all, - options.Craftsanity.internal_name: options.Craftsanity.option_all, - options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, - options.FriendsanityHeartSize.internal_name: 1, - options.NumberOfMovementBuffs.internal_name: 12, - options.NumberOfLuckBuffs.internal_name: 12, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, - options.TrapItems.internal_name: options.TrapItems.option_nightmare, - } - - -@cache_argsless -def allsanity_options_with_mods(): - allsanity = allsanity_options_without_mods().copy() - allsanity.update({options.Mods.internal_name: all_mods}) - return allsanity - - class SVTestCase(unittest.TestCase): # Set False to not skip some 'extra' tests skip_base_tests: bool = True @@ -219,7 +201,6 @@ def solo_world_sub_test(self, msg: Optional[str] = None, *, seed=DEFAULT_TEST_SEED, world_caching=True, - dirty_state=False, **kwargs) -> Tuple[MultiWorld, StardewValleyWorld]: if msg is not None: msg += " " @@ -228,17 +209,8 @@ def solo_world_sub_test(self, msg: Optional[str] = None, msg += f"[Seed = {seed}]" with self.subTest(msg, **kwargs): - if world_caching: - multi_world = setup_solo_multiworld(world_options, seed) - if dirty_state: - original_state = multi_world.state.copy() - else: - multi_world = setup_solo_multiworld(world_options, seed, _cache={}) - - yield multi_world, multi_world.worlds[1] - - if world_caching and dirty_state: - multi_world.state = original_state + with solo_multiworld(world_options, seed=seed, world_caching=world_caching) as (multiworld, world): + yield multiworld, world class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase): @@ -248,59 +220,140 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase): seed = DEFAULT_TEST_SEED - options = get_minsanity_options() + @classmethod + def setUpClass(cls) -> None: + if cls is SVTestBase: + raise unittest.SkipTest("No running tests on SVTestBase import.") + + super().setUpClass() def world_setup(self, *args, **kwargs): self.options = parse_class_option_keys(self.options) - super().world_setup(seed=self.seed) + self.multiworld = setup_solo_multiworld(self.options, seed=self.seed) + self.multiworld.lock.acquire() + world = self.multiworld.worlds[self.player] + + self.original_state = self.multiworld.state.copy() + self.original_itempool = self.multiworld.itempool.copy() + self.original_prog_item_count = world.total_progression_items + self.unfilled_locations = self.multiworld.get_unfilled_locations(1) if self.constructed: - self.world = self.multiworld.worlds[self.player] # noqa + self.world = world # noqa + + def tearDown(self) -> None: + self.multiworld.state = self.original_state + self.multiworld.itempool = self.original_itempool + for location in self.unfilled_locations: + location.item = None + self.world.total_progression_items = self.original_prog_item_count + + self.multiworld.lock.release() @property def run_default_tests(self) -> bool: if self.skip_base_tests: return False - # world_setup is overridden, so it'd always run default tests when importing SVTestBase - is_not_stardew_test = type(self) is not SVTestBase - should_run_default_tests = is_not_stardew_test and super().run_default_tests - return should_run_default_tests + return super().run_default_tests def collect_lots_of_money(self): self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False) - for i in range(100): + required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.25)) + for i in range(required_prog_items): self.multiworld.state.collect(self.world.create_item("Stardrop"), event=False) def collect_all_the_money(self): self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False) - for i in range(1000): + required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.95)) + for i in range(required_prog_items): self.multiworld.state.collect(self.world.create_item("Stardrop"), event=False) + def collect_everything(self): + non_event_items = [item for item in self.multiworld.get_items() if item.code] + for item in non_event_items: + self.multiworld.state.collect(item) + + def collect_all_except(self, item_to_not_collect: str): + for item in self.multiworld.get_items(): + if item.name != item_to_not_collect: + self.multiworld.state.collect(item) + def get_real_locations(self) -> List[Location]: return [location for location in self.multiworld.get_locations(self.player) if location.address is not None] def get_real_location_names(self) -> List[str]: return [location.name for location in self.get_real_locations()] + def collect(self, item: Union[str, Item, Iterable[Item]], count: int = 1) -> Union[None, Item, List[Item]]: + assert count > 0 + if not isinstance(item, str): + super().collect(item) + return + if count == 1: + item = self.create_item(item) + self.multiworld.state.collect(item) + return item + items = [] + for i in range(count): + item = self.create_item(item) + self.multiworld.state.collect(item) + items.append(item) + return items + + def create_item(self, item: str) -> StardewItem: + created_item = self.world.create_item(item) + if created_item.classification == ItemClassification.progression: + self.multiworld.worlds[self.player].total_progression_items -= 1 + return created_item + pre_generated_worlds = {} +@contextmanager +def solo_multiworld(world_options: Optional[Dict[Union[str, StardewValleyOption], Any]] = None, + *, + seed=DEFAULT_TEST_SEED, + world_caching=True) -> Tuple[MultiWorld, StardewValleyWorld]: + if not world_caching: + multiworld = setup_solo_multiworld(world_options, seed, _cache={}) + yield multiworld, multiworld.worlds[1] + else: + multiworld = setup_solo_multiworld(world_options, seed) + multiworld.lock.acquire() + world = multiworld.worlds[1] + + original_state = multiworld.state.copy() + original_itempool = multiworld.itempool.copy() + unfilled_locations = multiworld.get_unfilled_locations(1) + original_prog_item_count = world.total_progression_items + + yield multiworld, world + + multiworld.state = original_state + multiworld.itempool = original_itempool + for location in unfilled_locations: + location.item = None + multiworld.total_progression_items = original_prog_item_count + + multiworld.lock.release() + + # Mostly a copy of test.general.setup_solo_multiworld, I just don't want to change the core. def setup_solo_multiworld(test_options: Optional[Dict[Union[str, StardewValleyOption], str]] = None, seed=DEFAULT_TEST_SEED, - _cache: Dict[Hashable, MultiWorld] = {}, # noqa + _cache: Dict[frozenset, MultiWorld] = {}, # noqa _steps=gen_steps) -> MultiWorld: test_options = parse_class_option_keys(test_options) # Yes I reuse the worlds generated between tests, its speeds the execution by a couple seconds + # If the simple dict caching ends up taking too much memory, we could replace it with some kind of lru cache. should_cache = "start_inventory" not in test_options - frozen_options = frozenset({}) if should_cache: - frozen_options = frozenset(test_options.items()).union({seed}) - if frozen_options in _cache: - cached_multi_world = _cache[frozen_options] - print(f"Using cached solo multi world [Seed = {cached_multi_world.seed}]") + frozen_options = frozenset(test_options.items()).union({("seed", seed)}) + cached_multi_world = search_world_cache(_cache, frozen_options) + if cached_multi_world: + print(f"Using cached solo multi world [Seed = {cached_multi_world.seed}] [Cache size = {len(_cache)}]") return cached_multi_world multiworld = setup_base_solo_multiworld(StardewValleyWorld, (), seed=seed) @@ -326,28 +379,47 @@ def setup_solo_multiworld(test_options: Optional[Dict[Union[str, StardewValleyOp call_all(multiworld, step) if should_cache: - _cache[frozen_options] = multiworld + add_to_world_cache(_cache, frozen_options, multiworld) # noqa + + # Lock is needed for multi-threading tests + setattr(multiworld, "lock", threading.Lock()) return multiworld -def parse_class_option_keys(test_options: dict) -> dict: +def parse_class_option_keys(test_options: Optional[Dict]) -> dict: """ Now the option class is allowed as key. """ + if test_options is None: + return {} parsed_options = {} - if test_options: - for option, value in test_options.items(): - if hasattr(option, "internal_name"): - assert option.internal_name not in test_options, "Defined two times by class and internal_name" - parsed_options[option.internal_name] = value - else: - assert option in StardewValleyOptions.type_hints, \ - f"All keys of world_options must be a possible Stardew Valley option, {option} is not." - parsed_options[option] = value + for option, value in test_options.items(): + if hasattr(option, "internal_name"): + assert option.internal_name not in test_options, "Defined two times by class and internal_name" + parsed_options[option.internal_name] = value + else: + assert option in StardewValleyOptions.type_hints, \ + f"All keys of world_options must be a possible Stardew Valley option, {option} is not." + parsed_options[option] = value return parsed_options +def search_world_cache(cache: Dict[frozenset, MultiWorld], frozen_options: frozenset) -> Optional[MultiWorld]: + try: + return cache[frozen_options] + except KeyError: + for cached_options, multi_world in cache.items(): + if frozen_options.issubset(cached_options): + return multi_world + return None + + +def add_to_world_cache(cache: Dict[frozenset, MultiWorld], frozen_options: frozenset, multi_world: MultiWorld) -> None: + # We could complete the key with all the default options, but that does not seem to improve performances. + cache[frozen_options] = multi_world + + def complete_options_with_default(options_to_complete=None) -> StardewValleyOptions: if options_to_complete is None: options_to_complete = {} diff --git a/worlds/stardew_valley/test/assertion/mod_assert.py b/worlds/stardew_valley/test/assertion/mod_assert.py index eec7f805d2c5..baba9bbaf856 100644 --- a/worlds/stardew_valley/test/assertion/mod_assert.py +++ b/worlds/stardew_valley/test/assertion/mod_assert.py @@ -1,4 +1,4 @@ -from typing import Union, List +from typing import Union, Iterable from unittest import TestCase from BaseClasses import MultiWorld @@ -7,9 +7,11 @@ class ModAssertMixin(TestCase): - def assert_stray_mod_items(self, chosen_mods: Union[List[str], str], multiworld: MultiWorld): + def assert_stray_mod_items(self, chosen_mods: Union[Iterable[str], str], multiworld: MultiWorld): if isinstance(chosen_mods, str): chosen_mods = [chosen_mods] + else: + chosen_mods = list(chosen_mods) if ModNames.jasper in chosen_mods: # Jasper is a weird case because it shares NPC w/ SVE... diff --git a/worlds/stardew_valley/test/assertion/option_assert.py b/worlds/stardew_valley/test/assertion/option_assert.py index b384858f34f4..a07831f73e3f 100644 --- a/worlds/stardew_valley/test/assertion/option_assert.py +++ b/worlds/stardew_valley/test/assertion/option_assert.py @@ -63,8 +63,12 @@ def assert_cropsanity_same_number_items_and_locations(self, multiworld: MultiWor all_item_names = set(get_all_item_names(multiworld)) all_location_names = set(get_all_location_names(multiworld)) all_cropsanity_item_names = {item_name for item_name in all_item_names if Group.CROPSANITY in item_table[item_name].groups} - all_cropsanity_location_names = {location_name for location_name in all_location_names if LocationTags.CROPSANITY in location_table[location_name].tags} - self.assertEqual(len(all_cropsanity_item_names), len(all_cropsanity_location_names)) + all_cropsanity_location_names = {location_name + for location_name in all_location_names + if LocationTags.CROPSANITY in location_table[location_name].tags + # Qi Beans do not have an item + and location_name != "Harvest Qi Fruit"} + self.assertEqual(len(all_cropsanity_item_names) + 1, len(all_cropsanity_location_names)) def assert_all_rarecrows_exist(self, multiworld: MultiWorld): all_item_names = set(get_all_item_names(multiworld)) diff --git a/worlds/stardew_valley/test/assertion/rule_assert.py b/worlds/stardew_valley/test/assertion/rule_assert.py index f9b12394311a..5a1dad2925cf 100644 --- a/worlds/stardew_valley/test/assertion/rule_assert.py +++ b/worlds/stardew_valley/test/assertion/rule_assert.py @@ -1,17 +1,49 @@ from unittest import TestCase -from BaseClasses import CollectionState -from .rule_explain import explain -from ...stardew_rule import StardewRule, false_, MISSING_ITEM +from BaseClasses import CollectionState, Location +from ...stardew_rule import StardewRule, false_, MISSING_ITEM, Reach +from ...stardew_rule.rule_explain import explain class RuleAssertMixin(TestCase): def assert_rule_true(self, rule: StardewRule, state: CollectionState): - self.assertTrue(rule(state), explain(rule, state)) + expl = explain(rule, state) + try: + self.assertTrue(rule(state), expl) + except KeyError as e: + raise AssertionError(f"Error while checking rule {rule}: {e}" + f"\nExplanation: {expl}") def assert_rule_false(self, rule: StardewRule, state: CollectionState): - self.assertFalse(rule(state), explain(rule, state, expected=False)) + expl = explain(rule, state, expected=False) + try: + self.assertFalse(rule(state), expl) + except KeyError as e: + raise AssertionError(f"Error while checking rule {rule}: {e}" + f"\nExplanation: {expl}") def assert_rule_can_be_resolved(self, rule: StardewRule, complete_state: CollectionState): - self.assertNotIn(MISSING_ITEM, repr(rule)) - self.assertTrue(rule is false_ or rule(complete_state), explain(rule, complete_state)) + expl = explain(rule, complete_state) + try: + self.assertNotIn(MISSING_ITEM, repr(rule)) + self.assertTrue(rule is false_ or rule(complete_state), expl) + except KeyError as e: + raise AssertionError(f"Error while checking rule {rule}: {e}" + f"\nExplanation: {expl}") + + def assert_reach_location_true(self, location: Location, state: CollectionState): + expl = explain(Reach(location.name, "Location", 1), state) + try: + can_reach = location.can_reach(state) + self.assertTrue(can_reach, expl) + except KeyError as e: + raise AssertionError(f"Error while checking location {location.name}: {e}" + f"\nExplanation: {expl}") + + def assert_reach_location_false(self, location: Location, state: CollectionState): + expl = explain(Reach(location.name, "Location", 1), state, expected=False) + try: + self.assertFalse(location.can_reach(state), expl) + except KeyError as e: + raise AssertionError(f"Error while checking location {location.name}: {e}" + f"\nExplanation: {expl}") diff --git a/worlds/stardew_valley/test/assertion/rule_explain.py b/worlds/stardew_valley/test/assertion/rule_explain.py deleted file mode 100644 index f9bf97603404..000000000000 --- a/worlds/stardew_valley/test/assertion/rule_explain.py +++ /dev/null @@ -1,102 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from functools import cached_property, singledispatch -from typing import Iterable - -from BaseClasses import CollectionState -from worlds.generic.Rules import CollectionRule -from ...stardew_rule import StardewRule, AggregatingStardewRule, Count, Has, TotalReceived, Received, Reach - -max_explanation_depth = 10 - - -@dataclass -class RuleExplanation: - rule: StardewRule - state: CollectionState - expected: bool - sub_rules: Iterable[StardewRule] = field(default_factory=list) - - def summary(self, depth=0): - return " " * depth + f"{str(self.rule)} -> {self.result}" - - def __str__(self, depth=0): - if not self.sub_rules or depth >= max_explanation_depth: - return self.summary(depth) - - return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__str__(i, depth + 1) - if i.result is not self.expected else i.summary(depth + 1) - for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) - - def __repr__(self, depth=0): - if not self.sub_rules or depth >= max_explanation_depth: - return self.summary(depth) - - return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__repr__(i, depth + 1) - for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) - - @cached_property - def result(self): - return self.rule(self.state) - - @cached_property - def explained_sub_rules(self): - return [_explain(i, self.state, self.expected) for i in self.sub_rules] - - -def explain(rule: CollectionRule, state: CollectionState, expected: bool = True) -> RuleExplanation: - if isinstance(rule, StardewRule): - return _explain(rule, state, expected) - else: - return f"Value of rule {str(rule)} was not {str(expected)} in {str(state)}" # noqa - - -@singledispatch -def _explain(rule: StardewRule, state: CollectionState, expected: bool) -> RuleExplanation: - return RuleExplanation(rule, state, expected) - - -@_explain.register -def _(rule: AggregatingStardewRule, state: CollectionState, expected: bool) -> RuleExplanation: - return RuleExplanation(rule, state, expected, rule.original_rules) - - -@_explain.register -def _(rule: Count, state: CollectionState, expected: bool) -> RuleExplanation: - return RuleExplanation(rule, state, expected, rule.rules) - - -@_explain.register -def _(rule: Has, state: CollectionState, expected: bool) -> RuleExplanation: - return RuleExplanation(rule, state, expected, [rule.other_rules[rule.item]]) - - -@_explain.register -def _(rule: TotalReceived, state: CollectionState, expected=True) -> RuleExplanation: - return RuleExplanation(rule, state, expected, [Received(i, rule.player, 1) for i in rule.items]) - - -@_explain.register -def _(rule: Reach, state: CollectionState, expected=True) -> RuleExplanation: - access_rules = None - if rule.resolution_hint == 'Location': - spot = state.multiworld.get_location(rule.spot, rule.player) - - if isinstance(spot.access_rule, StardewRule): - access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] - - elif rule.resolution_hint == 'Entrance': - spot = state.multiworld.get_entrance(rule.spot, rule.player) - - if isinstance(spot.access_rule, StardewRule): - access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] - - else: - spot = state.multiworld.get_region(rule.spot, rule.player) - access_rules = [*(Reach(e.name, "Entrance", rule.player) for e in spot.entrances)] - - if not access_rules: - return RuleExplanation(rule, state, expected) - - return RuleExplanation(rule, state, expected, access_rules) diff --git a/worlds/stardew_valley/test/assertion/world_assert.py b/worlds/stardew_valley/test/assertion/world_assert.py index 1e5512682f92..c1c24bdf75b4 100644 --- a/worlds/stardew_valley/test/assertion/world_assert.py +++ b/worlds/stardew_valley/test/assertion/world_assert.py @@ -53,7 +53,7 @@ def assert_same_number_items_locations(self, multiworld: MultiWorld): def assert_can_reach_everything(self, multiworld: MultiWorld): for location in multiworld.get_locations(): - self.assert_rule_true(location.access_rule, multiworld.state) + self.assert_reach_location_true(location, multiworld.state) def assert_basic_checks(self, multiworld: MultiWorld): self.assert_same_number_items_locations(multiworld) diff --git a/worlds/stardew_valley/test/content/TestArtisanEquipment.py b/worlds/stardew_valley/test/content/TestArtisanEquipment.py new file mode 100644 index 000000000000..32821511c44f --- /dev/null +++ b/worlds/stardew_valley/test/content/TestArtisanEquipment.py @@ -0,0 +1,54 @@ +from . import SVContentPackTestBase +from ...data.artisan import MachineSource +from ...strings.artisan_good_names import ArtisanGood +from ...strings.crop_names import Vegetable, Fruit +from ...strings.food_names import Beverage +from ...strings.forageable_names import Forageable +from ...strings.machine_names import Machine +from ...strings.seed_names import Seed + +wine_base_fruits = [ + Fruit.ancient_fruit, Fruit.apple, Fruit.apricot, Forageable.blackberry, Fruit.blueberry, Forageable.cactus_fruit, Fruit.cherry, + Forageable.coconut, Fruit.cranberries, Forageable.crystal_fruit, Fruit.grape, Fruit.hot_pepper, Fruit.melon, Fruit.orange, Fruit.peach, + Fruit.pomegranate, Fruit.powdermelon, Fruit.rhubarb, Forageable.salmonberry, Forageable.spice_berry, Fruit.starfruit, Fruit.strawberry +] + +juice_base_vegetables = ( + Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.bok_choy, Vegetable.broccoli, Vegetable.carrot, Vegetable.cauliflower, + Vegetable.corn, Vegetable.eggplant, Forageable.fiddlehead_fern, Vegetable.garlic, Vegetable.green_bean, Vegetable.kale, Vegetable.parsnip, Vegetable.potato, + Vegetable.pumpkin, Vegetable.radish, Vegetable.red_cabbage, Vegetable.summer_squash, Vegetable.tomato, Vegetable.unmilled_rice, Vegetable.yam +) + +non_juice_base_vegetables = ( + Vegetable.hops, Vegetable.tea_leaves, Vegetable.wheat +) + + +class TestArtisanEquipment(SVContentPackTestBase): + + def test_keg_special_recipes(self): + self.assertIn(MachineSource(item=Vegetable.wheat, machine=Machine.keg), self.content.game_items[Beverage.beer].sources) + # self.assertIn(MachineSource(item=Ingredient.rice, machine=Machine.keg), self.content.game_items[Ingredient.vinegar].sources) + self.assertIn(MachineSource(item=Seed.coffee, machine=Machine.keg), self.content.game_items[Beverage.coffee].sources) + self.assertIn(MachineSource(item=Vegetable.tea_leaves, machine=Machine.keg), self.content.game_items[ArtisanGood.green_tea].sources) + self.assertIn(MachineSource(item=ArtisanGood.honey, machine=Machine.keg), self.content.game_items[ArtisanGood.mead].sources) + self.assertIn(MachineSource(item=Vegetable.hops, machine=Machine.keg), self.content.game_items[ArtisanGood.pale_ale].sources) + + def test_fruits_can_be_made_into_wines(self): + + for fruit in wine_base_fruits: + with self.subTest(fruit): + self.assertIn(MachineSource(item=fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(fruit)].sources) + self.assertIn(MachineSource(item=fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) + + def test_vegetables_can_be_made_into_juices(self): + for vegetable in juice_base_vegetables: + with self.subTest(vegetable): + self.assertIn(MachineSource(item=vegetable, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_juice(vegetable)].sources) + self.assertIn(MachineSource(item=vegetable, machine=Machine.keg), self.content.game_items[ArtisanGood.juice].sources) + + def test_non_juice_vegetables_cannot_be_made_into_juices(self): + for vegetable in non_juice_base_vegetables: + with self.subTest(vegetable): + self.assertNotIn(ArtisanGood.specific_juice(vegetable), self.content.game_items) + self.assertNotIn(MachineSource(item=vegetable, machine=Machine.keg), self.content.game_items[ArtisanGood.juice].sources) diff --git a/worlds/stardew_valley/test/content/TestGingerIsland.py b/worlds/stardew_valley/test/content/TestGingerIsland.py new file mode 100644 index 000000000000..7e7f866dfc8e --- /dev/null +++ b/worlds/stardew_valley/test/content/TestGingerIsland.py @@ -0,0 +1,55 @@ +from . import SVContentPackTestBase +from .. import SVTestBase +from ... import options +from ...content import content_packs +from ...data.artisan import MachineSource +from ...strings.artisan_good_names import ArtisanGood +from ...strings.crop_names import Fruit, Vegetable +from ...strings.fish_names import Fish +from ...strings.machine_names import Machine +from ...strings.villager_names import NPC + + +class TestGingerIsland(SVContentPackTestBase): + vanilla_packs = SVContentPackTestBase.vanilla_packs + (content_packs.ginger_island_content_pack,) + + def test_leo_is_included(self): + self.assertIn(NPC.leo, self.content.villagers) + + def test_ginger_island_fishes_are_included(self): + fish_names = self.content.fishes.keys() + + self.assertIn(Fish.blue_discus, fish_names) + self.assertIn(Fish.lionfish, fish_names) + self.assertIn(Fish.stingray, fish_names) + + # 63 from pelican town + 3 ginger island exclusive + self.assertEqual(63 + 3, len(self.content.fishes)) + + def test_ginger_island_fruits_can_be_made_into_wines(self): + self.assertIn(MachineSource(item=Fruit.banana, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.banana)].sources) + self.assertIn(MachineSource(item=Fruit.banana, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) + + self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.mango)].sources) + self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) + + self.assertIn(MachineSource(item=Fruit.pineapple, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.pineapple)].sources) + self.assertIn(MachineSource(item=Fruit.pineapple, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) + + def test_ginger_island_vegetables_can_be_made_into_wines(self): + taro_root_juice_sources = self.content.game_items[ArtisanGood.specific_juice(Vegetable.taro_root)].sources + self.assertIn(MachineSource(item=Vegetable.taro_root, machine=Machine.keg), taro_root_juice_sources) + self.assertIn(MachineSource(item=Vegetable.taro_root, machine=Machine.keg), self.content.game_items[ArtisanGood.juice].sources) + + +class TestWithoutGingerIslandE2E(SVTestBase): + options = { + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true + } + + def test_leo_is_not_in_the_pool(self): + for item in self.multiworld.itempool: + self.assertFalse(("Friendsanity: " + NPC.leo) in item.name) + + for location in self.multiworld.get_locations(self.player): + self.assertFalse(("Friendsanity: " + NPC.leo) in location.name) diff --git a/worlds/stardew_valley/test/content/TestPelicanTown.py b/worlds/stardew_valley/test/content/TestPelicanTown.py new file mode 100644 index 000000000000..fa70916c9d33 --- /dev/null +++ b/worlds/stardew_valley/test/content/TestPelicanTown.py @@ -0,0 +1,112 @@ +from . import SVContentPackTestBase +from ...strings.fish_names import Fish +from ...strings.villager_names import NPC + + +class TestPelicanTown(SVContentPackTestBase): + + def test_all_pelican_town_villagers_are_included(self): + self.assertIn(NPC.alex, self.content.villagers) + self.assertIn(NPC.elliott, self.content.villagers) + self.assertIn(NPC.harvey, self.content.villagers) + self.assertIn(NPC.sam, self.content.villagers) + self.assertIn(NPC.sebastian, self.content.villagers) + self.assertIn(NPC.shane, self.content.villagers) + self.assertIn(NPC.abigail, self.content.villagers) + self.assertIn(NPC.emily, self.content.villagers) + self.assertIn(NPC.haley, self.content.villagers) + self.assertIn(NPC.leah, self.content.villagers) + self.assertIn(NPC.maru, self.content.villagers) + self.assertIn(NPC.penny, self.content.villagers) + self.assertIn(NPC.caroline, self.content.villagers) + self.assertIn(NPC.clint, self.content.villagers) + self.assertIn(NPC.demetrius, self.content.villagers) + self.assertIn(NPC.dwarf, self.content.villagers) + self.assertIn(NPC.evelyn, self.content.villagers) + self.assertIn(NPC.george, self.content.villagers) + self.assertIn(NPC.gus, self.content.villagers) + self.assertIn(NPC.jas, self.content.villagers) + self.assertIn(NPC.jodi, self.content.villagers) + self.assertIn(NPC.kent, self.content.villagers) + self.assertIn(NPC.krobus, self.content.villagers) + self.assertIn(NPC.lewis, self.content.villagers) + self.assertIn(NPC.linus, self.content.villagers) + self.assertIn(NPC.marnie, self.content.villagers) + self.assertIn(NPC.pam, self.content.villagers) + self.assertIn(NPC.pierre, self.content.villagers) + self.assertIn(NPC.robin, self.content.villagers) + self.assertIn(NPC.sandy, self.content.villagers) + self.assertIn(NPC.vincent, self.content.villagers) + self.assertIn(NPC.willy, self.content.villagers) + self.assertIn(NPC.wizard, self.content.villagers) + + self.assertEqual(33, len(self.content.villagers)) + + def test_all_pelican_town_fishes_are_included(self): + fish_names = self.content.fishes.keys() + + self.assertIn(Fish.albacore, fish_names) + self.assertIn(Fish.anchovy, fish_names) + self.assertIn(Fish.bream, fish_names) + self.assertIn(Fish.bullhead, fish_names) + self.assertIn(Fish.carp, fish_names) + self.assertIn(Fish.catfish, fish_names) + self.assertIn(Fish.chub, fish_names) + self.assertIn(Fish.dorado, fish_names) + self.assertIn(Fish.eel, fish_names) + self.assertIn(Fish.flounder, fish_names) + self.assertIn(Fish.ghostfish, fish_names) + self.assertIn(Fish.goby, fish_names) + self.assertIn(Fish.halibut, fish_names) + self.assertIn(Fish.herring, fish_names) + self.assertIn(Fish.ice_pip, fish_names) + self.assertIn(Fish.largemouth_bass, fish_names) + self.assertIn(Fish.lava_eel, fish_names) + self.assertIn(Fish.lingcod, fish_names) + self.assertIn(Fish.midnight_carp, fish_names) + self.assertIn(Fish.octopus, fish_names) + self.assertIn(Fish.perch, fish_names) + self.assertIn(Fish.pike, fish_names) + self.assertIn(Fish.pufferfish, fish_names) + self.assertIn(Fish.rainbow_trout, fish_names) + self.assertIn(Fish.red_mullet, fish_names) + self.assertIn(Fish.red_snapper, fish_names) + self.assertIn(Fish.salmon, fish_names) + self.assertIn(Fish.sandfish, fish_names) + self.assertIn(Fish.sardine, fish_names) + self.assertIn(Fish.scorpion_carp, fish_names) + self.assertIn(Fish.sea_cucumber, fish_names) + self.assertIn(Fish.shad, fish_names) + self.assertIn(Fish.slimejack, fish_names) + self.assertIn(Fish.smallmouth_bass, fish_names) + self.assertIn(Fish.squid, fish_names) + self.assertIn(Fish.stonefish, fish_names) + self.assertIn(Fish.sturgeon, fish_names) + self.assertIn(Fish.sunfish, fish_names) + self.assertIn(Fish.super_cucumber, fish_names) + self.assertIn(Fish.tiger_trout, fish_names) + self.assertIn(Fish.tilapia, fish_names) + self.assertIn(Fish.tuna, fish_names) + self.assertIn(Fish.void_salmon, fish_names) + self.assertIn(Fish.walleye, fish_names) + self.assertIn(Fish.woodskip, fish_names) + self.assertIn(Fish.blobfish, fish_names) + self.assertIn(Fish.midnight_squid, fish_names) + self.assertIn(Fish.spook_fish, fish_names) + self.assertIn(Fish.angler, fish_names) + self.assertIn(Fish.crimsonfish, fish_names) + self.assertIn(Fish.glacierfish, fish_names) + self.assertIn(Fish.legend, fish_names) + self.assertIn(Fish.mutant_carp, fish_names) + self.assertIn(Fish.clam, fish_names) + self.assertIn(Fish.cockle, fish_names) + self.assertIn(Fish.crab, fish_names) + self.assertIn(Fish.crayfish, fish_names) + self.assertIn(Fish.lobster, fish_names) + self.assertIn(Fish.mussel, fish_names) + self.assertIn(Fish.oyster, fish_names) + self.assertIn(Fish.periwinkle, fish_names) + self.assertIn(Fish.shrimp, fish_names) + self.assertIn(Fish.snail, fish_names) + + self.assertEqual(63, len(self.content.fishes)) diff --git a/worlds/stardew_valley/test/content/TestQiBoard.py b/worlds/stardew_valley/test/content/TestQiBoard.py new file mode 100644 index 000000000000..b9d940d2c887 --- /dev/null +++ b/worlds/stardew_valley/test/content/TestQiBoard.py @@ -0,0 +1,27 @@ +from . import SVContentPackTestBase +from ...content import content_packs +from ...data.artisan import MachineSource +from ...strings.artisan_good_names import ArtisanGood +from ...strings.crop_names import Fruit +from ...strings.fish_names import Fish +from ...strings.machine_names import Machine + + +class TestQiBoard(SVContentPackTestBase): + vanilla_packs = SVContentPackTestBase.vanilla_packs + (content_packs.ginger_island_content_pack, content_packs.qi_board_content_pack) + + def test_extended_family_fishes_are_included(self): + fish_names = self.content.fishes.keys() + + self.assertIn(Fish.ms_angler, fish_names) + self.assertIn(Fish.son_of_crimsonfish, fish_names) + self.assertIn(Fish.glacierfish_jr, fish_names) + self.assertIn(Fish.legend_ii, fish_names) + self.assertIn(Fish.radioactive_carp, fish_names) + + # 63 from pelican town + 3 ginger island exclusive + 5 extended family + self.assertEqual(63 + 3 + 5, len(self.content.fishes)) + + def test_wines(self): + self.assertIn(MachineSource(item=Fruit.qi_fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.qi_fruit)].sources) + self.assertIn(MachineSource(item=Fruit.qi_fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) diff --git a/worlds/stardew_valley/test/content/__init__.py b/worlds/stardew_valley/test/content/__init__.py new file mode 100644 index 000000000000..4130dae90dc3 --- /dev/null +++ b/worlds/stardew_valley/test/content/__init__.py @@ -0,0 +1,23 @@ +import unittest +from typing import ClassVar, Tuple + +from ...content import content_packs, ContentPack, StardewContent, unpack_content, StardewFeatures, feature + +default_features = StardewFeatures( + feature.booksanity.BooksanityDisabled(), + feature.cropsanity.CropsanityDisabled(), + feature.fishsanity.FishsanityNone(), + feature.friendsanity.FriendsanityNone() +) + + +class SVContentPackTestBase(unittest.TestCase): + vanilla_packs: ClassVar[Tuple[ContentPack]] = (content_packs.pelican_town, content_packs.the_desert, content_packs.the_farm, content_packs.the_mines) + mods: ClassVar[Tuple[str]] = () + + content: ClassVar[StardewContent] + + @classmethod + def setUpClass(cls) -> None: + packs = cls.vanilla_packs + tuple(content_packs.by_mod[mod] for mod in cls.mods) + cls.content = unpack_content(default_features, packs) diff --git a/worlds/stardew_valley/test/content/feature/TestFriendsanity.py b/worlds/stardew_valley/test/content/feature/TestFriendsanity.py new file mode 100644 index 000000000000..804ac0978bb5 --- /dev/null +++ b/worlds/stardew_valley/test/content/feature/TestFriendsanity.py @@ -0,0 +1,33 @@ +import unittest + +from ....content.feature import friendsanity + + +class TestHeartSteps(unittest.TestCase): + + def test_given_size_of_one_when_calculate_steps_then_advance_one_heart_at_the_time(self): + steps = friendsanity.get_heart_steps(4, 1) + + self.assertEqual(steps, (1, 2, 3, 4)) + + def test_given_size_of_two_when_calculate_steps_then_advance_two_heart_at_the_time(self): + steps = friendsanity.get_heart_steps(6, 2) + + self.assertEqual(steps, (2, 4, 6)) + + def test_given_size_of_three_and_max_heart_not_multiple_of_three_when_calculate_steps_then_add_max_as_last_step(self): + steps = friendsanity.get_heart_steps(7, 3) + + self.assertEqual(steps, (3, 6, 7)) + + +class TestExtractNpcFromLocation(unittest.TestCase): + + def test_given_npc_with_space_in_name_when_extract_then_find_name_and_heart(self): + npc = "Mr. Ginger" + location_name = friendsanity.to_location_name(npc, 34) + + found_name, found_heart = friendsanity.extract_npc_from_location_name(location_name) + + self.assertEqual(found_name, npc) + self.assertEqual(found_heart, 34) diff --git a/worlds/stardew_valley/test/content/feature/__init__.py b/worlds/stardew_valley/test/content/feature/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/test/content/mods/TestDeepwoods.py b/worlds/stardew_valley/test/content/mods/TestDeepwoods.py new file mode 100644 index 000000000000..381502da13ba --- /dev/null +++ b/worlds/stardew_valley/test/content/mods/TestDeepwoods.py @@ -0,0 +1,14 @@ +from ....data.artisan import MachineSource +from ....mods.mod_data import ModNames +from ....strings.artisan_good_names import ArtisanGood +from ....strings.crop_names import Fruit +from ....strings.machine_names import Machine +from ....test.content import SVContentPackTestBase + + +class TestArtisanEquipment(SVContentPackTestBase): + mods = (ModNames.deepwoods,) + + def test_mango_wine_exists(self): + self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.mango)].sources) + self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) diff --git a/worlds/stardew_valley/test/content/mods/TestJasper.py b/worlds/stardew_valley/test/content/mods/TestJasper.py new file mode 100644 index 000000000000..40927e67c258 --- /dev/null +++ b/worlds/stardew_valley/test/content/mods/TestJasper.py @@ -0,0 +1,27 @@ +from .. import SVContentPackTestBase +from ....mods.mod_data import ModNames +from ....strings.villager_names import ModNPC + + +class TestJasperWithoutSVE(SVContentPackTestBase): + mods = (ModNames.jasper,) + + def test_gunther_is_added(self): + self.assertIn(ModNPC.gunther, self.content.villagers) + self.assertEqual(self.content.villagers[ModNPC.gunther].mod_name, ModNames.jasper) + + def test_marlon_is_added(self): + self.assertIn(ModNPC.marlon, self.content.villagers) + self.assertEqual(self.content.villagers[ModNPC.marlon].mod_name, ModNames.jasper) + + +class TestJasperWithSVE(SVContentPackTestBase): + mods = (ModNames.jasper, ModNames.sve) + + def test_gunther_is_added(self): + self.assertIn(ModNPC.gunther, self.content.villagers) + self.assertEqual(self.content.villagers[ModNPC.gunther].mod_name, ModNames.sve) + + def test_marlon_is_added(self): + self.assertIn(ModNPC.marlon, self.content.villagers) + self.assertEqual(self.content.villagers[ModNPC.marlon].mod_name, ModNames.sve) diff --git a/worlds/stardew_valley/test/content/mods/TestSVE.py b/worlds/stardew_valley/test/content/mods/TestSVE.py new file mode 100644 index 000000000000..4065498d6be7 --- /dev/null +++ b/worlds/stardew_valley/test/content/mods/TestSVE.py @@ -0,0 +1,143 @@ +from .. import SVContentPackTestBase +from ... import SVTestBase +from .... import options +from ....content import content_packs +from ....mods.mod_data import ModNames +from ....strings.fish_names import SVEFish +from ....strings.villager_names import ModNPC, NPC + +vanilla_villagers = 33 +vanilla_villagers_with_leo = 34 +sve_villagers = 13 +sve_villagers_with_lance = 14 +vanilla_pelican_town_fish = 63 +vanilla_ginger_island_fish = 3 +sve_pelican_town_fish = 16 +sve_ginger_island_fish = 10 + + +class TestVanilla(SVContentPackTestBase): + + def test_wizard_is_not_bachelor(self): + self.assertFalse(self.content.villagers[NPC.wizard].bachelor) + + +class TestSVE(SVContentPackTestBase): + mods = (ModNames.sve,) + + def test_lance_is_not_included(self): + self.assertNotIn(ModNPC.lance, self.content.villagers) + + def test_wizard_is_bachelor(self): + self.assertTrue(self.content.villagers[NPC.wizard].bachelor) + self.assertEqual(self.content.villagers[NPC.wizard].mod_name, ModNames.sve) + + def test_sve_npc_are_included(self): + self.assertIn(ModNPC.apples, self.content.villagers) + self.assertIn(ModNPC.claire, self.content.villagers) + self.assertIn(ModNPC.olivia, self.content.villagers) + self.assertIn(ModNPC.sophia, self.content.villagers) + self.assertIn(ModNPC.victor, self.content.villagers) + self.assertIn(ModNPC.andy, self.content.villagers) + self.assertIn(ModNPC.gunther, self.content.villagers) + self.assertIn(ModNPC.martin, self.content.villagers) + self.assertIn(ModNPC.marlon, self.content.villagers) + self.assertIn(ModNPC.morgan, self.content.villagers) + self.assertIn(ModNPC.morris, self.content.villagers) + self.assertIn(ModNPC.scarlett, self.content.villagers) + self.assertIn(ModNPC.susan, self.content.villagers) + + self.assertEqual(vanilla_villagers + sve_villagers, len(self.content.villagers)) + + def test_sve_has_sve_fish(self): + fish_names = self.content.fishes.keys() + + self.assertIn(SVEFish.bonefish, fish_names) + self.assertIn(SVEFish.bull_trout, fish_names) + self.assertIn(SVEFish.butterfish, fish_names) + self.assertIn(SVEFish.frog, fish_names) + self.assertIn(SVEFish.goldenfish, fish_names) + self.assertIn(SVEFish.grass_carp, fish_names) + self.assertIn(SVEFish.king_salmon, fish_names) + self.assertIn(SVEFish.kittyfish, fish_names) + self.assertIn(SVEFish.meteor_carp, fish_names) + self.assertIn(SVEFish.minnow, fish_names) + self.assertIn(SVEFish.puppyfish, fish_names) + self.assertIn(SVEFish.radioactive_bass, fish_names) + self.assertIn(SVEFish.snatcher_worm, fish_names) + self.assertIn(SVEFish.undeadfish, fish_names) + self.assertIn(SVEFish.void_eel, fish_names) + self.assertIn(SVEFish.water_grub, fish_names) + + self.assertEqual(vanilla_pelican_town_fish + sve_pelican_town_fish, len(self.content.fishes)) + + +class TestSVEWithGingerIsland(SVContentPackTestBase): + vanilla_packs = SVContentPackTestBase.vanilla_packs + (content_packs.ginger_island_content_pack,) + mods = (ModNames.sve,) + + def test_lance_is_included(self): + self.assertIn(ModNPC.lance, self.content.villagers) + + def test_other_sve_npc_are_included(self): + self.assertIn(ModNPC.apples, self.content.villagers) + self.assertIn(ModNPC.claire, self.content.villagers) + self.assertIn(ModNPC.olivia, self.content.villagers) + self.assertIn(ModNPC.sophia, self.content.villagers) + self.assertIn(ModNPC.victor, self.content.villagers) + self.assertIn(ModNPC.andy, self.content.villagers) + self.assertIn(ModNPC.gunther, self.content.villagers) + self.assertIn(ModNPC.martin, self.content.villagers) + self.assertIn(ModNPC.marlon, self.content.villagers) + self.assertIn(ModNPC.morgan, self.content.villagers) + self.assertIn(ModNPC.morris, self.content.villagers) + self.assertIn(ModNPC.scarlett, self.content.villagers) + self.assertIn(ModNPC.susan, self.content.villagers) + + self.assertEqual(vanilla_villagers_with_leo + sve_villagers_with_lance, len(self.content.villagers)) + + def test_sve_has_sve_fish(self): + fish_names = self.content.fishes.keys() + + self.assertIn(SVEFish.baby_lunaloo, fish_names) + self.assertIn(SVEFish.bonefish, fish_names) + self.assertIn(SVEFish.bull_trout, fish_names) + self.assertIn(SVEFish.butterfish, fish_names) + self.assertIn(SVEFish.clownfish, fish_names) + self.assertIn(SVEFish.daggerfish, fish_names) + self.assertIn(SVEFish.frog, fish_names) + self.assertIn(SVEFish.gemfish, fish_names) + self.assertIn(SVEFish.goldenfish, fish_names) + self.assertIn(SVEFish.grass_carp, fish_names) + self.assertIn(SVEFish.king_salmon, fish_names) + self.assertIn(SVEFish.kittyfish, fish_names) + self.assertIn(SVEFish.lunaloo, fish_names) + self.assertIn(SVEFish.meteor_carp, fish_names) + self.assertIn(SVEFish.minnow, fish_names) + self.assertIn(SVEFish.puppyfish, fish_names) + self.assertIn(SVEFish.radioactive_bass, fish_names) + self.assertIn(SVEFish.seahorse, fish_names) + self.assertIn(SVEFish.shiny_lunaloo, fish_names) + self.assertIn(SVEFish.snatcher_worm, fish_names) + self.assertIn(SVEFish.starfish, fish_names) + self.assertIn(SVEFish.torpedo_trout, fish_names) + self.assertIn(SVEFish.undeadfish, fish_names) + self.assertIn(SVEFish.void_eel, fish_names) + self.assertIn(SVEFish.water_grub, fish_names) + self.assertIn(SVEFish.sea_sponge, fish_names) + + self.assertEqual(vanilla_pelican_town_fish + vanilla_ginger_island_fish + sve_pelican_town_fish + sve_ginger_island_fish, len(self.content.fishes)) + + +class TestSVEWithoutGingerIslandE2E(SVTestBase): + options = { + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true, + options.Mods: ModNames.sve + } + + def test_lance_is_not_in_the_pool(self): + for item in self.multiworld.itempool: + self.assertFalse(("Friendsanity: " + ModNPC.lance) in item.name) + + for location in self.multiworld.get_locations(self.player): + self.assertFalse(("Friendsanity: " + ModNPC.lance) in location.name) diff --git a/worlds/stardew_valley/test/content/mods/__init__.py b/worlds/stardew_valley/test/content/mods/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/test/long/TestModsLong.py b/worlds/stardew_valley/test/long/TestModsLong.py index 9f76c10a9da4..395c48ee698a 100644 --- a/worlds/stardew_valley/test/long/TestModsLong.py +++ b/worlds/stardew_valley/test/long/TestModsLong.py @@ -2,22 +2,25 @@ from itertools import combinations, product from BaseClasses import get_seed -from .option_names import all_option_choices +from .option_names import all_option_choices, get_option_choices from .. import SVTestCase from ..assertion import WorldAssertMixin, ModAssertMixin from ... import options -from ...mods.mod_data import all_mods, ModNames +from ...mods.mod_data import ModNames assert unittest class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase): - def test_given_mod_pairs_when_generate_then_basic_checks(self): - if self.skip_long_tests: - return + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + if cls.skip_long_tests: + raise unittest.SkipTest("Long tests disabled") - for mod_pair in combinations(all_mods, 2): + def test_given_mod_pairs_when_generate_then_basic_checks(self): + for mod_pair in combinations(options.Mods.valid_keys, 2): world_options = { options.Mods: frozenset(mod_pair) } @@ -27,10 +30,7 @@ def test_given_mod_pairs_when_generate_then_basic_checks(self): self.assert_stray_mod_items(list(mod_pair), multiworld) def test_given_mod_names_when_generate_paired_with_other_options_then_basic_checks(self): - if self.skip_long_tests: - return - - for mod, (option, value) in product(all_mods, all_option_choices): + for mod, (option, value) in product(options.Mods.valid_keys, all_option_choices): world_options = { option: value, options.Mods: mod @@ -40,12 +40,28 @@ def test_given_mod_names_when_generate_paired_with_other_options_then_basic_chec self.assert_basic_checks(multiworld) self.assert_stray_mod_items(mod, multiworld) - # @unittest.skip + def test_given_no_quest_all_mods_when_generate_with_all_goals_then_basic_checks(self): + for goal, (option, value) in product(get_option_choices(options.Goal), all_option_choices): + if option is options.QuestLocations: + continue + + world_options = { + options.Goal: goal, + option: value, + options.QuestLocations: -1, + options.Mods: frozenset(options.Mods.valid_keys), + } + + with self.solo_world_sub_test(f"Goal: {goal}, {option.internal_name}: {value}", world_options, world_caching=False) as (multiworld, _): + self.assert_basic_checks(multiworld) + + @unittest.skip def test_troubleshoot_option(self): - seed = get_seed(45949559493817417717) + seed = get_seed(78709133382876990000) + world_options = { - options.ElevatorProgression: options.ElevatorProgression.option_vanilla, - options.Mods: ModNames.deepwoods + options.EntranceRandomization: options.EntranceRandomization.option_buildings, + options.Mods: ModNames.sve } with self.solo_world_sub_test(world_options=world_options, seed=seed, world_caching=False) as (multiworld, _): diff --git a/worlds/stardew_valley/test/long/TestOptionsLong.py b/worlds/stardew_valley/test/long/TestOptionsLong.py index ca9fc01b2922..0c8cfcb1e107 100644 --- a/worlds/stardew_valley/test/long/TestOptionsLong.py +++ b/worlds/stardew_valley/test/long/TestOptionsLong.py @@ -1,15 +1,18 @@ +import unittest from itertools import combinations +from BaseClasses import get_seed from .option_names import all_option_choices -from .. import setup_solo_multiworld, SVTestCase +from .. import SVTestCase, solo_multiworld from ..assertion.world_assert import WorldAssertMixin from ... import options +from ...mods.mod_data import ModNames class TestGenerateDynamicOptions(WorldAssertMixin, SVTestCase): def test_given_option_pair_when_generate_then_basic_checks(self): if self.skip_long_tests: - return + raise unittest.SkipTest("Long tests disabled") for (option1, option1_choice), (option2, option2_choice) in combinations(all_option_choices, 2): if option1 is option2: @@ -31,13 +34,14 @@ class TestDynamicOptionDebug(WorldAssertMixin, SVTestCase): def test_option_pair_debug(self): option_dict = { - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, - options.Monstersanity.internal_name: options.Monstersanity.option_one_per_monster, + options.Goal.internal_name: options.Goal.option_master_angler, + options.QuestLocations.internal_name: -1, + options.Fishsanity.internal_name: options.Fishsanity.option_all, + options.Mods.internal_name: frozenset({ModNames.sve}), } for i in range(1): - # seed = int(random() * pow(10, 18) - 1) - seed = 823942126251776128 + seed = get_seed() with self.subTest(f"Seed: {seed}"): print(f"Seed: {seed}") - multiworld = setup_solo_multiworld(option_dict, seed) - self.assert_basic_checks(multiworld) + with solo_multiworld(option_dict, seed=seed) as (multiworld, _): + self.assert_basic_checks(multiworld) diff --git a/worlds/stardew_valley/test/long/TestPreRolledRandomness.py b/worlds/stardew_valley/test/long/TestPreRolledRandomness.py index 66bc5aeba8bb..f233fc36dc84 100644 --- a/worlds/stardew_valley/test/long/TestPreRolledRandomness.py +++ b/worlds/stardew_valley/test/long/TestPreRolledRandomness.py @@ -1,3 +1,5 @@ +import unittest + from BaseClasses import get_seed from .. import SVTestCase from ..assertion import WorldAssertMixin @@ -7,7 +9,8 @@ class TestGeneratePreRolledRandomness(WorldAssertMixin, SVTestCase): def test_given_pre_rolled_difficult_randomness_when_generate_then_basic_checks(self): if self.skip_long_tests: - return + raise unittest.SkipTest("Long tests disabled") + choices = { options.EntranceRandomization.internal_name: options.EntranceRandomization.option_buildings, options.BundleRandomization.internal_name: options.BundleRandomization.option_remixed, diff --git a/worlds/stardew_valley/test/long/TestRandomWorlds.py b/worlds/stardew_valley/test/long/TestRandomWorlds.py index f3702c05f42b..6d4931280a79 100644 --- a/worlds/stardew_valley/test/long/TestRandomWorlds.py +++ b/worlds/stardew_valley/test/long/TestRandomWorlds.py @@ -1,10 +1,11 @@ import random +import unittest from typing import Dict from BaseClasses import MultiWorld, get_seed from Options import NamedRange, Range from .option_names import options_to_include -from .. import setup_solo_multiworld, SVTestCase +from .. import SVTestCase from ..assertion import GoalAssertMixin, OptionAssertMixin, WorldAssertMixin @@ -18,12 +19,6 @@ def get_option_choices(option) -> Dict[str, int]: return {} -def generate_random_multiworld(world_id: int): - world_options = generate_random_world_options(world_id) - multiworld = setup_solo_multiworld(world_options, seed=world_id) - return multiworld - - def generate_random_world_options(seed: int) -> Dict[str, int]: num_options = len(options_to_include) world_options = dict() @@ -57,7 +52,8 @@ def get_number_log_steps(number_worlds: int) -> int: class TestGenerateManyWorlds(GoalAssertMixin, OptionAssertMixin, WorldAssertMixin, SVTestCase): def test_generate_many_worlds_then_check_results(self): if self.skip_long_tests: - return + raise unittest.SkipTest("Long tests disabled") + number_worlds = 10 if self.skip_long_tests else 1000 seed = get_seed() self.generate_and_check_many_worlds(number_worlds, seed) diff --git a/worlds/stardew_valley/test/mods/TestModFish.py b/worlds/stardew_valley/test/mods/TestModFish.py deleted file mode 100644 index 81ac6ac0fb99..000000000000 --- a/worlds/stardew_valley/test/mods/TestModFish.py +++ /dev/null @@ -1,226 +0,0 @@ -import unittest -from typing import Set - -from ...data.fish_data import get_fish_for_mods -from ...mods.mod_data import ModNames -from ...strings.fish_names import Fish, SVEFish - -no_mods: Set[str] = set() -sve: Set[str] = {ModNames.sve} - - -class TestGetFishForMods(unittest.TestCase): - - def test_no_mods_all_vanilla_fish(self): - all_fish = get_fish_for_mods(no_mods) - fish_names = {fish.name for fish in all_fish} - - self.assertIn(Fish.albacore, fish_names) - self.assertIn(Fish.anchovy, fish_names) - self.assertIn(Fish.blue_discus, fish_names) - self.assertIn(Fish.bream, fish_names) - self.assertIn(Fish.bullhead, fish_names) - self.assertIn(Fish.carp, fish_names) - self.assertIn(Fish.catfish, fish_names) - self.assertIn(Fish.chub, fish_names) - self.assertIn(Fish.dorado, fish_names) - self.assertIn(Fish.eel, fish_names) - self.assertIn(Fish.flounder, fish_names) - self.assertIn(Fish.ghostfish, fish_names) - self.assertIn(Fish.halibut, fish_names) - self.assertIn(Fish.herring, fish_names) - self.assertIn(Fish.ice_pip, fish_names) - self.assertIn(Fish.largemouth_bass, fish_names) - self.assertIn(Fish.lava_eel, fish_names) - self.assertIn(Fish.lingcod, fish_names) - self.assertIn(Fish.lionfish, fish_names) - self.assertIn(Fish.midnight_carp, fish_names) - self.assertIn(Fish.octopus, fish_names) - self.assertIn(Fish.perch, fish_names) - self.assertIn(Fish.pike, fish_names) - self.assertIn(Fish.pufferfish, fish_names) - self.assertIn(Fish.rainbow_trout, fish_names) - self.assertIn(Fish.red_mullet, fish_names) - self.assertIn(Fish.red_snapper, fish_names) - self.assertIn(Fish.salmon, fish_names) - self.assertIn(Fish.sandfish, fish_names) - self.assertIn(Fish.sardine, fish_names) - self.assertIn(Fish.scorpion_carp, fish_names) - self.assertIn(Fish.sea_cucumber, fish_names) - self.assertIn(Fish.shad, fish_names) - self.assertIn(Fish.slimejack, fish_names) - self.assertIn(Fish.smallmouth_bass, fish_names) - self.assertIn(Fish.squid, fish_names) - self.assertIn(Fish.stingray, fish_names) - self.assertIn(Fish.stonefish, fish_names) - self.assertIn(Fish.sturgeon, fish_names) - self.assertIn(Fish.sunfish, fish_names) - self.assertIn(Fish.super_cucumber, fish_names) - self.assertIn(Fish.tiger_trout, fish_names) - self.assertIn(Fish.tilapia, fish_names) - self.assertIn(Fish.tuna, fish_names) - self.assertIn(Fish.void_salmon, fish_names) - self.assertIn(Fish.walleye, fish_names) - self.assertIn(Fish.woodskip, fish_names) - self.assertIn(Fish.blob_fish, fish_names) - self.assertIn(Fish.midnight_squid, fish_names) - self.assertIn(Fish.spook_fish, fish_names) - self.assertIn(Fish.angler, fish_names) - self.assertIn(Fish.crimsonfish, fish_names) - self.assertIn(Fish.glacierfish, fish_names) - self.assertIn(Fish.legend, fish_names) - self.assertIn(Fish.mutant_carp, fish_names) - self.assertIn(Fish.ms_angler, fish_names) - self.assertIn(Fish.son_of_crimsonfish, fish_names) - self.assertIn(Fish.glacierfish_jr, fish_names) - self.assertIn(Fish.legend_ii, fish_names) - self.assertIn(Fish.radioactive_carp, fish_names) - self.assertIn(Fish.clam, fish_names) - self.assertIn(Fish.cockle, fish_names) - self.assertIn(Fish.crab, fish_names) - self.assertIn(Fish.crayfish, fish_names) - self.assertIn(Fish.lobster, fish_names) - self.assertIn(Fish.mussel, fish_names) - self.assertIn(Fish.oyster, fish_names) - self.assertIn(Fish.periwinkle, fish_names) - self.assertIn(Fish.shrimp, fish_names) - self.assertIn(Fish.snail, fish_names) - - def test_no_mods_no_sve_fish(self): - all_fish = get_fish_for_mods(no_mods) - fish_names = {fish.name for fish in all_fish} - - self.assertNotIn(SVEFish.baby_lunaloo, fish_names) - self.assertNotIn(SVEFish.bonefish, fish_names) - self.assertNotIn(SVEFish.bull_trout, fish_names) - self.assertNotIn(SVEFish.butterfish, fish_names) - self.assertNotIn(SVEFish.clownfish, fish_names) - self.assertNotIn(SVEFish.daggerfish, fish_names) - self.assertNotIn(SVEFish.frog, fish_names) - self.assertNotIn(SVEFish.gemfish, fish_names) - self.assertNotIn(SVEFish.goldenfish, fish_names) - self.assertNotIn(SVEFish.grass_carp, fish_names) - self.assertNotIn(SVEFish.king_salmon, fish_names) - self.assertNotIn(SVEFish.kittyfish, fish_names) - self.assertNotIn(SVEFish.lunaloo, fish_names) - self.assertNotIn(SVEFish.meteor_carp, fish_names) - self.assertNotIn(SVEFish.minnow, fish_names) - self.assertNotIn(SVEFish.puppyfish, fish_names) - self.assertNotIn(SVEFish.radioactive_bass, fish_names) - self.assertNotIn(SVEFish.seahorse, fish_names) - self.assertNotIn(SVEFish.shiny_lunaloo, fish_names) - self.assertNotIn(SVEFish.snatcher_worm, fish_names) - self.assertNotIn(SVEFish.starfish, fish_names) - self.assertNotIn(SVEFish.torpedo_trout, fish_names) - self.assertNotIn(SVEFish.undeadfish, fish_names) - self.assertNotIn(SVEFish.void_eel, fish_names) - self.assertNotIn(SVEFish.water_grub, fish_names) - self.assertNotIn(SVEFish.sea_sponge, fish_names) - self.assertNotIn(SVEFish.dulse_seaweed, fish_names) - - def test_sve_all_vanilla_fish(self): - all_fish = get_fish_for_mods(no_mods) - fish_names = {fish.name for fish in all_fish} - - self.assertIn(Fish.albacore, fish_names) - self.assertIn(Fish.anchovy, fish_names) - self.assertIn(Fish.blue_discus, fish_names) - self.assertIn(Fish.bream, fish_names) - self.assertIn(Fish.bullhead, fish_names) - self.assertIn(Fish.carp, fish_names) - self.assertIn(Fish.catfish, fish_names) - self.assertIn(Fish.chub, fish_names) - self.assertIn(Fish.dorado, fish_names) - self.assertIn(Fish.eel, fish_names) - self.assertIn(Fish.flounder, fish_names) - self.assertIn(Fish.ghostfish, fish_names) - self.assertIn(Fish.halibut, fish_names) - self.assertIn(Fish.herring, fish_names) - self.assertIn(Fish.ice_pip, fish_names) - self.assertIn(Fish.largemouth_bass, fish_names) - self.assertIn(Fish.lava_eel, fish_names) - self.assertIn(Fish.lingcod, fish_names) - self.assertIn(Fish.lionfish, fish_names) - self.assertIn(Fish.midnight_carp, fish_names) - self.assertIn(Fish.octopus, fish_names) - self.assertIn(Fish.perch, fish_names) - self.assertIn(Fish.pike, fish_names) - self.assertIn(Fish.pufferfish, fish_names) - self.assertIn(Fish.rainbow_trout, fish_names) - self.assertIn(Fish.red_mullet, fish_names) - self.assertIn(Fish.red_snapper, fish_names) - self.assertIn(Fish.salmon, fish_names) - self.assertIn(Fish.sandfish, fish_names) - self.assertIn(Fish.sardine, fish_names) - self.assertIn(Fish.scorpion_carp, fish_names) - self.assertIn(Fish.sea_cucumber, fish_names) - self.assertIn(Fish.shad, fish_names) - self.assertIn(Fish.slimejack, fish_names) - self.assertIn(Fish.smallmouth_bass, fish_names) - self.assertIn(Fish.squid, fish_names) - self.assertIn(Fish.stingray, fish_names) - self.assertIn(Fish.stonefish, fish_names) - self.assertIn(Fish.sturgeon, fish_names) - self.assertIn(Fish.sunfish, fish_names) - self.assertIn(Fish.super_cucumber, fish_names) - self.assertIn(Fish.tiger_trout, fish_names) - self.assertIn(Fish.tilapia, fish_names) - self.assertIn(Fish.tuna, fish_names) - self.assertIn(Fish.void_salmon, fish_names) - self.assertIn(Fish.walleye, fish_names) - self.assertIn(Fish.woodskip, fish_names) - self.assertIn(Fish.blob_fish, fish_names) - self.assertIn(Fish.midnight_squid, fish_names) - self.assertIn(Fish.spook_fish, fish_names) - self.assertIn(Fish.angler, fish_names) - self.assertIn(Fish.crimsonfish, fish_names) - self.assertIn(Fish.glacierfish, fish_names) - self.assertIn(Fish.legend, fish_names) - self.assertIn(Fish.mutant_carp, fish_names) - self.assertIn(Fish.ms_angler, fish_names) - self.assertIn(Fish.son_of_crimsonfish, fish_names) - self.assertIn(Fish.glacierfish_jr, fish_names) - self.assertIn(Fish.legend_ii, fish_names) - self.assertIn(Fish.radioactive_carp, fish_names) - self.assertIn(Fish.clam, fish_names) - self.assertIn(Fish.cockle, fish_names) - self.assertIn(Fish.crab, fish_names) - self.assertIn(Fish.crayfish, fish_names) - self.assertIn(Fish.lobster, fish_names) - self.assertIn(Fish.mussel, fish_names) - self.assertIn(Fish.oyster, fish_names) - self.assertIn(Fish.periwinkle, fish_names) - self.assertIn(Fish.shrimp, fish_names) - self.assertIn(Fish.snail, fish_names) - - def test_sve_has_sve_fish(self): - all_fish = get_fish_for_mods(sve) - fish_names = {fish.name for fish in all_fish} - - self.assertIn(SVEFish.baby_lunaloo, fish_names) - self.assertIn(SVEFish.bonefish, fish_names) - self.assertIn(SVEFish.bull_trout, fish_names) - self.assertIn(SVEFish.butterfish, fish_names) - self.assertIn(SVEFish.clownfish, fish_names) - self.assertIn(SVEFish.daggerfish, fish_names) - self.assertIn(SVEFish.frog, fish_names) - self.assertIn(SVEFish.gemfish, fish_names) - self.assertIn(SVEFish.goldenfish, fish_names) - self.assertIn(SVEFish.grass_carp, fish_names) - self.assertIn(SVEFish.king_salmon, fish_names) - self.assertIn(SVEFish.kittyfish, fish_names) - self.assertIn(SVEFish.lunaloo, fish_names) - self.assertIn(SVEFish.meteor_carp, fish_names) - self.assertIn(SVEFish.minnow, fish_names) - self.assertIn(SVEFish.puppyfish, fish_names) - self.assertIn(SVEFish.radioactive_bass, fish_names) - self.assertIn(SVEFish.seahorse, fish_names) - self.assertIn(SVEFish.shiny_lunaloo, fish_names) - self.assertIn(SVEFish.snatcher_worm, fish_names) - self.assertIn(SVEFish.starfish, fish_names) - self.assertIn(SVEFish.torpedo_trout, fish_names) - self.assertIn(SVEFish.undeadfish, fish_names) - self.assertIn(SVEFish.void_eel, fish_names) - self.assertIn(SVEFish.water_grub, fish_names) - self.assertIn(SVEFish.sea_sponge, fish_names) - self.assertIn(SVEFish.dulse_seaweed, fish_names) diff --git a/worlds/stardew_valley/test/mods/TestModVillagers.py b/worlds/stardew_valley/test/mods/TestModVillagers.py deleted file mode 100644 index 3be437c3f737..000000000000 --- a/worlds/stardew_valley/test/mods/TestModVillagers.py +++ /dev/null @@ -1,132 +0,0 @@ -import unittest -from typing import Set - -from ...data.villagers_data import get_villagers_for_mods -from ...mods.mod_data import ModNames -from ...strings.villager_names import NPC, ModNPC - -no_mods: Set[str] = set() -sve: Set[str] = {ModNames.sve} - - -class TestGetVillagersForMods(unittest.TestCase): - - def test_no_mods_all_vanilla_villagers(self): - villagers = get_villagers_for_mods(no_mods) - villager_names = {villager.name for villager in villagers} - - self.assertIn(NPC.alex, villager_names) - self.assertIn(NPC.elliott, villager_names) - self.assertIn(NPC.harvey, villager_names) - self.assertIn(NPC.sam, villager_names) - self.assertIn(NPC.sebastian, villager_names) - self.assertIn(NPC.shane, villager_names) - self.assertIn(NPC.abigail, villager_names) - self.assertIn(NPC.emily, villager_names) - self.assertIn(NPC.haley, villager_names) - self.assertIn(NPC.leah, villager_names) - self.assertIn(NPC.maru, villager_names) - self.assertIn(NPC.penny, villager_names) - self.assertIn(NPC.caroline, villager_names) - self.assertIn(NPC.clint, villager_names) - self.assertIn(NPC.demetrius, villager_names) - self.assertIn(NPC.dwarf, villager_names) - self.assertIn(NPC.evelyn, villager_names) - self.assertIn(NPC.george, villager_names) - self.assertIn(NPC.gus, villager_names) - self.assertIn(NPC.jas, villager_names) - self.assertIn(NPC.jodi, villager_names) - self.assertIn(NPC.kent, villager_names) - self.assertIn(NPC.krobus, villager_names) - self.assertIn(NPC.leo, villager_names) - self.assertIn(NPC.lewis, villager_names) - self.assertIn(NPC.linus, villager_names) - self.assertIn(NPC.marnie, villager_names) - self.assertIn(NPC.pam, villager_names) - self.assertIn(NPC.pierre, villager_names) - self.assertIn(NPC.robin, villager_names) - self.assertIn(NPC.sandy, villager_names) - self.assertIn(NPC.vincent, villager_names) - self.assertIn(NPC.willy, villager_names) - self.assertIn(NPC.wizard, villager_names) - - def test_no_mods_no_mod_villagers(self): - villagers = get_villagers_for_mods(no_mods) - villager_names = {villager.name for villager in villagers} - - self.assertNotIn(ModNPC.alec, villager_names) - self.assertNotIn(ModNPC.ayeisha, villager_names) - self.assertNotIn(ModNPC.delores, villager_names) - self.assertNotIn(ModNPC.eugene, villager_names) - self.assertNotIn(ModNPC.jasper, villager_names) - self.assertNotIn(ModNPC.juna, villager_names) - self.assertNotIn(ModNPC.mr_ginger, villager_names) - self.assertNotIn(ModNPC.riley, villager_names) - self.assertNotIn(ModNPC.shiko, villager_names) - self.assertNotIn(ModNPC.wellwick, villager_names) - self.assertNotIn(ModNPC.yoba, villager_names) - self.assertNotIn(ModNPC.lance, villager_names) - self.assertNotIn(ModNPC.apples, villager_names) - self.assertNotIn(ModNPC.claire, villager_names) - self.assertNotIn(ModNPC.olivia, villager_names) - self.assertNotIn(ModNPC.sophia, villager_names) - self.assertNotIn(ModNPC.victor, villager_names) - self.assertNotIn(ModNPC.andy, villager_names) - self.assertNotIn(ModNPC.gunther, villager_names) - self.assertNotIn(ModNPC.martin, villager_names) - self.assertNotIn(ModNPC.marlon, villager_names) - self.assertNotIn(ModNPC.morgan, villager_names) - self.assertNotIn(ModNPC.morris, villager_names) - self.assertNotIn(ModNPC.scarlett, villager_names) - self.assertNotIn(ModNPC.susan, villager_names) - self.assertNotIn(ModNPC.goblin, villager_names) - self.assertNotIn(ModNPC.alecto, villager_names) - - def test_sve_has_sve_villagers(self): - villagers = get_villagers_for_mods(sve) - villager_names = {villager.name for villager in villagers} - - self.assertIn(ModNPC.lance, villager_names) - self.assertIn(ModNPC.apples, villager_names) - self.assertIn(ModNPC.claire, villager_names) - self.assertIn(ModNPC.olivia, villager_names) - self.assertIn(ModNPC.sophia, villager_names) - self.assertIn(ModNPC.victor, villager_names) - self.assertIn(ModNPC.andy, villager_names) - self.assertIn(ModNPC.gunther, villager_names) - self.assertIn(ModNPC.martin, villager_names) - self.assertIn(ModNPC.marlon, villager_names) - self.assertIn(ModNPC.morgan, villager_names) - self.assertIn(ModNPC.morris, villager_names) - self.assertIn(ModNPC.scarlett, villager_names) - self.assertIn(ModNPC.susan, villager_names) - - def test_sve_has_no_other_mod_villagers(self): - villagers = get_villagers_for_mods(sve) - villager_names = {villager.name for villager in villagers} - - self.assertNotIn(ModNPC.alec, villager_names) - self.assertNotIn(ModNPC.ayeisha, villager_names) - self.assertNotIn(ModNPC.delores, villager_names) - self.assertNotIn(ModNPC.eugene, villager_names) - self.assertNotIn(ModNPC.jasper, villager_names) - self.assertNotIn(ModNPC.juna, villager_names) - self.assertNotIn(ModNPC.mr_ginger, villager_names) - self.assertNotIn(ModNPC.riley, villager_names) - self.assertNotIn(ModNPC.shiko, villager_names) - self.assertNotIn(ModNPC.wellwick, villager_names) - self.assertNotIn(ModNPC.yoba, villager_names) - self.assertNotIn(ModNPC.goblin, villager_names) - self.assertNotIn(ModNPC.alecto, villager_names) - - def test_no_mods_wizard_is_not_bachelor(self): - villagers = get_villagers_for_mods(no_mods) - villagers_by_name = {villager.name: villager for villager in villagers} - self.assertFalse(villagers_by_name[NPC.wizard].bachelor) - self.assertEqual(villagers_by_name[NPC.wizard].mod_name, ModNames.vanilla) - - def test_sve_wizard_is_bachelor(self): - villagers = get_villagers_for_mods(sve) - villagers_by_name = {villager.name: villager for villager in villagers} - self.assertTrue(villagers_by_name[NPC.wizard].bachelor) - self.assertEqual(villagers_by_name[NPC.wizard].mod_name, ModNames.sve) diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 57bca5f25645..5e7e9d4143bd 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -1,48 +1,48 @@ import random from BaseClasses import get_seed -from .. import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods, complete_options_with_default +from .. import SVTestBase, SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, complete_options_with_default, solo_multiworld from ..assertion import ModAssertMixin, WorldAssertMixin from ... import items, Group, ItemClassification from ... import options from ...items import items_by_group -from ...mods.mod_data import all_mods +from ...options import SkillProgression, Walnutsanity from ...regions import RandomizationFlag, randomize_connections, create_final_connections_and_regions class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase): def test_given_single_mods_when_generate_then_basic_checks(self): - for mod in all_mods: - with self.solo_world_sub_test(f"Mod: {mod}", {options.Mods: mod}, dirty_state=True) as (multi_world, _): + for mod in options.Mods.valid_keys: + with self.solo_world_sub_test(f"Mod: {mod}", {options.Mods: mod}) as (multi_world, _): self.assert_basic_checks(multi_world) self.assert_stray_mod_items(mod, multi_world) def test_given_mod_names_when_generate_paired_with_entrance_randomizer_then_basic_checks(self): for option in options.EntranceRandomization.options: - for mod in all_mods: + for mod in options.Mods.valid_keys: world_options = { options.EntranceRandomization.internal_name: options.EntranceRandomization.options[option], options.Mods: mod } - with self.solo_world_sub_test(f"entrance_randomization: {option}, Mod: {mod}", world_options, dirty_state=True) as (multi_world, _): + with self.solo_world_sub_test(f"entrance_randomization: {option}, Mod: {mod}", world_options) as (multi_world, _): self.assert_basic_checks(multi_world) self.assert_stray_mod_items(mod, multi_world) def test_allsanity_all_mods_when_generate_then_basic_checks(self): - with self.solo_world_sub_test(world_options=allsanity_options_with_mods(), dirty_state=True) as (multi_world, _): + with self.solo_world_sub_test(world_options=allsanity_mods_6_x_x()) as (multi_world, _): self.assert_basic_checks(multi_world) def test_allsanity_all_mods_exclude_island_when_generate_then_basic_checks(self): - world_options = allsanity_options_with_mods() + world_options = allsanity_mods_6_x_x() world_options.update({options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true}) - with self.solo_world_sub_test(world_options=world_options, dirty_state=True) as (multi_world, _): + with self.solo_world_sub_test(world_options=world_options) as (multi_world, _): self.assert_basic_checks(multi_world) class TestBaseLocationDependencies(SVTestBase): options = { - options.Mods.internal_name: all_mods, + options.Mods.internal_name: frozenset(options.Mods.valid_keys), options.ToolProgression.internal_name: options.ToolProgression.option_progressive, options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized } @@ -50,13 +50,17 @@ class TestBaseLocationDependencies(SVTestBase): class TestBaseItemGeneration(SVTestBase): options = { - options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, + options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, options.Shipsanity.internal_name: options.Shipsanity.option_everything, options.Chefsanity.internal_name: options.Chefsanity.option_all, options.Craftsanity.internal_name: options.Craftsanity.option_all, - options.Mods.internal_name: all_mods + options.Booksanity.internal_name: options.Booksanity.option_all, + Walnutsanity.internal_name: Walnutsanity.preset_all, + options.Mods.internal_name: frozenset(options.Mods.valid_keys) } def test_all_progression_items_are_added_to_the_pool(self): @@ -78,13 +82,15 @@ def test_all_progression_items_are_added_to_the_pool(self): class TestNoGingerIslandModItemGeneration(SVTestBase): options = { - options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, + options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, options.Shipsanity.internal_name: options.Shipsanity.option_everything, options.Chefsanity.internal_name: options.Chefsanity.option_all, options.Craftsanity.internal_name: options.Craftsanity.option_all, - options.Mods.internal_name: all_mods + options.Booksanity.internal_name: options.Booksanity.option_all, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.Mods.internal_name: frozenset(options.Mods.valid_keys) } def test_all_progression_items_except_island_are_added_to_the_pool(self): @@ -112,11 +118,13 @@ class TestModEntranceRando(SVTestCase): def test_mod_entrance_randomization(self): for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), (options.EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION), + (options.EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS), (options.EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]: sv_options = complete_options_with_default({ options.EntranceRandomization.internal_name: option, options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, - options.Mods.internal_name: all_mods + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + options.Mods.internal_name: frozenset(options.Mods.valid_keys) }) seed = get_seed() rand = random.Random(seed) @@ -143,11 +151,11 @@ def test_given_traps_when_generate_then_all_traps_in_pool(self): if value == "no_traps": continue - world_options = allsanity_options_without_mods() - world_options.update({options.TrapItems.internal_name: options.TrapItems.options[value], options.Mods: "Magic"}) - multi_world = setup_solo_multiworld(world_options) - trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups] - multiworld_items = [item.name for item in multi_world.get_items()] - for item in trap_items: - with self.subTest(f"Option: {value}, Item: {item}"): - self.assertIn(item, multiworld_items) + world_options = allsanity_no_mods_6_x_x() + world_options.update({options.TrapItems.internal_name: options.TrapItems.options[value], options.Mods.internal_name: "Magic"}) + with solo_multiworld(world_options) as (multi_world, _): + trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups] + multiworld_items = [item.name for item in multi_world.get_items()] + for item in trap_items: + with self.subTest(f"Option: {value}, Item: {item}"): + self.assertIn(item, multiworld_items) diff --git a/worlds/stardew_valley/test/performance/TestPerformance.py b/worlds/stardew_valley/test/performance/TestPerformance.py index 0d453942c35f..b5ad0cae66c6 100644 --- a/worlds/stardew_valley/test/performance/TestPerformance.py +++ b/worlds/stardew_valley/test/performance/TestPerformance.py @@ -8,13 +8,10 @@ from BaseClasses import get_seed from Fill import distribute_items_restrictive, balance_multiworld_progression from worlds import AutoWorld -from .. import SVTestCase, minimal_locations_maximal_items, setup_multiworld, default_4_x_x_options, \ - allsanity_4_x_x_options_without_mods, default_options, allsanity_options_without_mods, allsanity_options_with_mods +from .. import SVTestCase, minimal_locations_maximal_items, setup_multiworld, default_6_x_x, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x -assert default_4_x_x_options -assert allsanity_4_x_x_options_without_mods -assert default_options -assert allsanity_options_without_mods +assert default_6_x_x +assert allsanity_no_mods_6_x_x default_number_generations = 25 acceptable_deviation = 4 @@ -45,8 +42,6 @@ class SVPerformanceTestCase(SVTestCase): acceptable_time_per_player: float results: List[PerformanceResults] - # Set False to run tests that take long - skip_performance_tests: bool = True # Set False to not call the fill in the tests""" skip_fill: bool = True # Set True to print results as CSV""" @@ -54,10 +49,11 @@ class SVPerformanceTestCase(SVTestCase): @classmethod def setUpClass(cls) -> None: - super().setUpClass() performance_tests_key = "performance" - if performance_tests_key in os.environ: - cls.skip_performance_tests = not bool(os.environ[performance_tests_key]) + if performance_tests_key not in os.environ or os.environ[performance_tests_key] != "True": + raise unittest.SkipTest("Performance tests disabled") + + super().setUpClass() fill_tests_key = "fill" if fill_tests_key in os.environ: @@ -102,7 +98,7 @@ def performance_test_multiworld(self, options): acceptable_average_time = self.acceptable_time_per_player * amount_of_players total_time = 0 all_times = [] - seeds = [get_seed() for _ in range(self.number_generations)] if not self.fixed_seed else [87876703343494157696] * self.number_generations + seeds = [get_seed() for _ in range(self.number_generations)] if not self.fixed_seed else [85635032403287291967] * self.number_generations for i, seed in enumerate(seeds): with self.subTest(f"Seed: {seed}"): @@ -139,38 +135,26 @@ def size_name(number_players): class TestDefaultOptions(SVPerformanceTestCase): acceptable_time_per_player = 2 - options = default_options() + options = default_6_x_x() results = [] def test_solo(self): - if self.skip_performance_tests: - return - number_players = 1 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_duo(self): - if self.skip_performance_tests: - return - number_players = 2 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_5_player(self): - if self.skip_performance_tests: - return - number_players = 5 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @unittest.skip def test_10_player(self): - if self.skip_performance_tests: - return - number_players = 10 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @@ -182,33 +166,21 @@ class TestMinLocationMaxItems(SVPerformanceTestCase): results = [] def test_solo(self): - if self.skip_performance_tests: - return - number_players = 1 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_duo(self): - if self.skip_performance_tests: - return - number_players = 2 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_5_player(self): - if self.skip_performance_tests: - return - number_players = 5 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_10_player(self): - if self.skip_performance_tests: - return - number_players = 10 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @@ -216,39 +188,27 @@ def test_10_player(self): class TestAllsanityWithoutMods(SVPerformanceTestCase): acceptable_time_per_player = 10 - options = allsanity_options_without_mods() + options = allsanity_no_mods_6_x_x() results = [] def test_solo(self): - if self.skip_performance_tests: - return - number_players = 1 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_duo(self): - if self.skip_performance_tests: - return - number_players = 2 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @unittest.skip def test_5_player(self): - if self.skip_performance_tests: - return - number_players = 5 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @unittest.skip def test_10_player(self): - if self.skip_performance_tests: - return - number_players = 10 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @@ -256,21 +216,17 @@ def test_10_player(self): class TestAllsanityWithMods(SVPerformanceTestCase): acceptable_time_per_player = 25 - options = allsanity_options_with_mods() + options = allsanity_mods_6_x_x() results = [] + @unittest.skip def test_solo(self): - if self.skip_performance_tests: - return - number_players = 1 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) + @unittest.skip def test_duo(self): - if self.skip_performance_tests: - return - number_players = 2 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) diff --git a/worlds/stardew_valley/test/rules/TestArcades.py b/worlds/stardew_valley/test/rules/TestArcades.py new file mode 100644 index 000000000000..fb62a456378a --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestArcades.py @@ -0,0 +1,97 @@ +from ... import options +from ...test import SVTestBase + + +class TestArcadeMachinesLogic(SVTestBase): + options = { + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, + } + + def test_prairie_king(self): + self.assertFalse(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + + boots = self.create_item("JotPK: Progressive Boots") + gun = self.create_item("JotPK: Progressive Gun") + ammo = self.create_item("JotPK: Progressive Ammo") + life = self.create_item("JotPK: Extra Life") + drop = self.create_item("JotPK: Increased Drop Rate") + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + self.remove(boots) + self.remove(gun) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(boots, event=True) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + self.remove(boots) + self.remove(boots) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(life, event=True) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + self.remove(boots) + self.remove(gun) + self.remove(ammo) + self.remove(life) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(life, event=True) + self.multiworld.state.collect(drop, event=True) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + self.remove(boots) + self.remove(gun) + self.remove(gun) + self.remove(ammo) + self.remove(ammo) + self.remove(life) + self.remove(drop) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(life, event=True) + self.multiworld.state.collect(drop, event=True) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + self.remove(boots) + self.remove(boots) + self.remove(gun) + self.remove(gun) + self.remove(gun) + self.remove(gun) + self.remove(ammo) + self.remove(ammo) + self.remove(ammo) + self.remove(life) + self.remove(drop) diff --git a/worlds/stardew_valley/test/rules/TestBuildings.py b/worlds/stardew_valley/test/rules/TestBuildings.py new file mode 100644 index 000000000000..b00e4138a195 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestBuildings.py @@ -0,0 +1,62 @@ +from ...options import BuildingProgression, FarmType +from ...test import SVTestBase + + +class TestBuildingLogic(SVTestBase): + options = { + FarmType.internal_name: FarmType.option_standard, + BuildingProgression.internal_name: BuildingProgression.option_progressive, + } + + def test_coop_blueprint(self): + self.assertFalse(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state)) + + self.collect_lots_of_money() + self.assertTrue(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state)) + + def test_big_coop_blueprint(self): + big_coop_blueprint_rule = self.world.logic.region.can_reach_location("Big Coop Blueprint") + self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") + + self.collect_lots_of_money() + self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") + + self.multiworld.state.collect(self.create_item("Can Construct Buildings"), event=True) + self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") + + self.multiworld.state.collect(self.create_item("Progressive Coop"), event=False) + self.assertTrue(big_coop_blueprint_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") + + def test_deluxe_coop_blueprint(self): + self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) + + self.collect_lots_of_money() + self.multiworld.state.collect(self.create_item("Can Construct Buildings"), event=True) + self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) + + self.multiworld.state.collect(self.create_item("Progressive Coop"), event=True) + self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) + + self.multiworld.state.collect(self.create_item("Progressive Coop"), event=True) + self.assertTrue(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) + + def test_big_shed_blueprint(self): + big_shed_rule = self.world.logic.region.can_reach_location("Big Shed Blueprint") + self.assertFalse(big_shed_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") + + self.collect_lots_of_money() + self.assertFalse(big_shed_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") + + self.multiworld.state.collect(self.create_item("Can Construct Buildings"), event=True) + self.assertFalse(big_shed_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") + + self.multiworld.state.collect(self.create_item("Progressive Shed"), event=True) + self.assertTrue(big_shed_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") diff --git a/worlds/stardew_valley/test/rules/TestBundles.py b/worlds/stardew_valley/test/rules/TestBundles.py new file mode 100644 index 000000000000..25d4c70b2ab0 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestBundles.py @@ -0,0 +1,66 @@ +from ... import options +from ...options import BundleRandomization +from ...strings.bundle_names import BundleName +from ...test import SVTestBase + + +class TestBundlesLogic(SVTestBase): + options = { + options.BundleRandomization: BundleRandomization.option_vanilla, + options.BundlePrice: options.BundlePrice.default, + } + + def test_vault_2500g_bundle(self): + self.assertFalse(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state)) + + self.collect_lots_of_money() + self.assertTrue(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state)) + + +class TestRemixedBundlesLogic(SVTestBase): + options = { + options.BundleRandomization: BundleRandomization.option_remixed, + options.BundlePrice: options.BundlePrice.default, + options.BundlePlando: frozenset({BundleName.sticky}) + } + + def test_sticky_bundle_has_grind_rules(self): + self.assertFalse(self.world.logic.region.can_reach_location("Sticky Bundle")(self.multiworld.state)) + + self.collect_all_the_money() + self.assertTrue(self.world.logic.region.can_reach_location("Sticky Bundle")(self.multiworld.state)) + + +class TestRaccoonBundlesLogic(SVTestBase): + options = { + options.BundleRandomization: BundleRandomization.option_vanilla, + options.BundlePrice: options.BundlePrice.option_normal, + options.Craftsanity: options.Craftsanity.option_all, + } + seed = 1234 # Magic seed that does what I want. Might need to get changed if we change the randomness behavior of raccoon bundles + + def test_raccoon_bundles_rely_on_previous_ones(self): + # The first raccoon bundle is a fishing one + raccoon_rule_1 = self.world.logic.region.can_reach_location("Raccoon Request 1") + + # The 3th raccoon bundle is a foraging one + raccoon_rule_3 = self.world.logic.region.can_reach_location("Raccoon Request 3") + self.collect("Progressive Raccoon", 6) + self.collect("Progressive Mine Elevator", 24) + self.collect("Mining Level", 12) + self.collect("Combat Level", 12) + self.collect("Progressive Axe", 4) + self.collect("Progressive Pickaxe", 4) + self.collect("Progressive Weapon", 4) + self.collect("Dehydrator Recipe") + self.collect("Mushroom Boxes") + self.collect("Progressive Fishing Rod", 4) + self.collect("Fishing Level", 10) + + self.assertFalse(raccoon_rule_1(self.multiworld.state)) + self.assertFalse(raccoon_rule_3(self.multiworld.state)) + + self.collect("Fish Smoker Recipe") + + self.assertTrue(raccoon_rule_1(self.multiworld.state)) + self.assertTrue(raccoon_rule_3(self.multiworld.state)) diff --git a/worlds/stardew_valley/test/rules/TestCookingRecipes.py b/worlds/stardew_valley/test/rules/TestCookingRecipes.py new file mode 100644 index 000000000000..81a91d1e7482 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestCookingRecipes.py @@ -0,0 +1,83 @@ +from ... import options +from ...options import BuildingProgression, ExcludeGingerIsland, Chefsanity +from ...test import SVTestBase + + +class TestRecipeLearnLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.Cooksanity.internal_name: options.Cooksanity.option_all, + Chefsanity.internal_name: Chefsanity.option_none, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_learn_qos_recipe(self): + location = "Cook Radish Salad" + rule = self.world.logic.region.can_reach_location(location) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Progressive House"), event=False) + self.multiworld.state.collect(self.create_item("Radish Seeds"), event=False) + self.multiworld.state.collect(self.create_item("Spring"), event=False) + self.multiworld.state.collect(self.create_item("Summer"), event=False) + self.collect_lots_of_money() + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("The Queen of Sauce"), event=False) + self.assert_rule_true(rule, self.multiworld.state) + + +class TestRecipeReceiveLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.Cooksanity.internal_name: options.Cooksanity.option_all, + Chefsanity.internal_name: Chefsanity.option_all, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_learn_qos_recipe(self): + location = "Cook Radish Salad" + rule = self.world.logic.region.can_reach_location(location) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Progressive House"), event=False) + self.multiworld.state.collect(self.create_item("Radish Seeds"), event=False) + self.multiworld.state.collect(self.create_item("Summer"), event=False) + self.collect_lots_of_money() + self.assert_rule_false(rule, self.multiworld.state) + + spring = self.create_item("Spring") + qos = self.create_item("The Queen of Sauce") + self.multiworld.state.collect(spring, event=False) + self.multiworld.state.collect(qos, event=False) + self.assert_rule_false(rule, self.multiworld.state) + self.multiworld.state.remove(spring) + self.multiworld.state.remove(qos) + + self.multiworld.state.collect(self.create_item("Radish Salad Recipe"), event=False) + self.assert_rule_true(rule, self.multiworld.state) + + def test_get_chefsanity_check_recipe(self): + location = "Radish Salad Recipe" + rule = self.world.logic.region.can_reach_location(location) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Spring"), event=False) + self.collect_lots_of_money() + self.assert_rule_false(rule, self.multiworld.state) + + seeds = self.create_item("Radish Seeds") + summer = self.create_item("Summer") + house = self.create_item("Progressive House") + self.multiworld.state.collect(seeds, event=False) + self.multiworld.state.collect(summer, event=False) + self.multiworld.state.collect(house, event=False) + self.assert_rule_false(rule, self.multiworld.state) + self.multiworld.state.remove(seeds) + self.multiworld.state.remove(summer) + self.multiworld.state.remove(house) + + self.multiworld.state.collect(self.create_item("The Queen of Sauce"), event=False) + self.assert_rule_true(rule, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/TestCraftingRecipes.py b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py new file mode 100644 index 000000000000..59d41f6a63d6 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py @@ -0,0 +1,123 @@ +from ... import options +from ...data.craftable_data import all_crafting_recipes_by_name +from ...options import BuildingProgression, ExcludeGingerIsland, Craftsanity, SeasonRandomization +from ...test import SVTestBase + + +class TestCraftsanityLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + Craftsanity.internal_name: Craftsanity.option_all, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_craft_recipe(self): + location = "Craft Marble Brazier" + rule = self.world.logic.region.can_reach_location(location) + self.collect([self.create_item("Progressive Pickaxe")] * 4) + self.collect([self.create_item("Progressive Fishing Rod")] * 4) + self.collect([self.create_item("Progressive Sword")] * 4) + self.collect([self.create_item("Progressive Mine Elevator")] * 24) + self.collect([self.create_item("Mining Level")] * 10) + self.collect([self.create_item("Combat Level")] * 10) + self.collect([self.create_item("Fishing Level")] * 10) + self.collect_all_the_money() + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Marble Brazier Recipe"), event=False) + self.assert_rule_true(rule, self.multiworld.state) + + def test_can_learn_crafting_recipe(self): + location = "Marble Brazier Recipe" + rule = self.world.logic.region.can_reach_location(location) + self.assert_rule_false(rule, self.multiworld.state) + + self.collect_lots_of_money() + self.assert_rule_true(rule, self.multiworld.state) + + def test_can_craft_festival_recipe(self): + recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] + self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), event=False) + self.multiworld.state.collect(self.create_item("Torch Recipe"), event=False) + self.collect_lots_of_money() + rule = self.world.logic.crafting.can_craft(recipe) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Fall"), event=False) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), event=False) + self.assert_rule_true(rule, self.multiworld.state) + + +class TestCraftsanityWithFestivalsLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, + Craftsanity.internal_name: Craftsanity.option_all, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_craft_festival_recipe(self): + recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] + self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), event=False) + self.multiworld.state.collect(self.create_item("Fall"), event=False) + self.collect_lots_of_money() + rule = self.world.logic.crafting.can_craft(recipe) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), event=False) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Torch Recipe"), event=False) + self.assert_rule_true(rule, self.multiworld.state) + + +class TestNoCraftsanityLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + SeasonRandomization.internal_name: SeasonRandomization.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, + Craftsanity.internal_name: Craftsanity.option_none, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_craft_recipe(self): + recipe = all_crafting_recipes_by_name["Wood Floor"] + rule = self.world.logic.crafting.can_craft(recipe) + self.assert_rule_true(rule, self.multiworld.state) + + def test_can_craft_festival_recipe(self): + recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] + self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), event=False) + self.collect_lots_of_money() + rule = self.world.logic.crafting.can_craft(recipe) + result = rule(self.multiworld.state) + self.assertFalse(result) + + self.collect([self.create_item("Progressive Season")] * 2) + self.assert_rule_true(rule, self.multiworld.state) + + +class TestNoCraftsanityWithFestivalsLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, + Craftsanity.internal_name: Craftsanity.option_none, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_craft_festival_recipe(self): + recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] + self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), event=False) + self.multiworld.state.collect(self.create_item("Fall"), event=False) + self.collect_lots_of_money() + rule = self.world.logic.crafting.can_craft(recipe) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), event=False) + self.assert_rule_true(rule, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/TestDonations.py b/worlds/stardew_valley/test/rules/TestDonations.py new file mode 100644 index 000000000000..84ceac50ff5a --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestDonations.py @@ -0,0 +1,73 @@ +from ... import options +from ...locations import locations_by_tag, LocationTags, location_table +from ...strings.entrance_names import Entrance +from ...strings.region_names import Region +from ...test import SVTestBase + + +class TestDonationLogicAll(SVTestBase): + options = { + options.Museumsanity.internal_name: options.Museumsanity.option_all + } + + def test_cannot_make_any_donation_without_museum_access(self): + railroad_item = "Railroad Boulder Removed" + swap_museum_and_bathhouse(self.multiworld, self.player) + self.collect_all_except(railroad_item) + + for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: + self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + self.multiworld.state.collect(self.create_item(railroad_item), event=False) + + for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: + self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + +class TestDonationLogicRandomized(SVTestBase): + options = { + options.Museumsanity.internal_name: options.Museumsanity.option_randomized + } + + def test_cannot_make_any_donation_without_museum_access(self): + railroad_item = "Railroad Boulder Removed" + swap_museum_and_bathhouse(self.multiworld, self.player) + self.collect_all_except(railroad_item) + donation_locations = [location for location in self.get_real_locations() if + LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags] + + for donation in donation_locations: + self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + self.multiworld.state.collect(self.create_item(railroad_item), event=False) + + for donation in donation_locations: + self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + +class TestDonationLogicMilestones(SVTestBase): + options = { + options.Museumsanity.internal_name: options.Museumsanity.option_milestones + } + + def test_cannot_make_any_donation_without_museum_access(self): + railroad_item = "Railroad Boulder Removed" + swap_museum_and_bathhouse(self.multiworld, self.player) + self.collect_all_except(railroad_item) + + for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: + self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + self.multiworld.state.collect(self.create_item(railroad_item), event=False) + + for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: + self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + +def swap_museum_and_bathhouse(multiworld, player): + museum_region = multiworld.get_region(Region.museum, player) + bathhouse_region = multiworld.get_region(Region.bathhouse_entrance, player) + museum_entrance = multiworld.get_entrance(Entrance.town_to_museum, player) + bathhouse_entrance = multiworld.get_entrance(Entrance.enter_bathhouse_entrance, player) + museum_entrance.connect(bathhouse_region) + bathhouse_entrance.connect(museum_region) diff --git a/worlds/stardew_valley/test/rules/TestFriendship.py b/worlds/stardew_valley/test/rules/TestFriendship.py new file mode 100644 index 000000000000..43c5e55c7fca --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestFriendship.py @@ -0,0 +1,58 @@ +from ...options import SeasonRandomization, Friendsanity, FriendsanityHeartSize +from ...test import SVTestBase + + +class TestFriendsanityDatingRules(SVTestBase): + options = { + SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, + FriendsanityHeartSize.internal_name: 3 + } + + def test_earning_dating_heart_requires_dating(self): + self.collect_all_the_money() + self.multiworld.state.collect(self.create_item("Fall"), event=False) + self.multiworld.state.collect(self.create_item("Beach Bridge"), event=False) + self.multiworld.state.collect(self.create_item("Progressive House"), event=False) + for i in range(3): + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), event=False) + self.multiworld.state.collect(self.create_item("Progressive Weapon"), event=False) + self.multiworld.state.collect(self.create_item("Progressive Axe"), event=False) + self.multiworld.state.collect(self.create_item("Progressive Barn"), event=False) + for i in range(10): + self.multiworld.state.collect(self.create_item("Foraging Level"), event=False) + self.multiworld.state.collect(self.create_item("Farming Level"), event=False) + self.multiworld.state.collect(self.create_item("Mining Level"), event=False) + self.multiworld.state.collect(self.create_item("Combat Level"), event=False) + self.multiworld.state.collect(self.create_item("Progressive Mine Elevator"), event=False) + self.multiworld.state.collect(self.create_item("Progressive Mine Elevator"), event=False) + + npc = "Abigail" + heart_name = f"{npc} <3" + step = 3 + + self.assert_can_reach_heart_up_to(npc, 3, step) + self.multiworld.state.collect(self.create_item(heart_name), event=False) + self.assert_can_reach_heart_up_to(npc, 6, step) + self.multiworld.state.collect(self.create_item(heart_name), event=False) + self.assert_can_reach_heart_up_to(npc, 8, step) + self.multiworld.state.collect(self.create_item(heart_name), event=False) + self.assert_can_reach_heart_up_to(npc, 10, step) + self.multiworld.state.collect(self.create_item(heart_name), event=False) + self.assert_can_reach_heart_up_to(npc, 14, step) + + def assert_can_reach_heart_up_to(self, npc: str, max_reachable: int, step: int): + prefix = "Friendsanity: " + suffix = " <3" + for i in range(1, max_reachable + 1): + if i % step != 0 and i != 14: + continue + location = f"{prefix}{npc} {i}{suffix}" + can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state) + self.assertTrue(can_reach, f"Should be able to earn relationship up to {i} hearts") + for i in range(max_reachable + 1, 14 + 1): + if i % step != 0 and i != 14: + continue + location = f"{prefix}{npc} {i}{suffix}" + can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state) + self.assertFalse(can_reach, f"Should not be able to earn relationship up to {i} hearts") diff --git a/worlds/stardew_valley/test/rules/TestMuseum.py b/worlds/stardew_valley/test/rules/TestMuseum.py new file mode 100644 index 000000000000..35dad8f43ebc --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestMuseum.py @@ -0,0 +1,16 @@ +from collections import Counter + +from ...options import Museumsanity +from .. import SVTestBase + + +class TestMuseumMilestones(SVTestBase): + options = { + Museumsanity.internal_name: Museumsanity.option_milestones + } + + def test_50_milestone(self): + self.multiworld.state.prog_items = {1: Counter()} + + milestone_rule = self.world.logic.museum.can_find_museum_items(50) + self.assert_rule_false(milestone_rule, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/TestShipping.py b/worlds/stardew_valley/test/rules/TestShipping.py new file mode 100644 index 000000000000..378933b7d75d --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestShipping.py @@ -0,0 +1,82 @@ +from ...locations import LocationTags, location_table +from ...options import BuildingProgression, Shipsanity +from ...test import SVTestBase + + +class TestShipsanityNone(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_none + } + + def test_no_shipsanity_locations(self): + for location in self.get_real_locations(): + self.assertFalse("Shipsanity" in location.name) + self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags) + + +class TestShipsanityCrops(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_crops + } + + def test_only_crop_shipsanity_locations(self): + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: + self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags) + + +class TestShipsanityFish(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_fish + } + + def test_only_fish_shipsanity_locations(self): + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: + self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) + + +class TestShipsanityFullShipment(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_full_shipment + } + + def test_only_full_shipment_shipsanity_locations(self): + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: + self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags) + self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) + + +class TestShipsanityFullShipmentWithFish(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish + } + + def test_only_full_shipment_and_fish_shipsanity_locations(self): + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: + self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or + LocationTags.SHIPSANITY_FISH in location_table[location.name].tags) + + +class TestShipsanityEverything(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_everything, + BuildingProgression.internal_name: BuildingProgression.option_progressive + } + + def test_all_shipsanity_locations_require_shipping_bin(self): + bin_name = "Shipping Bin" + self.collect_all_except(bin_name) + shipsanity_locations = [location for location in self.get_real_locations() if + LocationTags.SHIPSANITY in location_table[location.name].tags] + bin_item = self.create_item(bin_name) + for location in shipsanity_locations: + with self.subTest(location.name): + self.remove(bin_item) + self.assertFalse(self.world.logic.region.can_reach_location(location.name)(self.multiworld.state)) + self.multiworld.state.collect(bin_item, event=False) + shipsanity_rule = self.world.logic.region.can_reach_location(location.name) + self.assert_rule_true(shipsanity_rule, self.multiworld.state) + self.remove(bin_item) diff --git a/worlds/stardew_valley/test/rules/TestSkills.py b/worlds/stardew_valley/test/rules/TestSkills.py new file mode 100644 index 000000000000..1c6874f31529 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestSkills.py @@ -0,0 +1,40 @@ +from ... import HasProgressionPercent +from ...options import ToolProgression, SkillProgression, Mods +from ...strings.skill_names import all_skills +from ...test import SVTestBase + + +class TestVanillaSkillLogicSimplification(SVTestBase): + options = { + SkillProgression.internal_name: SkillProgression.option_vanilla, + ToolProgression.internal_name: ToolProgression.option_progressive, + } + + def test_skill_logic_has_level_only_uses_one_has_progression_percent(self): + rule = self.multiworld.worlds[1].logic.skill.has_level("Farming", 8) + self.assertEqual(1, sum(1 for i in rule.current_rules if type(i) == HasProgressionPercent)) + + +class TestAllSkillsRequirePrevious(SVTestBase): + options = { + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + Mods.internal_name: frozenset(Mods.valid_keys), + } + + def test_all_skill_levels_require_previous_level(self): + for skill in all_skills: + self.collect_everything() + self.remove_by_name(f"{skill} Level") + for level in range(1, 11): + location_name = f"Level {level} {skill}" + with self.subTest(location_name): + can_reach = self.can_reach_location(location_name) + if level > 1: + self.assertFalse(can_reach) + self.collect(f"{skill} Level") + can_reach = self.can_reach_location(location_name) + self.assertTrue(can_reach) + self.multiworld.state = self.original_state.copy() + + + diff --git a/worlds/stardew_valley/test/rules/TestStateRules.py b/worlds/stardew_valley/test/rules/TestStateRules.py new file mode 100644 index 000000000000..4f53b9a7f536 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestStateRules.py @@ -0,0 +1,12 @@ +import unittest + +from BaseClasses import ItemClassification +from ...test import solo_multiworld + + +class TestHasProgressionPercent(unittest.TestCase): + def test_max_item_amount_is_full_collection(self): + # Not caching because it fails too often for some reason + with solo_multiworld(world_caching=False) as (multiworld, world): + progression_item_count = sum(1 for i in multiworld.get_items() if ItemClassification.progression in i.classification) + self.assertEqual(world.total_progression_items, progression_item_count - 1) # -1 to skip Victory diff --git a/worlds/stardew_valley/test/rules/TestTools.py b/worlds/stardew_valley/test/rules/TestTools.py new file mode 100644 index 000000000000..a1fb152812c8 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestTools.py @@ -0,0 +1,141 @@ +from collections import Counter + +from .. import SVTestBase +from ... import Event, options +from ...options import ToolProgression, SeasonRandomization +from ...strings.entrance_names import Entrance +from ...strings.region_names import Region +from ...strings.tool_names import Tool, ToolMaterial + + +class TestProgressiveToolsLogic(SVTestBase): + options = { + ToolProgression.internal_name: ToolProgression.option_progressive, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + } + + def test_sturgeon(self): + self.multiworld.state.prog_items = {1: Counter()} + + sturgeon_rule = self.world.logic.has("Sturgeon") + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + summer = self.create_item("Summer") + self.multiworld.state.collect(summer, event=False) + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + fishing_rod = self.create_item("Progressive Fishing Rod") + self.multiworld.state.collect(fishing_rod, event=False) + self.multiworld.state.collect(fishing_rod, event=False) + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + fishing_level = self.create_item("Fishing Level") + self.multiworld.state.collect(fishing_level, event=False) + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + self.multiworld.state.collect(fishing_level, event=False) + self.multiworld.state.collect(fishing_level, event=False) + self.multiworld.state.collect(fishing_level, event=False) + self.multiworld.state.collect(fishing_level, event=False) + self.multiworld.state.collect(fishing_level, event=False) + self.assert_rule_true(sturgeon_rule, self.multiworld.state) + + self.remove(summer) + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + winter = self.create_item("Winter") + self.multiworld.state.collect(winter, event=False) + self.assert_rule_true(sturgeon_rule, self.multiworld.state) + + self.remove(fishing_rod) + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + def test_old_master_cannoli(self): + self.multiworld.state.prog_items = {1: Counter()} + + self.multiworld.state.collect(self.create_item("Progressive Axe"), event=False) + self.multiworld.state.collect(self.create_item("Progressive Axe"), event=False) + self.multiworld.state.collect(self.create_item("Summer"), event=False) + self.collect_lots_of_money() + + rule = self.world.logic.region.can_reach_location("Old Master Cannoli") + self.assert_rule_false(rule, self.multiworld.state) + + fall = self.create_item("Fall") + self.multiworld.state.collect(fall, event=False) + self.assert_rule_false(rule, self.multiworld.state) + + tuesday = self.create_item("Traveling Merchant: Tuesday") + self.multiworld.state.collect(tuesday, event=False) + self.assert_rule_false(rule, self.multiworld.state) + + rare_seed = self.create_item("Rare Seed") + self.multiworld.state.collect(rare_seed, event=False) + self.assert_rule_true(rule, self.multiworld.state) + + self.remove(fall) + self.remove(self.create_item(Event.fall_farming)) + self.assert_rule_false(rule, self.multiworld.state) + self.remove(tuesday) + + green_house = self.create_item("Greenhouse") + self.collect(self.create_item(Event.fall_farming)) + self.multiworld.state.collect(green_house, event=False) + self.assert_rule_false(rule, self.multiworld.state) + + friday = self.create_item("Traveling Merchant: Friday") + self.multiworld.state.collect(friday, event=False) + self.assertTrue(self.multiworld.get_location("Old Master Cannoli", 1).access_rule(self.multiworld.state)) + + self.remove(green_house) + self.remove(self.create_item(Event.fall_farming)) + self.assert_rule_false(rule, self.multiworld.state) + self.remove(friday) + + +class TestToolVanillaRequiresBlacksmith(SVTestBase): + options = { + options.EntranceRandomization: options.EntranceRandomization.option_buildings, + options.ToolProgression: options.ToolProgression.option_vanilla, + } + seed = 4111845104987680262 + + # Seed is hardcoded to make sure the ER is a valid roll that actually lock the blacksmith behind the Railroad Boulder Removed. + + def test_cannot_get_any_tool_without_blacksmith_access(self): + railroad_item = "Railroad Boulder Removed" + place_region_at_entrance(self.multiworld, self.player, Region.blacksmith, Entrance.enter_bathhouse_entrance) + self.collect_all_except(railroad_item) + + for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: + for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: + self.assert_rule_false(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) + + self.multiworld.state.collect(self.create_item(railroad_item), event=False) + + for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: + for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: + self.assert_rule_true(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) + + def test_cannot_get_fishing_rod_without_willy_access(self): + railroad_item = "Railroad Boulder Removed" + place_region_at_entrance(self.multiworld, self.player, Region.fish_shop, Entrance.enter_bathhouse_entrance) + self.collect_all_except(railroad_item) + + for fishing_rod_level in [3, 4]: + self.assert_rule_false(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) + + self.multiworld.state.collect(self.create_item(railroad_item), event=False) + + for fishing_rod_level in [3, 4]: + self.assert_rule_true(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) + + +def place_region_at_entrance(multiworld, player, region, entrance): + region_to_place = multiworld.get_region(region, player) + entrance_to_place_region = multiworld.get_entrance(entrance, player) + + entrance_to_switch = region_to_place.entrances[0] + region_to_switch = entrance_to_place_region.connected_region + entrance_to_switch.connect(region_to_switch) + entrance_to_place_region.connect(region_to_place) diff --git a/worlds/stardew_valley/test/rules/TestWeapons.py b/worlds/stardew_valley/test/rules/TestWeapons.py new file mode 100644 index 000000000000..77887f8eca0c --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestWeapons.py @@ -0,0 +1,75 @@ +from ... import options +from ...options import ToolProgression +from ...test import SVTestBase + + +class TestWeaponsLogic(SVTestBase): + options = { + ToolProgression.internal_name: ToolProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + } + + def test_mine(self): + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), event=True) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), event=True) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), event=True) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), event=True) + self.multiworld.state.collect(self.create_item("Progressive House"), event=True) + self.collect([self.create_item("Combat Level")] * 10) + self.collect([self.create_item("Mining Level")] * 10) + self.collect([self.create_item("Progressive Mine Elevator")] * 24) + self.multiworld.state.collect(self.create_item("Bus Repair"), event=True) + self.multiworld.state.collect(self.create_item("Skull Key"), event=True) + + self.GiveItemAndCheckReachableMine("Progressive Sword", 1) + self.GiveItemAndCheckReachableMine("Progressive Dagger", 1) + self.GiveItemAndCheckReachableMine("Progressive Club", 1) + + self.GiveItemAndCheckReachableMine("Progressive Sword", 2) + self.GiveItemAndCheckReachableMine("Progressive Dagger", 2) + self.GiveItemAndCheckReachableMine("Progressive Club", 2) + + self.GiveItemAndCheckReachableMine("Progressive Sword", 3) + self.GiveItemAndCheckReachableMine("Progressive Dagger", 3) + self.GiveItemAndCheckReachableMine("Progressive Club", 3) + + self.GiveItemAndCheckReachableMine("Progressive Sword", 4) + self.GiveItemAndCheckReachableMine("Progressive Dagger", 4) + self.GiveItemAndCheckReachableMine("Progressive Club", 4) + + self.GiveItemAndCheckReachableMine("Progressive Sword", 5) + self.GiveItemAndCheckReachableMine("Progressive Dagger", 5) + self.GiveItemAndCheckReachableMine("Progressive Club", 5) + + def GiveItemAndCheckReachableMine(self, item_name: str, reachable_level: int): + item = self.multiworld.create_item(item_name, self.player) + self.multiworld.state.collect(item, event=True) + rule = self.world.logic.mine.can_mine_in_the_mines_floor_1_40() + if reachable_level > 0: + self.assert_rule_true(rule, self.multiworld.state) + else: + self.assert_rule_false(rule, self.multiworld.state) + + rule = self.world.logic.mine.can_mine_in_the_mines_floor_41_80() + if reachable_level > 1: + self.assert_rule_true(rule, self.multiworld.state) + else: + self.assert_rule_false(rule, self.multiworld.state) + + rule = self.world.logic.mine.can_mine_in_the_mines_floor_81_120() + if reachable_level > 2: + self.assert_rule_true(rule, self.multiworld.state) + else: + self.assert_rule_false(rule, self.multiworld.state) + + rule = self.world.logic.mine.can_mine_in_the_skull_cavern() + if reachable_level > 3: + self.assert_rule_true(rule, self.multiworld.state) + else: + self.assert_rule_false(rule, self.multiworld.state) + + rule = self.world.logic.ability.can_mine_perfectly_in_the_skull_cavern() + if reachable_level > 4: + self.assert_rule_true(rule, self.multiworld.state) + else: + self.assert_rule_false(rule, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/__init__.py b/worlds/stardew_valley/test/rules/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/test/script/__init__.py b/worlds/stardew_valley/test/script/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/test/script/benchmark_locations.py b/worlds/stardew_valley/test/script/benchmark_locations.py new file mode 100644 index 000000000000..04553e39968e --- /dev/null +++ b/worlds/stardew_valley/test/script/benchmark_locations.py @@ -0,0 +1,140 @@ +""" +Copy of the script in test/benchmark, adapted to Stardew Valley. + +Run with `python -m worlds.stardew_valley.test.script.benchmark_locations --options minimal_locations_maximal_items` +""" + +import argparse +import collections +import gc +import logging +import os +import sys +import time +import typing + +from BaseClasses import CollectionState, Location +from Utils import init_logging +from worlds.stardew_valley.stardew_rule.rule_explain import explain +from ... import test + + +def run_locations_benchmark(): + init_logging("Benchmark Runner") + logger = logging.getLogger("Benchmark") + + class BenchmarkRunner: + gen_steps: typing.Tuple[str, ...] = ( + "generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") + rule_iterations: int = 100_000 + + @staticmethod + def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str: + return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top)) + + def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float: + with TimeIt(f"{test_location.game} {self.rule_iterations} " + f"runs of {test_location}.access_rule({state_name})", logger) as t: + for _ in range(self.rule_iterations): + test_location.access_rule(state) + # if time is taken to disentangle complex ref chains, + # this time should be attributed to the rule. + gc.collect() + return t.dif + + def main(self): + game = "Stardew Valley" + summary_data: typing.Dict[str, collections.Counter[str]] = { + "empty_state": collections.Counter(), + "all_state": collections.Counter(), + } + try: + parser = argparse.ArgumentParser() + parser.add_argument('--options', help="Define the option set to use, from the preset in test/__init__.py .", type=str, required=True) + parser.add_argument('--seed', help="Define the seed to use.", type=int, required=True) + parser.add_argument('--location', help="Define the specific location to benchmark.", type=str, default=None) + parser.add_argument('--state', help="Define the state in which the location will be benchmarked.", type=str, default=None) + args = parser.parse_args() + options_set = args.options + options = getattr(test, options_set)() + seed = args.seed + location = args.location + state = args.state + + multiworld = test.setup_solo_multiworld(options, seed) + gc.collect() + + if location: + locations = [multiworld.get_location(location, 1)] + else: + locations = sorted(multiworld.get_unfilled_locations()) + + all_state = multiworld.get_all_state(False) + for location in locations: + if state != "all_state": + time_taken = self.location_test(location, multiworld.state, "empty_state") + summary_data["empty_state"][location.name] = time_taken + + if state != "empty_state": + time_taken = self.location_test(location, all_state, "all_state") + summary_data["all_state"][location.name] = time_taken + + total_empty_state = sum(summary_data["empty_state"].values()) + total_all_state = sum(summary_data["all_state"].values()) + + logger.info(f"{game} took {total_empty_state / len(locations):.4f} " + f"seconds per location in empty_state and {total_all_state / len(locations):.4f} " + f"in all_state. (all times summed for {self.rule_iterations} runs.)") + logger.info(f"Top times in empty_state:\n" + f"{self.format_times_from_counter(summary_data['empty_state'])}") + logger.info(f"Top times in all_state:\n" + f"{self.format_times_from_counter(summary_data['all_state'])}") + + if len(locations) == 1: + logger.info(str(explain(locations[0].access_rule, all_state, False))) + + except Exception as e: + logger.exception(e) + + runner = BenchmarkRunner() + runner.main() + + +class TimeIt: + def __init__(self, name: str, time_logger=None): + self.name = name + self.logger = time_logger + self.timer = None + self.end_timer = None + + def __enter__(self): + self.timer = time.perf_counter() + return self + + @property + def dif(self): + return self.end_timer - self.timer + + def __exit__(self, exc_type, exc_val, exc_tb): + if not self.end_timer: + self.end_timer = time.perf_counter() + if self.logger: + self.logger.info(f"{self.dif:.4f} seconds in {self.name}.") + + +def change_home(): + """Allow scripts to run from "this" folder.""" + old_home = os.path.dirname(__file__) + sys.path.remove(old_home) + new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) + os.chdir(new_home) + sys.path.append(new_home) + # fallback to local import + sys.path.append(old_home) + + from Utils import local_path + local_path.cached_path = new_home + + +if __name__ == "__main__": + run_locations_benchmark() diff --git a/worlds/stardew_valley/test/stability/StabilityOutputScript.py b/worlds/stardew_valley/test/stability/StabilityOutputScript.py index baf17dde8423..4b31011d9f49 100644 --- a/worlds/stardew_valley/test/stability/StabilityOutputScript.py +++ b/worlds/stardew_valley/test/stability/StabilityOutputScript.py @@ -1,7 +1,7 @@ import argparse import json -from ...test import setup_solo_multiworld, allsanity_options_with_mods +from ...test import setup_solo_multiworld, allsanity_mods_6_x_x if __name__ == "__main__": parser = argparse.ArgumentParser() @@ -11,7 +11,7 @@ seed = args.seed multi_world = setup_solo_multiworld( - allsanity_options_with_mods(), + allsanity_mods_6_x_x(), seed=seed ) diff --git a/worlds/stardew_valley/test/stability/TestStability.py b/worlds/stardew_valley/test/stability/TestStability.py index 48cd663cb301..aaa8b331846a 100644 --- a/worlds/stardew_valley/test/stability/TestStability.py +++ b/worlds/stardew_valley/test/stability/TestStability.py @@ -2,10 +2,14 @@ import re import subprocess import sys +import unittest from BaseClasses import get_seed from .. import SVTestCase +# There seems to be 4 bytes that appear at random at the end of the output, breaking the json... I don't know where they came from. +BYTES_TO_REMOVE = 4 + # at 0x102ca98a0> lambda_regex = re.compile(r"^ at (.*)>$") # Python 3.10.2\r\n @@ -18,16 +22,16 @@ class TestGenerationIsStable(SVTestCase): def test_all_locations_and_items_are_the_same_between_two_generations(self): if self.skip_long_tests: - return + raise unittest.SkipTest("Long tests disabled") # seed = get_seed(33778671150797368040) # troubleshooting seed - seed = get_seed() + seed = get_seed(74716545478307145559) output_a = subprocess.check_output([sys.executable, '-m', 'worlds.stardew_valley.test.stability.StabilityOutputScript', '--seed', str(seed)]) output_b = subprocess.check_output([sys.executable, '-m', 'worlds.stardew_valley.test.stability.StabilityOutputScript', '--seed', str(seed)]) - result_a = json.loads(output_a) - result_b = json.loads(output_b) + result_a = json.loads(output_a[:-BYTES_TO_REMOVE]) + result_b = json.loads(output_b[:-BYTES_TO_REMOVE]) for i, ((room_a, bundles_a), (room_b, bundles_b)) in enumerate(zip(result_a["bundles"].items(), result_b["bundles"].items())): self.assertEqual(room_a, room_b, f"Bundle rooms at index {i} is different between both executions. Seed={seed}") diff --git a/worlds/subnautica/test/__init__.py b/worlds/subnautica/test/__init__.py index 69f0b6c7cdc9..c0e0a593a9f5 100644 --- a/worlds/subnautica/test/__init__.py +++ b/worlds/subnautica/test/__init__.py @@ -15,7 +15,7 @@ def testIDRange(self): self.assertGreater(self.scancutoff, id) def testGroupAssociation(self): - from worlds.subnautica import items + from .. import items for item_id, item_data in items.item_table.items(): if item_data.type == items.ItemType.group: with self.subTest(item=item_data.name): diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 624208da3a0b..f63193e6aeef 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -156,9 +156,6 @@ def create_item(self, name: str) -> TunicItem: return TunicItem(name, item_data.classification, self.item_name_to_id[name], self.player) def create_items(self) -> None: - keys_behind_bosses = self.options.keys_behind_bosses - hexagon_quest = self.options.hexagon_quest - sword_progression = self.options.sword_progression tunic_items: List[TunicItem] = [] self.slot_data_items = [] @@ -172,7 +169,7 @@ def create_items(self) -> None: if self.options.start_with_sword: self.multiworld.push_precollected(self.create_item("Sword")) - if sword_progression: + if self.options.sword_progression: items_to_create["Stick"] = 0 items_to_create["Sword"] = 0 else: @@ -189,9 +186,9 @@ def create_items(self) -> None: self.slot_data_items.append(laurels) items_to_create["Hero's Laurels"] = 0 - if keys_behind_bosses: + if self.options.keys_behind_bosses: for rgb_hexagon, location in hexagon_locations.items(): - hex_item = self.create_item(gold_hexagon if hexagon_quest else rgb_hexagon) + hex_item = self.create_item(gold_hexagon if self.options.hexagon_quest else rgb_hexagon) self.multiworld.get_location(location, self.player).place_locked_item(hex_item) self.slot_data_items.append(hex_item) items_to_create[rgb_hexagon] = 0 @@ -203,7 +200,7 @@ def create_items(self) -> None: # Remove filler to make room for other items def remove_filler(amount: int) -> None: - for _ in range(0, amount): + for _ in range(amount): if not available_filler: fill = "Fool Trap" else: @@ -222,7 +219,7 @@ def remove_filler(amount: int) -> None: ladder_count += 1 remove_filler(ladder_count) - if hexagon_quest: + if self.options.hexagon_quest: # Calculate number of hexagons in item pool hexagon_goal = self.options.hexagon_goal extra_hexagons = self.options.extra_hexagon_percentage @@ -238,6 +235,18 @@ def remove_filler(amount: int) -> None: remove_filler(items_to_create[gold_hexagon]) + for hero_relic in item_name_groups["Hero Relics"]: + relic_item = TunicItem(hero_relic, ItemClassification.useful, self.item_name_to_id[hero_relic], self.player) + tunic_items.append(relic_item) + items_to_create[hero_relic] = 0 + + if not self.options.ability_shuffling: + for page in item_name_groups["Abilities"]: + if items_to_create[page] > 0: + page_item = TunicItem(page, ItemClassification.useful, self.item_name_to_id[page], self.player) + tunic_items.append(page_item) + items_to_create[page] = 0 + if self.options.maskless: mask_item = TunicItem("Scavenger Mask", ItemClassification.useful, self.item_name_to_id["Scavenger Mask"], self.player) tunic_items.append(mask_item) @@ -249,7 +258,7 @@ def remove_filler(amount: int) -> None: items_to_create["Lantern"] = 0 for item, quantity in items_to_create.items(): - for i in range(0, quantity): + for _ in range(quantity): tunic_item: TunicItem = self.create_item(item) if item in slot_data_item_names: self.slot_data_items.append(tunic_item) @@ -300,10 +309,10 @@ def create_regions(self) -> None: def set_rules(self) -> None: if self.options.entrance_rando or self.options.shuffle_ladders: - set_er_location_rules(self, self.ability_unlocks) + set_er_location_rules(self) else: - set_region_rules(self, self.ability_unlocks) - set_location_rules(self, self.ability_unlocks) + set_region_rules(self) + set_location_rules(self) def get_filler_item_name(self) -> str: return self.random.choice(filler_items) @@ -378,7 +387,7 @@ def fill_slot_data(self) -> Dict[str, Any]: if start_item in slot_data_item_names: if start_item not in slot_data: slot_data[start_item] = [] - for i in range(0, self.options.start_inventory_from_pool[start_item]): + for _ in range(self.options.start_inventory_from_pool[start_item]): slot_data[start_item].extend(["Your Pocket", self.player]) for plando_item in self.multiworld.plando_items[self.player]: diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md index 29a7255ea771..27df4ce38be4 100644 --- a/worlds/tunic/docs/en_TUNIC.md +++ b/worlds/tunic/docs/en_TUNIC.md @@ -53,6 +53,7 @@ You can also use the Universal Tracker (by Faris and qwint) to find a complete l ## What should I know regarding logic? In general: - Nighttime is not considered in logic. Every check in the game is obtainable during the day. +- Bushes are not considered in logic. It is assumed that the player will find a way past them, whether it is with a sword, a bomb, fire, luring an enemy, etc. There is also an option in the in-game randomizer settings menu to clear some of the early bushes. - The Cathedral is accessible during the day by using the Hero's Laurels to reach the Overworld fuse near the Swamp entrance. - The Secret Legend chest at the Cathedral can be obtained during the day by opening the Holy Cross door from the outside. @@ -81,8 +82,13 @@ Notes: - The Entrance Randomizer option must be enabled for it to work. - The `direction` field is not supported. Connections are always coupled. - For a list of entrance names, check `er_data.py` in the TUNIC world folder or generate a game with the Entrance Randomizer option enabled and check the spoiler log. -- There is no limit to the number of Shops hard-coded into place. +- There is no limit to the number of Shops you can plando. - If you have more than one shop in a scene, you may be wrong warped when exiting a shop. - If you have a shop in every scene, and you have an odd number of shops, it will error out. See the [Archipelago Plando Guide](../../../tutorial/Archipelago/plando/en) for more information on Plando and Connection Plando. + +## Is there anything else I should know? +You can go to [The TUNIC Randomizer Website](https://rando.tunic.run/) for a list of randomizer features as well as some helpful tips. +You can use the Fairy Seeking Spell (ULU RDR) to locate the nearest unchecked location. +You can use the Entrance Seeking Spell (RDR ULU) to locate the nearest unused entrance. diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index bbee212f5d5a..81e9d48b4afc 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -2,7 +2,6 @@ from worlds.generic.Rules import set_rule, forbid_item from .rules import has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage from .er_data import Portal -from .options import TunicOptions from BaseClasses import Region, CollectionState if TYPE_CHECKING: @@ -28,12 +27,11 @@ gold_hexagon = "Gold Questagon" -def has_ladder(ladder: str, state: CollectionState, player: int, options: TunicOptions): - return not options.shuffle_ladders or state.has(ladder, player) +def has_ladder(ladder: str, state: CollectionState, world: "TunicWorld") -> bool: + return not world.options.shuffle_ladders or state.has(ladder, world.player) -def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], regions: Dict[str, Region], - portal_pairs: Dict[Portal, Portal]) -> None: +def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_pairs: Dict[Portal, Portal]) -> None: player = world.player options = world.options @@ -43,16 +41,16 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re # Overworld regions["Overworld"].connect( connecting_region=regions["Overworld Holy Cross"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) # grapple on the west side, down the stairs from moss wall, across from ruined shop regions["Overworld"].connect( connecting_region=regions["Overworld Beach"], - rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) + rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) or state.has_any({laurels, grapple}, player)) regions["Overworld Beach"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) + rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) or state.has_any({laurels, grapple}, player)) regions["Overworld Beach"].connect( @@ -64,10 +62,10 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld Beach"].connect( connecting_region=regions["Overworld to Atoll Upper"], - rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, player, options)) + rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, world)) regions["Overworld to Atoll Upper"].connect( connecting_region=regions["Overworld Beach"], - rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, player, options)) + rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld to Atoll Upper"], @@ -84,14 +82,14 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld Belltower"].connect( connecting_region=regions["Overworld to West Garden Upper"], - rule=lambda state: has_ladder("Ladders to West Bell", state, player, options)) + rule=lambda state: has_ladder("Ladders to West Bell", state, world)) regions["Overworld to West Garden Upper"].connect( connecting_region=regions["Overworld Belltower"], - rule=lambda state: has_ladder("Ladders to West Bell", state, player, options)) + rule=lambda state: has_ladder("Ladders to West Bell", state, world)) regions["Overworld Belltower"].connect( connecting_region=regions["Overworld Belltower at Bell"], - rule=lambda state: has_ladder("Ladders to West Bell", state, player, options)) + rule=lambda state: has_ladder("Ladders to West Bell", state, world)) # long dong, do not make a reverse connection here or to belltower regions["Overworld above Patrol Cave"].connect( @@ -109,52 +107,52 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld"].connect( connecting_region=regions["After Ruined Passage"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world) + or has_ice_grapple_logic(True, state, world)) regions["After Ruined Passage"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options)) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world)) regions["Overworld"].connect( connecting_region=regions["Above Ruined Passage"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world) or state.has(laurels, player)) regions["Above Ruined Passage"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world) or state.has(laurels, player)) regions["After Ruined Passage"].connect( connecting_region=regions["Above Ruined Passage"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options)) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world)) regions["Above Ruined Passage"].connect( connecting_region=regions["After Ruined Passage"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options)) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world)) regions["Above Ruined Passage"].connect( connecting_region=regions["East Overworld"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world) + or has_ice_grapple_logic(True, state, world)) regions["East Overworld"].connect( connecting_region=regions["Above Ruined Passage"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world) or state.has(laurels, player)) # nmg: ice grapple the slimes, works both ways consistently regions["East Overworld"].connect( connecting_region=regions["After Ruined Passage"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) regions["After Ruined Passage"].connect( connecting_region=regions["East Overworld"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) regions["Overworld"].connect( connecting_region=regions["East Overworld"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world) + or has_ice_grapple_logic(True, state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options)) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld at Patrol Cave"]) @@ -164,35 +162,35 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld at Patrol Cave"].connect( connecting_region=regions["Overworld above Patrol Cave"], - rule=lambda state: has_ladder("Ladders near Patrol Cave", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) + or has_ice_grapple_logic(True, state, world)) regions["Overworld above Patrol Cave"].connect( connecting_region=regions["Overworld at Patrol Cave"], - rule=lambda state: has_ladder("Ladders near Patrol Cave", state, player, options)) + rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld above Patrol Cave"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world) or state.has(grapple, player)) regions["Overworld above Patrol Cave"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options)) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld above Patrol Cave"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world) + or has_ice_grapple_logic(True, state, world)) regions["Overworld above Patrol Cave"].connect( connecting_region=regions["East Overworld"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options)) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) regions["Overworld above Patrol Cave"].connect( connecting_region=regions["Upper Overworld"], - rule=lambda state: has_ladder("Ladders near Patrol Cave", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) + or has_ice_grapple_logic(True, state, world)) regions["Upper Overworld"].connect( connecting_region=regions["Overworld above Patrol Cave"], - rule=lambda state: has_ladder("Ladders near Patrol Cave", state, player, options) + rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) or state.has(grapple, player)) regions["Upper Overworld"].connect( @@ -204,18 +202,18 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Upper Overworld"].connect( connecting_region=regions["Overworld after Temple Rafters"], - rule=lambda state: has_ladder("Ladder near Temple Rafters", state, player, options)) + rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world)) regions["Overworld after Temple Rafters"].connect( connecting_region=regions["Upper Overworld"], - rule=lambda state: has_ladder("Ladder near Temple Rafters", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world) + or has_ice_grapple_logic(True, state, world)) regions["Overworld above Quarry Entrance"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Dark Tomb", state, player, options)) + rule=lambda state: has_ladder("Ladders near Dark Tomb", state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld above Quarry Entrance"], - rule=lambda state: has_ladder("Ladders near Dark Tomb", state, player, options)) + rule=lambda state: has_ladder("Ladders near Dark Tomb", state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld after Envoy"], @@ -230,18 +228,18 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld after Envoy"].connect( connecting_region=regions["Overworld Quarry Entry"], - rule=lambda state: has_ladder("Ladder to Quarry", state, player, options)) + rule=lambda state: has_ladder("Ladder to Quarry", state, world)) regions["Overworld Quarry Entry"].connect( connecting_region=regions["Overworld after Envoy"], - rule=lambda state: has_ladder("Ladder to Quarry", state, player, options)) + rule=lambda state: has_ladder("Ladder to Quarry", state, world)) # ice grapple through the gate regions["Overworld"].connect( connecting_region=regions["Overworld Quarry Entry"], - rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(False, state, world)) regions["Overworld Quarry Entry"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(False, state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld Swamp Upper Entry"], @@ -252,10 +250,10 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld"].connect( connecting_region=regions["Overworld Swamp Lower Entry"], - rule=lambda state: has_ladder("Ladder to Swamp", state, player, options)) + rule=lambda state: has_ladder("Ladder to Swamp", state, world)) regions["Overworld Swamp Lower Entry"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladder to Swamp", state, player, options)) + rule=lambda state: has_ladder("Ladder to Swamp", state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld Special Shop Entry"], @@ -266,41 +264,41 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld"].connect( connecting_region=regions["Overworld Well Ladder"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + rule=lambda state: has_ladder("Ladders in Well", state, world)) regions["Overworld Well Ladder"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + rule=lambda state: has_ladder("Ladders in Well", state, world)) # nmg: can ice grapple through the door regions["Overworld"].connect( connecting_region=regions["Overworld Old House Door"], rule=lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(False, state, world)) # not including ice grapple through this because it's very tedious to get an enemy here regions["Overworld"].connect( connecting_region=regions["Overworld Southeast Cross Door"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) regions["Overworld Southeast Cross Door"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) # not including ice grapple through this because we're not including it on the other door regions["Overworld"].connect( connecting_region=regions["Overworld Fountain Cross Door"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) regions["Overworld Fountain Cross Door"].connect( connecting_region=regions["Overworld"]) regions["Overworld"].connect( connecting_region=regions["Overworld Town Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Overworld Town Portal"].connect( connecting_region=regions["Overworld"]) regions["Overworld"].connect( connecting_region=regions["Overworld Spawn Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Overworld Spawn Portal"].connect( connecting_region=regions["Overworld"]) @@ -308,7 +306,7 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld"].connect( connecting_region=regions["Overworld Temple Door"], rule=lambda state: state.has_all({"Ring Eastern Bell", "Ring Western Bell"}, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(False, state, world)) regions["Overworld Temple Door"].connect( connecting_region=regions["Overworld above Patrol Cave"], @@ -316,17 +314,17 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld Tunnel Turret"].connect( connecting_region=regions["Overworld Beach"], - rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) + rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) or state.has(grapple, player)) regions["Overworld Beach"].connect( connecting_region=regions["Overworld Tunnel Turret"], - rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) + or has_ice_grapple_logic(True, state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld Tunnel Turret"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(True, state, world)) regions["Overworld Tunnel Turret"].connect( connecting_region=regions["Overworld"], rule=lambda state: state.has_any({grapple, laurels}, player)) @@ -368,7 +366,7 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Hourglass Cave"].connect( connecting_region=regions["Hourglass Cave Tower"], - rule=lambda state: has_ladder("Ladders in Hourglass Cave", state, player, options)) + rule=lambda state: has_ladder("Ladders in Hourglass Cave", state, world)) # East Forest regions["Forest Belltower Upper"].connect( @@ -376,32 +374,31 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Forest Belltower Main"].connect( connecting_region=regions["Forest Belltower Lower"], - rule=lambda state: has_ladder("Ladder to East Forest", state, player, options)) + rule=lambda state: has_ladder("Ladder to East Forest", state, world)) # nmg: ice grapple up to dance fox spot, and vice versa regions["East Forest"].connect( connecting_region=regions["East Forest Dance Fox Spot"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(True, state, world)) regions["East Forest Dance Fox Spot"].connect( connecting_region=regions["East Forest"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(True, state, world)) regions["East Forest"].connect( connecting_region=regions["East Forest Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["East Forest Portal"].connect( connecting_region=regions["East Forest"]) regions["East Forest"].connect( connecting_region=regions["Lower Forest"], - rule=lambda state: has_ladder("Ladders to Lower Forest", state, player, options) - or (state.has_all({grapple, fire_wand, ice_dagger}, player) # do ice slime, then go to the lower hook - and has_ability(state, player, icebolt, options, ability_unlocks))) + rule=lambda state: has_ladder("Ladders to Lower Forest", state, world) + or (state.has_all({grapple, fire_wand, ice_dagger}, player) and has_ability(icebolt, state, world))) regions["Lower Forest"].connect( connecting_region=regions["East Forest"], - rule=lambda state: has_ladder("Ladders to Lower Forest", state, player, options)) + rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)) regions["Guard House 1 East"].connect( connecting_region=regions["Guard House 1 West"]) @@ -411,16 +408,16 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Guard House 2 Upper"].connect( connecting_region=regions["Guard House 2 Lower"], - rule=lambda state: has_ladder("Ladders to Lower Forest", state, player, options)) + rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)) regions["Guard House 2 Lower"].connect( connecting_region=regions["Guard House 2 Upper"], - rule=lambda state: has_ladder("Ladders to Lower Forest", state, player, options)) + rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)) # nmg: ice grapple from upper grave path exit to the rest of it regions["Forest Grave Path Upper"].connect( connecting_region=regions["Forest Grave Path Main"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(True, state, world)) regions["Forest Grave Path Main"].connect( connecting_region=regions["Forest Grave Path Upper"], rule=lambda state: state.has(laurels, player)) @@ -430,23 +427,22 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re # nmg: ice grapple or laurels through the gate regions["Forest Grave Path by Grave"].connect( connecting_region=regions["Forest Grave Path Main"], - rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks) + rule=lambda state: has_ice_grapple_logic(False, state, world) or (state.has(laurels, player) and options.logic_rules)) regions["Forest Grave Path by Grave"].connect( connecting_region=regions["Forest Hero's Grave"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Forest Hero's Grave"].connect( connecting_region=regions["Forest Grave Path by Grave"]) # Beneath the Well and Dark Tomb - # don't need the ladder when entering at the ladder spot regions["Beneath the Well Ladder Exit"].connect( connecting_region=regions["Beneath the Well Front"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + rule=lambda state: has_ladder("Ladders in Well", state, world)) regions["Beneath the Well Front"].connect( connecting_region=regions["Beneath the Well Ladder Exit"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + rule=lambda state: has_ladder("Ladders in Well", state, world)) regions["Beneath the Well Front"].connect( connecting_region=regions["Beneath the Well Main"], @@ -457,10 +453,10 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Beneath the Well Main"].connect( connecting_region=regions["Beneath the Well Back"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + rule=lambda state: has_ladder("Ladders in Well", state, world)) regions["Beneath the Well Back"].connect( connecting_region=regions["Beneath the Well Main"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options) + rule=lambda state: has_ladder("Ladders in Well", state, world) and (has_stick(state, player) or state.has(fire_wand, player))) regions["Well Boss"].connect( @@ -472,22 +468,22 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Dark Tomb Entry Point"].connect( connecting_region=regions["Dark Tomb Upper"], - rule=lambda state: has_lantern(state, player, options)) + rule=lambda state: has_lantern(state, world)) regions["Dark Tomb Upper"].connect( connecting_region=regions["Dark Tomb Entry Point"]) regions["Dark Tomb Upper"].connect( connecting_region=regions["Dark Tomb Main"], - rule=lambda state: has_ladder("Ladder in Dark Tomb", state, player, options)) + rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)) regions["Dark Tomb Main"].connect( connecting_region=regions["Dark Tomb Upper"], - rule=lambda state: has_ladder("Ladder in Dark Tomb", state, player, options)) + rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)) regions["Dark Tomb Main"].connect( connecting_region=regions["Dark Tomb Dark Exit"]) regions["Dark Tomb Dark Exit"].connect( connecting_region=regions["Dark Tomb Main"], - rule=lambda state: has_lantern(state, player, options)) + rule=lambda state: has_lantern(state, world)) # West Garden regions["West Garden Laurels Exit Region"].connect( @@ -506,7 +502,7 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["West Garden"].connect( connecting_region=regions["West Garden Hero's Grave Region"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["West Garden Hero's Grave Region"].connect( connecting_region=regions["West Garden"]) @@ -515,29 +511,29 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re rule=lambda state: state.has(laurels, player)) regions["West Garden Portal Item"].connect( connecting_region=regions["West Garden Portal"], - rule=lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) # nmg: can ice grapple to and from the item behind the magic dagger house regions["West Garden Portal Item"].connect( connecting_region=regions["West Garden"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) regions["West Garden"].connect( connecting_region=regions["West Garden Portal Item"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) # Atoll and Frog's Domain # nmg: ice grapple the bird below the portal regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Lower Entry Area"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(True, state, world)) regions["Ruined Atoll Lower Entry Area"].connect( connecting_region=regions["Ruined Atoll"], rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Ladder Tops"], - rule=lambda state: has_ladder("Ladders in South Atoll", state, player, options)) + rule=lambda state: has_ladder("Ladders in South Atoll", state, world)) regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Frog Mouth"], @@ -548,48 +544,48 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Frog Eye"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Ruined Atoll Frog Eye"].connect( connecting_region=regions["Ruined Atoll"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Ruined Atoll Portal"].connect( connecting_region=regions["Ruined Atoll"]) regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Statue"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) - and has_ladder("Ladders in South Atoll", state, player, options)) + rule=lambda state: has_ability(prayer, state, world) + and has_ladder("Ladders in South Atoll", state, world)) regions["Ruined Atoll Statue"].connect( connecting_region=regions["Ruined Atoll"]) regions["Frog Stairs Eye Exit"].connect( connecting_region=regions["Frog Stairs Upper"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog Stairs Upper"].connect( connecting_region=regions["Frog Stairs Eye Exit"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog Stairs Upper"].connect( connecting_region=regions["Frog Stairs Lower"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog Stairs Lower"].connect( connecting_region=regions["Frog Stairs Upper"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog Stairs Lower"].connect( connecting_region=regions["Frog Stairs to Frog's Domain"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog Stairs to Frog's Domain"].connect( connecting_region=regions["Frog Stairs Lower"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog's Domain Entry"].connect( connecting_region=regions["Frog's Domain"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog's Domain"].connect( connecting_region=regions["Frog's Domain Back"], @@ -599,71 +595,71 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Library Exterior Tree Region"].connect( connecting_region=regions["Library Exterior Ladder Region"], rule=lambda state: state.has_any({grapple, laurels}, player) - and has_ladder("Ladders in Library", state, player, options)) + and has_ladder("Ladders in Library", state, world)) regions["Library Exterior Ladder Region"].connect( connecting_region=regions["Library Exterior Tree Region"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) - and (state.has(grapple, player) or (state.has(laurels, player) - and has_ladder("Ladders in Library", state, player, options)))) + rule=lambda state: has_ability(prayer, state, world) + and ((state.has(laurels, player) and has_ladder("Ladders in Library", state, world)) + or state.has(grapple, player))) regions["Library Hall Bookshelf"].connect( connecting_region=regions["Library Hall"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Hall"].connect( connecting_region=regions["Library Hall Bookshelf"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Hall"].connect( connecting_region=regions["Library Hero's Grave Region"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Library Hero's Grave Region"].connect( connecting_region=regions["Library Hall"]) regions["Library Hall to Rotunda"].connect( connecting_region=regions["Library Hall"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Hall"].connect( connecting_region=regions["Library Hall to Rotunda"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Rotunda to Hall"].connect( connecting_region=regions["Library Rotunda"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Rotunda"].connect( connecting_region=regions["Library Rotunda to Hall"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Rotunda"].connect( connecting_region=regions["Library Rotunda to Lab"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Rotunda to Lab"].connect( connecting_region=regions["Library Rotunda"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Lab Lower"].connect( connecting_region=regions["Library Lab"], rule=lambda state: state.has_any({grapple, laurels}, player) - and has_ladder("Ladders in Library", state, player, options)) + and has_ladder("Ladders in Library", state, world)) regions["Library Lab"].connect( connecting_region=regions["Library Lab Lower"], rule=lambda state: state.has(laurels, player) - and has_ladder("Ladders in Library", state, player, options)) + and has_ladder("Ladders in Library", state, world)) regions["Library Lab"].connect( connecting_region=regions["Library Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) - and has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ability(prayer, state, world) + and has_ladder("Ladders in Library", state, world)) regions["Library Portal"].connect( connecting_region=regions["Library Lab"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options) + rule=lambda state: has_ladder("Ladders in Library", state, world) or state.has(laurels, player)) regions["Library Lab"].connect( connecting_region=regions["Library Lab to Librarian"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Lab to Librarian"].connect( connecting_region=regions["Library Lab"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) # Eastern Vault Fortress regions["Fortress Exterior from East Forest"].connect( @@ -678,14 +674,14 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re rule=lambda state: state.has(laurels, player)) regions["Fortress Exterior from Overworld"].connect( connecting_region=regions["Fortress Exterior near cave"], - rule=lambda state: state.has(laurels, player) or has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: state.has(laurels, player) or has_ability(prayer, state, world)) regions["Fortress Exterior near cave"].connect( connecting_region=regions["Beneath the Vault Entry"], - rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options)) + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)) regions["Beneath the Vault Entry"].connect( connecting_region=regions["Fortress Exterior near cave"], - rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options)) + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)) regions["Fortress Courtyard"].connect( connecting_region=regions["Fortress Exterior from Overworld"], @@ -694,48 +690,48 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Fortress Exterior from Overworld"].connect( connecting_region=regions["Fortress Courtyard"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(True, state, world)) regions["Fortress Courtyard Upper"].connect( connecting_region=regions["Fortress Courtyard"]) # nmg: can ice grapple to the upper ledge regions["Fortress Courtyard"].connect( connecting_region=regions["Fortress Courtyard Upper"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) regions["Fortress Courtyard Upper"].connect( connecting_region=regions["Fortress Exterior from Overworld"]) regions["Beneath the Vault Ladder Exit"].connect( connecting_region=regions["Beneath the Vault Main"], - rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options) - and has_lantern(state, player, options)) + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world) + and has_lantern(state, world)) regions["Beneath the Vault Main"].connect( connecting_region=regions["Beneath the Vault Ladder Exit"], - rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options)) + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)) regions["Beneath the Vault Main"].connect( connecting_region=regions["Beneath the Vault Back"]) regions["Beneath the Vault Back"].connect( connecting_region=regions["Beneath the Vault Main"], - rule=lambda state: has_lantern(state, player, options)) + rule=lambda state: has_lantern(state, world)) regions["Fortress East Shortcut Upper"].connect( connecting_region=regions["Fortress East Shortcut Lower"]) # nmg: can ice grapple upwards regions["Fortress East Shortcut Lower"].connect( connecting_region=regions["Fortress East Shortcut Upper"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) # nmg: ice grapple through the big gold door, can do it both ways regions["Eastern Vault Fortress"].connect( connecting_region=regions["Eastern Vault Fortress Gold Door"], rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses", "Activate Eastern Vault East Fuse"}, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(False, state, world)) regions["Eastern Vault Fortress Gold Door"].connect( connecting_region=regions["Eastern Vault Fortress"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) regions["Fortress Grave Path"].connect( connecting_region=regions["Fortress Grave Path Dusty Entrance Region"], @@ -746,14 +742,14 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Fortress Grave Path"].connect( connecting_region=regions["Fortress Hero's Grave Region"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Fortress Hero's Grave Region"].connect( connecting_region=regions["Fortress Grave Path"]) # nmg: ice grapple from upper grave path to lower regions["Fortress Grave Path Upper"].connect( connecting_region=regions["Fortress Grave Path"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) regions["Fortress Arena"].connect( connecting_region=regions["Fortress Arena Portal"], @@ -764,10 +760,10 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re # Quarry regions["Lower Mountain"].connect( connecting_region=regions["Lower Mountain Stairs"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) regions["Lower Mountain Stairs"].connect( connecting_region=regions["Lower Mountain"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) regions["Quarry Entry"].connect( connecting_region=regions["Quarry Portal"], @@ -805,25 +801,24 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Quarry"].connect( connecting_region=regions["Lower Quarry"], - rule=lambda state: has_mask(state, player, options)) + rule=lambda state: has_mask(state, world)) # need the ladder, or you can ice grapple down in nmg regions["Lower Quarry"].connect( connecting_region=regions["Even Lower Quarry"], - rule=lambda state: has_ladder("Ladders in Lower Quarry", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders in Lower Quarry", state, world) + or has_ice_grapple_logic(True, state, world)) # nmg: bring a scav over, then ice grapple through the door, only with ER on to avoid soft lock regions["Even Lower Quarry"].connect( connecting_region=regions["Lower Quarry Zig Door"], rule=lambda state: state.has("Activate Quarry Fuse", player) - or (has_ice_grapple_logic(False, state, player, options, ability_unlocks) and options.entrance_rando)) + or (has_ice_grapple_logic(False, state, world) and options.entrance_rando)) # nmg: use ice grapple to get from the beginning of Quarry to the door without really needing mask only with ER on regions["Quarry"].connect( connecting_region=regions["Lower Quarry Zig Door"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks) - and options.entrance_rando) + rule=lambda state: has_ice_grapple_logic(True, state, world) and options.entrance_rando) regions["Monastery Front"].connect( connecting_region=regions["Monastery Back"]) @@ -834,7 +829,7 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Monastery Back"].connect( connecting_region=regions["Monastery Hero's Grave Region"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Monastery Hero's Grave Region"].connect( connecting_region=regions["Monastery Back"]) @@ -855,20 +850,19 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Rooted Ziggurat Lower Front"].connect( connecting_region=regions["Rooted Ziggurat Lower Back"], rule=lambda state: state.has(laurels, player) - or (has_sword(state, player) and has_ability(state, player, prayer, options, ability_unlocks))) + or (has_sword(state, player) and has_ability(prayer, state, world))) # unrestricted: use ladder storage to get to the front, get hit by one of the many enemies # nmg: can ice grapple on the voidlings to the double admin fight, still need to pray at the fuse regions["Rooted Ziggurat Lower Back"].connect( connecting_region=regions["Rooted Ziggurat Lower Front"], - rule=lambda state: ((state.has(laurels, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) - and has_ability(state, player, prayer, options, ability_unlocks) + rule=lambda state: ((state.has(laurels, player) or has_ice_grapple_logic(True, state, world)) + and has_ability(prayer, state, world) and has_sword(state, player)) - or can_ladder_storage(state, player, options)) + or can_ladder_storage(state, world)) regions["Rooted Ziggurat Lower Back"].connect( connecting_region=regions["Rooted Ziggurat Portal Room Entrance"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Rooted Ziggurat Portal Room Entrance"].connect( connecting_region=regions["Rooted Ziggurat Lower Back"]) @@ -880,41 +874,40 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re rule=lambda state: state.has("Activate Ziggurat Fuse", player)) regions["Rooted Ziggurat Portal Room Exit"].connect( connecting_region=regions["Rooted Ziggurat Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) # Swamp and Cathedral regions["Swamp Front"].connect( connecting_region=regions["Swamp Mid"], - rule=lambda state: has_ladder("Ladders in Swamp", state, player, options) + rule=lambda state: has_ladder("Ladders in Swamp", state, world) or state.has(laurels, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) # nmg: ice grapple through gate + or has_ice_grapple_logic(False, state, world)) # nmg: ice grapple through gate regions["Swamp Mid"].connect( connecting_region=regions["Swamp Front"], - rule=lambda state: has_ladder("Ladders in Swamp", state, player, options) + rule=lambda state: has_ladder("Ladders in Swamp", state, world) or state.has(laurels, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) # nmg: ice grapple through gate + or has_ice_grapple_logic(False, state, world)) # nmg: ice grapple through gate # nmg: ice grapple through cathedral door, can do it both ways regions["Swamp Mid"].connect( connecting_region=regions["Swamp to Cathedral Main Entrance Region"], - rule=lambda state: (has_ability(state, player, prayer, options, ability_unlocks) - and state.has(laurels, player)) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + rule=lambda state: (has_ability(prayer, state, world) and state.has(laurels, player)) + or has_ice_grapple_logic(False, state, world)) regions["Swamp to Cathedral Main Entrance Region"].connect( connecting_region=regions["Swamp Mid"], - rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(False, state, world)) regions["Swamp Mid"].connect( connecting_region=regions["Swamp Ledge under Cathedral Door"], - rule=lambda state: has_ladder("Ladders in Swamp", state, player, options)) + rule=lambda state: has_ladder("Ladders in Swamp", state, world)) regions["Swamp Ledge under Cathedral Door"].connect( connecting_region=regions["Swamp Mid"], - rule=lambda state: has_ladder("Ladders in Swamp", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) # nmg: ice grapple the enemy at door + rule=lambda state: has_ladder("Ladders in Swamp", state, world) + or has_ice_grapple_logic(True, state, world)) # nmg: ice grapple the enemy at door regions["Swamp Ledge under Cathedral Door"].connect( connecting_region=regions["Swamp to Cathedral Treasure Room"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) regions["Swamp to Cathedral Treasure Room"].connect( connecting_region=regions["Swamp Ledge under Cathedral Door"]) @@ -929,11 +922,11 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Back of Swamp Laurels Area"].connect( connecting_region=regions["Swamp Mid"], rule=lambda state: state.has(laurels, player) - and has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + and has_ice_grapple_logic(True, state, world)) regions["Back of Swamp"].connect( connecting_region=regions["Swamp Hero's Grave Region"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Swamp Hero's Grave Region"].connect( connecting_region=regions["Back of Swamp"]) @@ -991,7 +984,9 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re connecting_region=regions["Spirit Arena Victory"], rule=lambda state: (state.has(gold_hexagon, player, world.options.hexagon_goal.value) if world.options.hexagon_quest else - state.has_all({red_hexagon, green_hexagon, blue_hexagon, "Unseal the Heir"}, player))) + (state.has_all({red_hexagon, green_hexagon, blue_hexagon, "Unseal the Heir"}, player) + and state.has_group_unique("Hero Relics", player, 6) + and has_sword(state, player)))) # connecting the regions portals are in to other portals you can access via ladder storage # using has_stick instead of can_ladder_storage since it's already checking the logic rules @@ -1227,9 +1222,9 @@ def get_portal_info(portal_sd: str) -> Tuple[str, str]: regions[paired_region], name=portal_name + " (LS) " + region_name, rule=lambda state: has_stick(state, player) - and has_ability(state, player, holy_cross, options, ability_unlocks) - and (has_ladder("Ladders in Swamp", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks) + and has_ability(holy_cross, state, world) + and (has_ladder("Ladders in Swamp", state, world) + or has_ice_grapple_logic(True, state, world) or not options.entrance_rando)) # soft locked without this ladder elif portal_name == "West Garden Exit after Boss" and not options.entrance_rando: @@ -1252,8 +1247,7 @@ def get_portal_info(portal_sd: str) -> Tuple[str, str]: regions[region_name].connect( regions[paired_region], name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and state.has_any(ladders, player) + rule=lambda state: has_stick(state, player) and state.has_any(ladders, player) and state.has_any({"Ladder in Dark Tomb", "Ladders to West Bell"}, player)) # soft locked if you can't get past garden knight backwards or up the belltower ladders elif portal_name == "West Garden Entrance near Belltower" and not options.entrance_rando: @@ -1267,24 +1261,21 @@ def get_portal_info(portal_sd: str) -> Tuple[str, str]: regions[region_name].connect( regions[paired_region], name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and state.has("Ladder to Beneath the Vault", player) - and has_lantern(state, player, options)) + rule=lambda state: has_stick(state, player) and state.has("Ladder to Beneath the Vault", player) + and has_lantern(state, world)) elif portal_name == "Atoll Lower Entrance" and not options.entrance_rando: regions[region_name].connect( regions[paired_region], name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and state.has_any(ladders, player) + rule=lambda state: has_stick(state, player) and state.has_any(ladders, player) and (state.has_any({"Ladders in Overworld Town", grapple}, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks))) + or has_ice_grapple_logic(True, state, world))) elif portal_name == "Atoll Upper Entrance" and not options.entrance_rando: regions[region_name].connect( regions[paired_region], name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and state.has_any(ladders, player) - and state.has(grapple, player) or has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_stick(state, player) and state.has_any(ladders, player) + and state.has(grapple, player) or has_ability(prayer, state, world)) # soft lock potential elif portal_name in {"Special Shop Entrance", "Stairs to Top of the Mountain", "Swamp Upper Entrance", "Swamp Lower Entrance", "Caustic Light Cave Entrance"} and not options.entrance_rando: @@ -1303,7 +1294,7 @@ def get_portal_info(portal_sd: str) -> Tuple[str, str]: or state.has("Ladder to Quarry", player) and (state.has(fire_wand, player) or has_sword(state, player)))) or state.has("Ladders near Overworld Checkpoint", player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks))))) + or has_ice_grapple_logic(True, state, world))))) # if no ladder items are required, just do the basic stick only lambda elif not ladders or not options.shuffle_ladders: regions[region_name].connect( @@ -1316,54 +1307,53 @@ def get_portal_info(portal_sd: str) -> Tuple[str, str]: regions[region_name].connect( regions[paired_region], name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and state.has(ladder, player)) + rule=lambda state: has_stick(state, player) and state.has(ladder, player)) # if multiple ladders can be used else: regions[region_name].connect( regions[paired_region], name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and state.has_any(ladders, player)) + rule=lambda state: has_stick(state, player) and state.has_any(ladders, player)) -def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: +def set_er_location_rules(world: "TunicWorld") -> None: player = world.player multiworld = world.multiworld options = world.options + forbid_item(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), fairies, player) # Ability Shuffle Exclusive Rules set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player), lambda state: state.has("Activate Furnace Fuse", player)) set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Overworld - [Southwest] Flowers Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Overworld - [East] Weathervane Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Overworld - [Northeast] Flowers Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Overworld - [Southwest] Haiku Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Overworld - [Northwest] Golden Obelisk Page", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) # Overworld set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player), @@ -1379,35 +1369,37 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), lambda state: state.has(grapple, player)) set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Caustic Light Cave - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Cube Cave - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Old House - Holy Cross Door Page", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Maze Cave - Maze Room Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Patrol Cave - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Ruined Passage - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Hourglass Cave - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Secret Gathering Place - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player), lambda state: state.has(fairies, player, 10)) set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), lambda state: state.has(fairies, player, 20)) - set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player), lambda state: state.has(coins, player, 3)) - set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player), lambda state: state.has(coins, player, 6)) + set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player), + lambda state: state.has(coins, player, 3)) + set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player), + lambda state: state.has(coins, player, 6)) set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player), lambda state: state.has(coins, player, 10)) set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player), @@ -1419,8 +1411,7 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player), lambda state: state.has_all({grapple, laurels}, player)) set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), lambda state: ( - state.has_all({grapple, ice_dagger, fire_wand}, player) and - has_ability(state, player, icebolt, options, ability_unlocks))) + state.has_all({grapple, ice_dagger, fire_wand}, player) and has_ability(icebolt, state, world))) # West Garden set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player), @@ -1428,8 +1419,7 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player), - lambda state: state.has(laurels, player) and has_ability(state, player, holy_cross, options, - ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player), @@ -1464,10 +1454,12 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) lambda state: state.has(laurels, player)) # Ziggurat + # if ER is off, you still need to get past the Admin or you'll get stuck in lower zig set_rule(multiworld.get_location("Rooted Ziggurat Upper - Near Bridge Switch", player), - lambda state: has_sword(state, player) or state.has(fire_wand, player)) + lambda state: has_sword(state, player) or (state.has(fire_wand, player) and (state.has(laurels, player) + or options.entrance_rando))) set_rule(multiworld.get_location("Rooted Ziggurat Lower - After Guarded Fuse", player), - lambda state: has_sword(state, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_sword(state, player) and has_ability(prayer, state, world)) # Bosses set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player), @@ -1475,7 +1467,7 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) # nmg - kill Librarian with a lure, or gun I guess set_rule(multiworld.get_location("Librarian - Hexagon Green", player), lambda state: (has_sword(state, player) or options.logic_rules) - and has_ladder("Ladders in Library", state, player, options)) + and has_ladder("Ladders in Library", state, world)) # nmg - kill boss scav with orb + firecracker, or similar set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player), lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) @@ -1513,11 +1505,11 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) set_rule(multiworld.get_location("Western Bell", player), lambda state: (has_stick(state, player) or state.has(fire_wand, player))) set_rule(multiworld.get_location("Furnace Fuse", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) set_rule(multiworld.get_location("South and West Fortress Exterior Fuses", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) set_rule(multiworld.get_location("Upper and Central Fortress Exterior Fuses", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) set_rule(multiworld.get_location("Beneath the Vault Fuse", player), lambda state: state.has("Activate South and West Fortress Exterior Fuses", player)) set_rule(multiworld.get_location("Eastern Vault West Fuses", player), @@ -1526,12 +1518,22 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) lambda state: state.has_all({"Activate Upper and Central Fortress Exterior Fuses", "Activate South and West Fortress Exterior Fuses"}, player)) set_rule(multiworld.get_location("Quarry Connector Fuse", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks) and state.has(grapple, player)) + lambda state: has_ability(prayer, state, world) and state.has(grapple, player)) set_rule(multiworld.get_location("Quarry Fuse", player), lambda state: state.has("Activate Quarry Connector Fuse", player)) set_rule(multiworld.get_location("Ziggurat Fuse", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) set_rule(multiworld.get_location("West Garden Fuse", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) set_rule(multiworld.get_location("Library Fuse", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) + + # Shop + set_rule(multiworld.get_location("Shop - Potion 1", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Shop - Potion 2", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Shop - Coin 1", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Shop - Coin 2", player), + lambda state: has_sword(state, player)) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 9d25137ba469..0bd8c5e80681 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -34,7 +34,7 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: for region_name, region_data in tunic_er_regions.items(): regions[region_name] = Region(region_name, world.player, world.multiworld) - set_er_region_rules(world, world.ability_unlocks, regions, portal_pairs) + set_er_region_rules(world, regions, portal_pairs) for location_name, location_id in world.location_name_to_id.items(): region = regions[location_table[location_name].er_region] @@ -67,7 +67,7 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: "Eastern Vault West Fuses": "Eastern Vault Fortress", "Eastern Vault East Fuse": "Eastern Vault Fortress", "Quarry Connector Fuse": "Quarry Connector", - "Quarry Fuse": "Quarry", + "Quarry Fuse": "Quarry Entry", "Ziggurat Fuse": "Rooted Ziggurat Lower Back", "West Garden Fuse": "West Garden", "Library Fuse": "Library Lab", diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index f470ea540d76..a8aec9f74485 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -64,12 +64,12 @@ class TunicItemData(NamedTuple): "HP Offering": TunicItemData(ItemClassification.useful, 6, 48, "Offerings"), "MP Offering": TunicItemData(ItemClassification.useful, 3, 49, "Offerings"), "SP Offering": TunicItemData(ItemClassification.useful, 2, 50, "Offerings"), - "Hero Relic - ATT": TunicItemData(ItemClassification.useful, 1, 51, "Hero Relics"), - "Hero Relic - DEF": TunicItemData(ItemClassification.useful, 1, 52, "Hero Relics"), - "Hero Relic - HP": TunicItemData(ItemClassification.useful, 1, 53, "Hero Relics"), - "Hero Relic - MP": TunicItemData(ItemClassification.useful, 1, 54, "Hero Relics"), - "Hero Relic - POTION": TunicItemData(ItemClassification.useful, 1, 55, "Hero Relics"), - "Hero Relic - SP": TunicItemData(ItemClassification.useful, 1, 56, "Hero Relics"), + "Hero Relic - ATT": TunicItemData(ItemClassification.progression_skip_balancing, 1, 51, "Hero Relics"), + "Hero Relic - DEF": TunicItemData(ItemClassification.progression_skip_balancing, 1, 52, "Hero Relics"), + "Hero Relic - HP": TunicItemData(ItemClassification.progression_skip_balancing, 1, 53, "Hero Relics"), + "Hero Relic - MP": TunicItemData(ItemClassification.progression_skip_balancing, 1, 54, "Hero Relics"), + "Hero Relic - POTION": TunicItemData(ItemClassification.progression_skip_balancing, 1, 55, "Hero Relics"), + "Hero Relic - SP": TunicItemData(ItemClassification.progression_skip_balancing, 1, 56, "Hero Relics"), "Orange Peril Ring": TunicItemData(ItemClassification.useful, 1, 57, "Cards"), "Tincture": TunicItemData(ItemClassification.useful, 1, 58, "Cards"), "Scavenger Mask": TunicItemData(ItemClassification.progression, 1, 59, "Cards"), @@ -143,7 +143,6 @@ class TunicItemData(NamedTuple): "Pages 50-51": TunicItemData(ItemClassification.useful, 1, 127, "Pages"), "Pages 52-53 (Icebolt)": TunicItemData(ItemClassification.progression, 1, 128, "Pages"), "Pages 54-55": TunicItemData(ItemClassification.useful, 1, 129, "Pages"), - "Ladders near Weathervane": TunicItemData(ItemClassification.progression, 0, 130, "Ladders"), "Ladders near Overworld Checkpoint": TunicItemData(ItemClassification.progression, 0, 131, "Ladders"), "Ladders near Patrol Cave": TunicItemData(ItemClassification.progression, 0, 132, "Ladders"), diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index ff9872ab4807..bf1dd860ae79 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -174,6 +174,13 @@ class ShuffleLadders(Toggle): class TunicPlandoConnections(PlandoConnections): + """ + Generic connection plando. Format is: + - entrance: "Entrance Name" + exit: "Exit Name" + percentage: 100 + Percentage is an integer from 0 to 100 which determines whether that connection will be made. Defaults to 100 if omitted. + """ entrances = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"} exits = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"} diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index e0a2c305101b..73eb8118901b 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -38,102 +38,98 @@ def randomize_ability_unlocks(random: Random, options: TunicOptions) -> Dict[str return dict(zip(abilities, ability_requirement)) -def has_ability(state: CollectionState, player: int, ability: str, options: TunicOptions, - ability_unlocks: Dict[str, int]) -> bool: +def has_ability(ability: str, state: CollectionState, world: "TunicWorld") -> bool: + options = world.options + ability_unlocks = world.ability_unlocks if not options.ability_shuffling: return True if options.hexagon_quest: - return state.has(gold_hexagon, player, ability_unlocks[ability]) - return state.has(ability, player) + return state.has(gold_hexagon, world.player, ability_unlocks[ability]) + return state.has(ability, world.player) # a check to see if you can whack things in melee at all def has_stick(state: CollectionState, player: int) -> bool: - return state.has("Stick", player) or state.has("Sword Upgrade", player, 1) or state.has("Sword", player) + return (state.has("Stick", player) or state.has("Sword Upgrade", player, 1) + or state.has("Sword", player)) def has_sword(state: CollectionState, player: int) -> bool: return state.has("Sword", player) or state.has("Sword Upgrade", player, 2) -def has_ice_grapple_logic(long_range: bool, state: CollectionState, player: int, options: TunicOptions, - ability_unlocks: Dict[str, int]) -> bool: - if not options.logic_rules: +def has_ice_grapple_logic(long_range: bool, state: CollectionState, world: "TunicWorld") -> bool: + player = world.player + if not world.options.logic_rules: return False - if not long_range: return state.has_all({ice_dagger, grapple}, player) else: - return state.has_all({ice_dagger, fire_wand, grapple}, player) and \ - has_ability(state, player, icebolt, options, ability_unlocks) + return state.has_all({ice_dagger, fire_wand, grapple}, player) and has_ability(icebolt, state, world) -def can_ladder_storage(state: CollectionState, player: int, options: TunicOptions) -> bool: - if options.logic_rules == "unrestricted" and has_stick(state, player): - return True - else: - return False +def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool: + return world.options.logic_rules == "unrestricted" and has_stick(state, world.player) -def has_mask(state: CollectionState, player: int, options: TunicOptions) -> bool: - if options.maskless: +def has_mask(state: CollectionState, world: "TunicWorld") -> bool: + if world.options.maskless: return True else: - return state.has(mask, player) + return state.has(mask, world.player) -def has_lantern(state: CollectionState, player: int, options: TunicOptions) -> bool: - if options.lanternless: +def has_lantern(state: CollectionState, world: "TunicWorld") -> bool: + if world.options.lanternless: return True else: - return state.has(lantern, player) + return state.has(lantern, world.player) -def set_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: +def set_region_rules(world: "TunicWorld") -> None: multiworld = world.multiworld player = world.player options = world.options multiworld.get_entrance("Overworld -> Overworld Holy Cross", player).access_rule = \ - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) + lambda state: has_ability(holy_cross, state, world) multiworld.get_entrance("Overworld -> Beneath the Well", player).access_rule = \ lambda state: has_stick(state, player) or state.has(fire_wand, player) multiworld.get_entrance("Overworld -> Dark Tomb", player).access_rule = \ - lambda state: has_lantern(state, player, options) + lambda state: has_lantern(state, world) multiworld.get_entrance("Overworld -> West Garden", player).access_rule = \ lambda state: state.has(laurels, player) \ - or can_ladder_storage(state, player, options) + or can_ladder_storage(state, world) multiworld.get_entrance("Overworld -> Eastern Vault Fortress", player).access_rule = \ lambda state: state.has(laurels, player) \ - or has_ice_grapple_logic(True, state, player, options, ability_unlocks) \ - or can_ladder_storage(state, player, options) + or has_ice_grapple_logic(True, state, world) \ + or can_ladder_storage(state, world) # using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules multiworld.get_entrance("Overworld -> Beneath the Vault", player).access_rule = \ - lambda state: has_lantern(state, player, options) and \ - has_ability(state, player, prayer, options, ability_unlocks) + lambda state: has_lantern(state, world) and has_ability(prayer, state, world) multiworld.get_entrance("Ruined Atoll -> Library", player).access_rule = \ - lambda state: state.has_any({grapple, laurels}, player) and \ - has_ability(state, player, prayer, options, ability_unlocks) + lambda state: state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world) multiworld.get_entrance("Overworld -> Quarry", player).access_rule = \ lambda state: (has_sword(state, player) or state.has(fire_wand, player)) \ - and (state.has_any({grapple, laurels}, player) or can_ladder_storage(state, player, options)) + and (state.has_any({grapple, laurels}, player) or can_ladder_storage(state, world)) multiworld.get_entrance("Quarry Back -> Quarry", player).access_rule = \ lambda state: has_sword(state, player) or state.has(fire_wand, player) multiworld.get_entrance("Quarry -> Lower Quarry", player).access_rule = \ - lambda state: has_mask(state, player, options) + lambda state: has_mask(state, world) multiworld.get_entrance("Lower Quarry -> Rooted Ziggurat", player).access_rule = \ - lambda state: state.has(grapple, player) and has_ability(state, player, prayer, options, ability_unlocks) + lambda state: state.has(grapple, player) and has_ability(prayer, state, world) multiworld.get_entrance("Swamp -> Cathedral", player).access_rule = \ - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks) \ - or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world) \ + or has_ice_grapple_logic(False, state, world) multiworld.get_entrance("Overworld -> Spirit Arena", player).access_rule = \ - lambda state: (state.has(gold_hexagon, player, options.hexagon_goal.value) if options.hexagon_quest.value - else state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player)) and \ - has_ability(state, player, prayer, options, ability_unlocks) and has_sword(state, player) and \ - state.has_any({lantern, laurels}, player) + lambda state: ((state.has(gold_hexagon, player, options.hexagon_goal.value) if options.hexagon_quest.value + else state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player) + and state.has_group_unique("Hero Relics", player, 6)) + and has_ability(prayer, state, world) and has_sword(state, player) + and state.has_any({lantern, laurels}, player)) -def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: +def set_location_rules(world: "TunicWorld") -> None: multiworld = world.multiworld player = world.player options = world.options @@ -142,37 +138,36 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> # Ability Shuffle Exclusive Rules set_rule(multiworld.get_location("Far Shore - Page Pickup", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) set_rule(multiworld.get_location("Fortress Courtyard - Chest Near Cave", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks) or state.has(laurels, player) - or can_ladder_storage(state, player, options) - or (has_ice_grapple_logic(True, state, player, options, ability_unlocks) - and has_lantern(state, player, options))) + lambda state: has_ability(prayer, state, world) + or state.has(laurels, player) + or can_ladder_storage(state, world) + or (has_ice_grapple_logic(True, state, world) and has_lantern(state, world))) set_rule(multiworld.get_location("Fortress Courtyard - Page Near Cave", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks) or state.has(laurels, player) - or can_ladder_storage(state, player, options) - or (has_ice_grapple_logic(True, state, player, options, ability_unlocks) - and has_lantern(state, player, options))) + lambda state: has_ability(prayer, state, world) or state.has(laurels, player) + or can_ladder_storage(state, world) + or (has_ice_grapple_logic(True, state, world) and has_lantern(state, world))) set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) # Overworld set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player), @@ -182,21 +177,21 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player), lambda state: state.has_any({grapple, laurels}, player)) set_rule(multiworld.get_location("Far Shore - Secret Chest", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) set_rule(multiworld.get_location("Overworld - [Southeast] Page on Pillar by Swamp", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Old House - Normal Chest", player), lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + or has_ice_grapple_logic(False, state, world) or (state.has(laurels, player) and options.logic_rules)) set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) and - (state.has(house_key, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks) - or (state.has(laurels, player) and options.logic_rules))) + lambda state: has_ability(holy_cross, state, world) and ( + state.has(house_key, player) + or has_ice_grapple_logic(False, state, world) + or (state.has(laurels, player) and options.logic_rules))) set_rule(multiworld.get_location("Old House - Shield Pickup", player), lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + or has_ice_grapple_logic(False, state, world) or (state.has(laurels, player) and options.logic_rules)) set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), lambda state: state.has(laurels, player)) @@ -204,8 +199,8 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Overworld - [West] Chest After Bell", player), lambda state: state.has(laurels, player) - or (has_lantern(state, player, options) and has_sword(state, player)) - or can_ladder_storage(state, player, options)) + or (has_lantern(state, world) and has_sword(state, player)) + or can_ladder_storage(state, world)) set_rule(multiworld.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate", player), lambda state: state.has_any({grapple, laurels}, player) or options.logic_rules) set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), @@ -213,15 +208,14 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> set_rule(multiworld.get_location("Special Shop - Secret Page Pickup", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) and - (state.has(laurels, player) - or (has_lantern(state, player, options) and - (has_sword(state, player) or state.has(fire_wand, player))) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) + lambda state: has_ability(holy_cross, state, world) + and (state.has(laurels, player) or (has_lantern(state, world) and (has_sword(state, player) + or state.has(fire_wand, player))) + or has_ice_grapple_logic(False, state, world))) set_rule(multiworld.get_location("Sealed Temple - Page Pickup", player), lambda state: state.has(laurels, player) - or (has_lantern(state, player, options) and (has_sword(state, player) or state.has(fire_wand, player))) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + or (has_lantern(state, world) and (has_sword(state, player) or state.has(fire_wand, player))) + or has_ice_grapple_logic(False, state, world)) set_rule(multiworld.get_location("West Furnace - Lantern Pickup", player), lambda state: has_stick(state, player) or state.has_any({fire_wand, laurels}, player)) @@ -245,7 +239,7 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> lambda state: state.has_all({grapple, laurels}, player)) set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), lambda state: state.has_all({grapple, ice_dagger, fire_wand}, player) - and has_ability(state, player, icebolt, options, ability_unlocks)) + and has_ability(icebolt, state, world)) # West Garden set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player), @@ -253,17 +247,16 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player), - lambda state: state.has(laurels, player) - and has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House", player), - lambda state: (state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + lambda state: (state.has(laurels, player) and has_ability(prayer, state, world)) + or has_ice_grapple_logic(True, state, world)) set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("West Garden - [Central Highlands] After Garden Knight", player), lambda state: state.has(laurels, player) - or (has_lantern(state, player, options) and has_sword(state, player)) - or can_ladder_storage(state, player, options)) + or (has_lantern(state, world) and has_sword(state, player)) + or can_ladder_storage(state, world)) # Ruined Atoll set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player), @@ -287,23 +280,23 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> set_rule(multiworld.get_location("Fortress Leaf Piles - Secret Chest", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player), - lambda state: has_sword(state, player) and - (has_ability(state, player, prayer, options, ability_unlocks) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) + lambda state: has_sword(state, player) + and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world))) set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player), - lambda state: state.has(vault_key, player) and - (has_ability(state, player, prayer, options, ability_unlocks) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) + lambda state: state.has(vault_key, player) + and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world))) # Beneath the Vault set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player), lambda state: has_stick(state, player) or state.has_any({laurels, fire_wand}, player)) set_rule(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player), - lambda state: has_stick(state, player) and has_lantern(state, player, options)) + lambda state: has_stick(state, player) and has_lantern(state, world)) # Quarry set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player), lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Rooted Ziggurat Upper - Near Bridge Switch", player), + lambda state: has_sword(state, player) or state.has_all({fire_wand, laurels}, player)) # nmg - kill boss scav with orb + firecracker, or similar set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player), lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) @@ -311,8 +304,7 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> # Swamp set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player), lambda state: (state.has(fire_wand, player) and has_sword(state, player)) - and (state.has(laurels, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) + and (state.has(laurels, player) or has_ice_grapple_logic(False, state, world))) set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player), @@ -324,14 +316,24 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> # Hero's Grave set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) + + # Shop + set_rule(multiworld.get_location("Shop - Potion 1", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Shop - Potion 2", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Shop - Coin 1", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Shop - Coin 2", player), + lambda state: has_sword(state, player)) diff --git a/worlds/undertale/Items.py b/worlds/undertale/Items.py index 033102664c82..9f2ce1afec9e 100644 --- a/worlds/undertale/Items.py +++ b/worlds/undertale/Items.py @@ -105,7 +105,6 @@ class UndertaleItem(Item): non_key_items = { "Butterscotch Pie": 1, "500G": 2, - "1000G": 2, "Face Steak": 1, "Snowman Piece": 1, "Instant Noodles": 1, @@ -147,6 +146,7 @@ class UndertaleItem(Item): key_items = { "Complete Skeleton": 1, "Fish": 1, + "1000G": 2, "DT Extractor": 1, "Mettaton Plush": 1, "Punch Card": 3, diff --git a/worlds/undertale/Options.py b/worlds/undertale/Options.py index 146a7838f766..b2de41a3d577 100644 --- a/worlds/undertale/Options.py +++ b/worlds/undertale/Options.py @@ -1,5 +1,5 @@ -import typing -from Options import Choice, Option, Toggle, Range +from Options import Choice, Toggle, Range, PerGameCommonOptions +from dataclasses import dataclass class RouteRequired(Choice): @@ -86,17 +86,17 @@ class RandoBattleOptions(Toggle): default = 0 -undertale_options: typing.Dict[str, type(Option)] = { - "route_required": RouteRequired, - "starting_area": StartingArea, - "key_hunt": KeyHunt, - "key_pieces": KeyPieces, - "rando_love": RandomizeLove, - "rando_stats": RandomizeStats, - "temy_include": IncludeTemy, - "no_equips": NoEquips, - "only_flakes": OnlyFlakes, - "prog_armor": ProgressiveArmor, - "prog_weapons": ProgressiveWeapons, - "rando_item_button": RandoBattleOptions, -} +@dataclass +class UndertaleOptions(PerGameCommonOptions): + route_required: RouteRequired + starting_area: StartingArea + key_hunt: KeyHunt + key_pieces: KeyPieces + rando_love: RandomizeLove + rando_stats: RandomizeStats + temy_include: IncludeTemy + no_equips: NoEquips + only_flakes: OnlyFlakes + prog_armor: ProgressiveArmor + prog_weapons: ProgressiveWeapons + rando_item_button: RandoBattleOptions diff --git a/worlds/undertale/Rules.py b/worlds/undertale/Rules.py index 897484b0508f..2de61d8eb00c 100644 --- a/worlds/undertale/Rules.py +++ b/worlds/undertale/Rules.py @@ -1,18 +1,22 @@ -from worlds.generic.Rules import set_rule, add_rule, add_item_rule -from BaseClasses import MultiWorld, CollectionState +from worlds.generic.Rules import set_rule, add_rule +from BaseClasses import CollectionState +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from . import UndertaleWorld -def _undertale_is_route(state: CollectionState, player: int, route: int): + +def _undertale_is_route(world: "UndertaleWorld", route: int): if route == 3: - return state.multiworld.route_required[player].current_key == "all_routes" - if state.multiworld.route_required[player].current_key == "all_routes": + return world.options.route_required.current_key == "all_routes" + if world.options.route_required.current_key == "all_routes": return True if route == 0: - return state.multiworld.route_required[player].current_key == "neutral" + return world.options.route_required.current_key == "neutral" if route == 1: - return state.multiworld.route_required[player].current_key == "pacifist" + return world.options.route_required.current_key == "pacifist" if route == 2: - return state.multiworld.route_required[player].current_key == "genocide" + return world.options.route_required.current_key == "genocide" return False @@ -27,7 +31,7 @@ def _undertale_has_plot(state: CollectionState, player: int, item: str): return state.has("DT Extractor", player) -def _undertale_can_level(state: CollectionState, exp: int, lvl: int): +def _undertale_can_level(exp: int, lvl: int): if exp >= 10 and lvl == 1: return True elif exp >= 30 and lvl == 2: @@ -70,7 +74,9 @@ def _undertale_can_level(state: CollectionState, exp: int, lvl: int): # Sets rules on entrances and advancements that are always applied -def set_rules(multiworld: MultiWorld, player: int): +def set_rules(world: "UndertaleWorld"): + player = world.player + multiworld = world.multiworld set_rule(multiworld.get_entrance("Ruins Hub", player), lambda state: state.has("Ruins Key", player)) set_rule(multiworld.get_entrance("Snowdin Hub", player), lambda state: state.has("Snowdin Key", player)) set_rule(multiworld.get_entrance("Waterfall Hub", player), lambda state: state.has("Waterfall Key", player)) @@ -81,16 +87,16 @@ def set_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_entrance("New Home Exit", player), lambda state: (state.has("Left Home Key", player) and state.has("Right Home Key", player)) or - state.has("Key Piece", player, state.multiworld.key_pieces[player].value)) - if _undertale_is_route(multiworld.state, player, 1): + state.has("Key Piece", player, world.options.key_pieces.value)) + if _undertale_is_route(world, 1): set_rule(multiworld.get_entrance("Papyrus\" Home Entrance", player), lambda state: _undertale_has_plot(state, player, "Complete Skeleton")) set_rule(multiworld.get_entrance("Undyne\"s Home Entrance", player), lambda state: _undertale_has_plot(state, player, "Fish") and state.has("Papyrus Date", player)) set_rule(multiworld.get_entrance("Lab Elevator", player), lambda state: state.has("Alphys Date", player) and state.has("DT Extractor", player) and - ((state.has("Left Home Key", player) and state.has("Right Home Key", player)) or - state.has("Key Piece", player, state.multiworld.key_pieces[player].value))) + ((state.has("Left Home Key", player) and state.has("Right Home Key", player)) or + state.has("Key Piece", player, world.options.key_pieces.value))) set_rule(multiworld.get_location("Alphys Date", player), lambda state: state.can_reach("New Home", "Region", player) and state.has("Undyne Letter EX", player) and state.has("Undyne Date", player)) @@ -101,7 +107,10 @@ def set_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_location("True Lab Plot", player), lambda state: state.can_reach("New Home", "Region", player) and state.can_reach("Letter Quest", "Location", player) - and state.can_reach("Alphys Date", "Location", player)) + and state.can_reach("Alphys Date", "Location", player) + and ((state.has("Left Home Key", player) and + state.has("Right Home Key", player)) or + state.has("Key Piece", player, world.options.key_pieces.value))) set_rule(multiworld.get_location("Chisps Machine", player), lambda state: state.can_reach("True Lab", "Region", player)) set_rule(multiworld.get_location("Dog Sale 1", player), @@ -118,7 +127,7 @@ def set_rules(multiworld: MultiWorld, player: int): lambda state: state.can_reach("News Show", "Region", player) and state.has("Hot Dog...?", player, 1)) set_rule(multiworld.get_location("Letter Quest", player), lambda state: state.can_reach("Last Corridor", "Region", player) and state.has("Undyne Date", player)) - if (not _undertale_is_route(multiworld.state, player, 2)) or _undertale_is_route(multiworld.state, player, 3): + if (not _undertale_is_route(world, 2)) or _undertale_is_route(world, 3): set_rule(multiworld.get_location("Nicecream Punch Card", player), lambda state: state.has("Punch Card", player, 3) and state.can_reach("Waterfall", "Region", player)) set_rule(multiworld.get_location("Nicecream Snowdin", player), @@ -129,26 +138,26 @@ def set_rules(multiworld: MultiWorld, player: int): lambda state: state.can_reach("Waterfall", "Region", player)) set_rule(multiworld.get_location("Apron Hidden", player), lambda state: state.can_reach("Cooking Show", "Region", player)) - if _undertale_is_route(multiworld.state, player, 2) and \ - (bool(multiworld.rando_love[player].value) or bool(multiworld.rando_stats[player].value)): + if _undertale_is_route(world, 2) and \ + (bool(world.options.rando_love.value) or bool(world.options.rando_stats.value)): maxlv = 1 exp = 190 curarea = "Old Home" while maxlv < 20: maxlv += 1 - if multiworld.rando_love[player]: + if world.options.rando_love: set_rule(multiworld.get_location(("LOVE " + str(maxlv)), player), lambda state: False) - if multiworld.rando_stats[player]: + if world.options.rando_stats: set_rule(multiworld.get_location(("ATK "+str(maxlv)), player), lambda state: False) set_rule(multiworld.get_location(("HP "+str(maxlv)), player), lambda state: False) if maxlv in {5, 9, 13, 17}: set_rule(multiworld.get_location(("DEF "+str(maxlv)), player), lambda state: False) maxlv = 1 while maxlv < 20: - while _undertale_can_level(multiworld.state, exp, maxlv): + while _undertale_can_level(exp, maxlv): maxlv += 1 - if multiworld.rando_stats[player]: + if world.options.rando_stats: if curarea == "Old Home": add_rule(multiworld.get_location(("ATK "+str(maxlv)), player), lambda state: (state.can_reach("Old Home", "Region", player)), combine="or") @@ -197,7 +206,7 @@ def set_rules(multiworld: MultiWorld, player: int): if maxlv == 5 or maxlv == 9 or maxlv == 13 or maxlv == 17: add_rule(multiworld.get_location(("DEF "+str(maxlv)), player), lambda state: (state.can_reach("New Home Exit", "Entrance", player)), combine="or") - if multiworld.rando_love[player]: + if world.options.rando_love: if curarea == "Old Home": add_rule(multiworld.get_location(("LOVE "+str(maxlv)), player), lambda state: (state.can_reach("Old Home", "Region", player)), combine="or") @@ -307,9 +316,9 @@ def set_rules(multiworld: MultiWorld, player: int): # Sets rules on completion condition -def set_completion_rules(multiworld: MultiWorld, player: int): - completion_requirements = lambda state: state.can_reach("Barrier", "Region", player) - if _undertale_is_route(multiworld.state, player, 1): - completion_requirements = lambda state: state.can_reach("True Lab", "Region", player) - - multiworld.completion_condition[player] = lambda state: completion_requirements(state) +def set_completion_rules(world: "UndertaleWorld"): + player = world.player + multiworld = world.multiworld + multiworld.completion_condition[player] = lambda state: state.can_reach("Barrier", "Region", player) + if _undertale_is_route(world, 1): + multiworld.completion_condition[player] = lambda state: state.can_reach("True Lab", "Region", player) diff --git a/worlds/undertale/__init__.py b/worlds/undertale/__init__.py index b87d3ac01e8f..9084c77b0065 100644 --- a/worlds/undertale/__init__.py +++ b/worlds/undertale/__init__.py @@ -5,9 +5,9 @@ from .Rules import set_rules, set_completion_rules from worlds.generic.Rules import exclusion_rules from BaseClasses import Region, Entrance, Tutorial, Item -from .Options import undertale_options +from .Options import UndertaleOptions from worlds.AutoWorld import World, WebWorld -from worlds.LauncherComponents import Component, components, Type +from worlds.LauncherComponents import Component, components from multiprocessing import Process @@ -46,7 +46,8 @@ class UndertaleWorld(World): from their underground prison. """ game = "Undertale" - option_definitions = undertale_options + options_dataclass = UndertaleOptions + options: UndertaleOptions web = UndertaleWeb() item_name_to_id = {name: data.code for name, data in item_table.items()} @@ -54,39 +55,55 @@ class UndertaleWorld(World): def _get_undertale_data(self): return { - "world_seed": self.multiworld.per_slot_randoms[self.player].getrandbits(32), + "world_seed": self.random.getrandbits(32), "seed_name": self.multiworld.seed_name, "player_name": self.multiworld.get_player_name(self.player), "player_id": self.player, "client_version": self.required_client_version, "race": self.multiworld.is_race, - "route": self.multiworld.route_required[self.player].current_key, - "starting_area": self.multiworld.starting_area[self.player].current_key, - "temy_armor_include": bool(self.multiworld.temy_include[self.player].value), - "only_flakes": bool(self.multiworld.only_flakes[self.player].value), - "no_equips": bool(self.multiworld.no_equips[self.player].value), - "key_hunt": bool(self.multiworld.key_hunt[self.player].value), - "key_pieces": self.multiworld.key_pieces[self.player].value, - "rando_love": bool(self.multiworld.rando_love[self.player].value), - "rando_stats": bool(self.multiworld.rando_stats[self.player].value), - "prog_armor": bool(self.multiworld.prog_armor[self.player].value), - "prog_weapons": bool(self.multiworld.prog_weapons[self.player].value), - "rando_item_button": bool(self.multiworld.rando_item_button[self.player].value) + "route": self.options.route_required.current_key, + "starting_area": self.options.starting_area.current_key, + "temy_armor_include": bool(self.options.temy_include.value), + "only_flakes": bool(self.options.only_flakes.value), + "no_equips": bool(self.options.no_equips.value), + "key_hunt": bool(self.options.key_hunt.value), + "key_pieces": self.options.key_pieces.value, + "rando_love": bool(self.options.rando_love.value), + "rando_stats": bool(self.options.rando_stats.value), + "prog_armor": bool(self.options.prog_armor.value), + "prog_weapons": bool(self.options.prog_weapons.value), + "rando_item_button": bool(self.options.rando_item_button.value) } + def get_filler_item_name(self): + if self.options.route_required == "all_routes": + junk_pool = junk_weights_all + elif self.options.route_required == "genocide": + junk_pool = junk_weights_genocide + elif self.options.route_required == "neutral": + junk_pool = junk_weights_neutral + elif self.options.route_required == "pacifist": + junk_pool = junk_weights_pacifist + else: + junk_pool = junk_weights_all + if not self.options.only_flakes: + return self.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()))[0] + else: + return "Temmie Flakes" + def create_items(self): self.multiworld.get_location("Undyne Date", self.player).place_locked_item(self.create_item("Undyne Date")) self.multiworld.get_location("Alphys Date", self.player).place_locked_item(self.create_item("Alphys Date")) self.multiworld.get_location("Papyrus Date", self.player).place_locked_item(self.create_item("Papyrus Date")) # Generate item pool itempool = [] - if self.multiworld.route_required[self.player] == "all_routes": + if self.options.route_required == "all_routes": junk_pool = junk_weights_all.copy() - elif self.multiworld.route_required[self.player] == "genocide": + elif self.options.route_required == "genocide": junk_pool = junk_weights_genocide.copy() - elif self.multiworld.route_required[self.player] == "neutral": + elif self.options.route_required == "neutral": junk_pool = junk_weights_neutral.copy() - elif self.multiworld.route_required[self.player] == "pacifist": + elif self.options.route_required == "pacifist": junk_pool = junk_weights_pacifist.copy() else: junk_pool = junk_weights_all.copy() @@ -99,73 +116,68 @@ def create_items(self): itempool += [name] * num for name, num in non_key_items.items(): itempool += [name] * num - if self.multiworld.rando_item_button[self.player]: + if self.options.rando_item_button: itempool += ["ITEM"] else: self.multiworld.push_precollected(self.create_item("ITEM")) self.multiworld.push_precollected(self.create_item("FIGHT")) self.multiworld.push_precollected(self.create_item("ACT")) self.multiworld.push_precollected(self.create_item("MERCY")) - if self.multiworld.route_required[self.player] == "genocide": + if self.options.route_required == "genocide": itempool = [item for item in itempool if item != "Popato Chisps" and item != "Stained Apron" and item != "Nice Cream" and item != "Hot Cat" and item != "Hot Dog...?" and item != "Punch Card"] - elif self.multiworld.route_required[self.player] == "neutral": + elif self.options.route_required == "neutral": itempool = [item for item in itempool if item != "Popato Chisps" and item != "Hot Cat" and item != "Hot Dog...?"] - if self.multiworld.route_required[self.player] == "pacifist" or \ - self.multiworld.route_required[self.player] == "all_routes": + if self.options.route_required == "pacifist" or self.options.route_required == "all_routes": itempool += ["Undyne Letter EX"] else: itempool.remove("Complete Skeleton") itempool.remove("Fish") itempool.remove("DT Extractor") itempool.remove("Hush Puppy") - if self.multiworld.key_hunt[self.player]: - itempool += ["Key Piece"] * self.multiworld.key_pieces[self.player].value + if self.options.key_hunt: + itempool += ["Key Piece"] * self.options.key_pieces.value else: itempool += ["Left Home Key"] itempool += ["Right Home Key"] - if not self.multiworld.rando_love[self.player] or \ - (self.multiworld.route_required[self.player] != "genocide" and - self.multiworld.route_required[self.player] != "all_routes"): + if not self.options.rando_love or \ + (self.options.route_required != "genocide" and self.options.route_required != "all_routes"): itempool = [item for item in itempool if not item == "LOVE"] - if not self.multiworld.rando_stats[self.player] or \ - (self.multiworld.route_required[self.player] != "genocide" and - self.multiworld.route_required[self.player] != "all_routes"): + if not self.options.rando_stats or \ + (self.options.route_required != "genocide" and self.options.route_required != "all_routes"): itempool = [item for item in itempool if not (item == "ATK Up" or item == "DEF Up" or item == "HP Up")] - if self.multiworld.temy_include[self.player]: + if self.options.temy_include: itempool += ["temy armor"] - if self.multiworld.no_equips[self.player]: + if self.options.no_equips: itempool = [item for item in itempool if item not in required_armor and item not in required_weapons] else: - if self.multiworld.prog_armor[self.player]: + if self.options.prog_armor: itempool = [item if (item not in required_armor and not item == "temy armor") else "Progressive Armor" for item in itempool] - if self.multiworld.prog_weapons[self.player]: + if self.options.prog_weapons: itempool = [item if item not in required_weapons else "Progressive Weapons" for item in itempool] - if self.multiworld.route_required[self.player] == "genocide" or \ - self.multiworld.route_required[self.player] == "all_routes": - if not self.multiworld.only_flakes[self.player]: + if self.options.route_required == "genocide" or \ + self.options.route_required == "all_routes": + if not self.options.only_flakes: itempool += ["Snowman Piece"] * 2 - if not self.multiworld.no_equips[self.player]: + if not self.options.no_equips: itempool = ["Real Knife" if item == "Worn Dagger" else "The Locket" if item == "Heart Locket" else item for item in itempool] - if self.multiworld.only_flakes[self.player]: + if self.options.only_flakes: itempool = [item for item in itempool if item not in non_key_items] - starting_key = self.multiworld.starting_area[self.player].current_key.title() + " Key" + starting_key = self.options.starting_area.current_key.title() + " Key" itempool.remove(starting_key) self.multiworld.push_precollected(self.create_item(starting_key)) # Choose locations to automatically exclude based on settings exclusion_pool = set() - exclusion_pool.update(exclusion_table[self.multiworld.route_required[self.player].current_key]) - if not self.multiworld.rando_love[self.player] or \ - (self.multiworld.route_required[self.player] != "genocide" and - self.multiworld.route_required[self.player] != "all_routes"): + exclusion_pool.update(exclusion_table[self.options.route_required.current_key]) + if not self.options.rando_love or \ + (self.options.route_required != "genocide" and self.options.route_required != "all_routes"): exclusion_pool.update(exclusion_table["NoLove"]) - if not self.multiworld.rando_stats[self.player] or \ - (self.multiworld.route_required[self.player] != "genocide" and - self.multiworld.route_required[self.player] != "all_routes"): + if not self.options.rando_stats or \ + (self.options.route_required != "genocide" and self.options.route_required != "all_routes"): exclusion_pool.update(exclusion_table["NoStats"]) # Choose locations to automatically exclude based on settings @@ -173,36 +185,33 @@ def create_items(self): exclusion_checks.update(["Nicecream Punch Card", "Hush Trade"]) exclusion_rules(self.multiworld, self.player, exclusion_checks) - # Fill remaining items with randomly generated junk or Temmie Flakes - if not self.multiworld.only_flakes[self.player]: - itempool += self.multiworld.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()), - k=len(self.location_names)-len(itempool)-len(exclusion_pool)) - else: - itempool += ["Temmie Flakes"] * (len(self.location_names) - len(itempool) - len(exclusion_pool)) # Convert itempool into real items itempool = [item for item in map(lambda name: self.create_item(name), itempool)] + # Fill remaining items with randomly generated junk or Temmie Flakes + while len(itempool) < len(self.multiworld.get_unfilled_locations(self.player)): + itempool.append(self.create_filler()) self.multiworld.itempool += itempool def set_rules(self): - set_rules(self.multiworld, self.player) - set_completion_rules(self.multiworld, self.player) + set_rules(self) + set_completion_rules(self) def create_regions(self): def UndertaleRegion(region_name: str, exits=[]): ret = Region(region_name, self.player, self.multiworld) ret.locations += [UndertaleAdvancement(self.player, loc_name, loc_data.id, ret) - for loc_name, loc_data in advancement_table.items() - if loc_data.region == region_name and - (loc_name not in exclusion_table["NoStats"] or - (self.multiworld.rando_stats[self.player] and - (self.multiworld.route_required[self.player] == "genocide" or - self.multiworld.route_required[self.player] == "all_routes"))) and - (loc_name not in exclusion_table["NoLove"] or - (self.multiworld.rando_love[self.player] and - (self.multiworld.route_required[self.player] == "genocide" or - self.multiworld.route_required[self.player] == "all_routes"))) and - loc_name not in exclusion_table[self.multiworld.route_required[self.player].current_key]] + for loc_name, loc_data in advancement_table.items() + if loc_data.region == region_name and + (loc_name not in exclusion_table["NoStats"] or + (self.options.rando_stats and + (self.options.route_required == "genocide" or + self.options.route_required == "all_routes"))) and + (loc_name not in exclusion_table["NoLove"] or + (self.options.rando_love and + (self.options.route_required == "genocide" or + self.options.route_required == "all_routes"))) and + loc_name not in exclusion_table[self.options.route_required.current_key]] for exit in exits: ret.exits.append(Entrance(self.player, exit, ret)) return ret @@ -212,11 +221,11 @@ def UndertaleRegion(region_name: str, exits=[]): def fill_slot_data(self): slot_data = self._get_undertale_data() - for option_name in undertale_options: + for option_name in self.options.as_dict(): option = getattr(self.multiworld, option_name)[self.player] if (option_name == "rando_love" or option_name == "rando_stats") and \ - self.multiworld.route_required[self.player] != "genocide" and \ - self.multiworld.route_required[self.player] != "all_routes": + self.options.route_required != "genocide" and \ + self.options.route_required != "all_routes": option.value = False if slot_data.get(option_name, None) is None and type(option.value) in {str, int}: slot_data[option_name] = int(option.value) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index ecab25db3d71..254064098db9 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -11,11 +11,12 @@ from worlds.AutoWorld import WebWorld, World from .data import static_items as static_witness_items +from .data import static_locations as static_witness_locations from .data import static_logic as static_witness_logic from .data.item_definition_classes import DoorItemDefinition, ItemData from .data.utils import get_audio_logs from .hints import CompactItemData, create_all_hints, make_compact_hint_data, make_laser_hints -from .locations import WitnessPlayerLocations, static_witness_locations +from .locations import WitnessPlayerLocations from .options import TheWitnessOptions, witness_option_groups from .player_items import WitnessItem, WitnessPlayerItems from .player_logic import WitnessPlayerLogic @@ -53,7 +54,8 @@ class WitnessWorld(World): options: TheWitnessOptions item_name_to_id = { - name: data.ap_code for name, data in static_witness_items.ITEM_DATA.items() + # ITEM_DATA doesn't have any event items in it + name: cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items() } location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID item_name_groups = static_witness_items.ITEM_GROUPS @@ -142,7 +144,7 @@ def generate_early(self) -> None: ) self.player_regions: WitnessPlayerRegions = WitnessPlayerRegions(self.player_locations, self) - self.log_ids_to_hints = dict() + self.log_ids_to_hints = {} self.determine_sufficient_progression() @@ -183,21 +185,22 @@ def create_regions(self) -> None: self.items_placed_early.append("Puzzle Skip") - # Pick an early item to place on the tutorial gate. - early_items = [ - item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items() - ] - if early_items: - random_early_item = self.random.choice(early_items) - if self.options.puzzle_randomization == "sigma_expert": - # In Expert, only tag the item as early, rather than forcing it onto the gate. - self.multiworld.local_early_items[self.player][random_early_item] = 1 - else: - # Force the item onto the tutorial gate check and remove it from our random pool. - gate_item = self.create_item(random_early_item) - self.get_location("Tutorial Gate Open").place_locked_item(gate_item) - self.own_itempool.append(gate_item) - self.items_placed_early.append(random_early_item) + if self.options.early_symbol_item: + # Pick an early item to place on the tutorial gate. + early_items = [ + item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items() + ] + if early_items: + random_early_item = self.random.choice(early_items) + if self.options.puzzle_randomization == "sigma_expert": + # In Expert, only tag the item as early, rather than forcing it onto the gate. + self.multiworld.local_early_items[self.player][random_early_item] = 1 + else: + # Force the item onto the tutorial gate check and remove it from our random pool. + gate_item = self.create_item(random_early_item) + self.get_location("Tutorial Gate Open").place_locked_item(gate_item) + self.own_itempool.append(gate_item) + self.items_placed_early.append(random_early_item) # There are some really restrictive settings in The Witness. # They are rarely played, but when they are, we add some extra sphere 1 locations. @@ -279,7 +282,7 @@ def create_items(self) -> None: remaining_item_slots = pool_size - sum(item_pool.values()) # Add puzzle skips. - num_puzzle_skips = self.options.puzzle_skip_amount + num_puzzle_skips = self.options.puzzle_skip_amount.value if num_puzzle_skips > remaining_item_slots: warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world has insufficient locations" @@ -301,21 +304,21 @@ def create_items(self) -> None: if self.player_items.item_data[item_name].local_only: self.options.local_items.value.add(item_name) - def fill_slot_data(self) -> dict: - self.log_ids_to_hints: Dict[int, CompactItemData] = dict() - self.laser_ids_to_hints: Dict[int, CompactItemData] = dict() + def fill_slot_data(self) -> Dict[str, Any]: + self.log_ids_to_hints: Dict[int, CompactItemData] = {} + self.laser_ids_to_hints: Dict[int, CompactItemData] = {} already_hinted_locations = set() # Laser hints if self.options.laser_hints: - laser_hints = make_laser_hints(self, static_witness_items.ITEM_GROUPS["Lasers"]) + laser_hints = make_laser_hints(self, sorted(static_witness_items.ITEM_GROUPS["Lasers"])) for item_name, hint in laser_hints.items(): item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]) self.laser_ids_to_hints[int(item_def.panel_id_hexes[0], 16)] = make_compact_hint_data(hint, self.player) - already_hinted_locations.add(hint.location) + already_hinted_locations.add(cast(Location, hint.location)) # Audio Log Hints @@ -378,13 +381,13 @@ class WitnessLocation(Location): game: str = "The Witness" entity_hex: int = -1 - def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1) -> None: + def __init__(self, player: int, name: str, address: Optional[int], parent: Region, ch_hex: int = -1) -> None: super().__init__(player, name, address, parent) self.entity_hex = ch_hex def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlayerLocations, - region_locations=None, exits=None) -> Region: + region_locations: Optional[List[str]] = None, exits: Optional[List[str]] = None) -> Region: """ Create an Archipelago Region for The Witness """ @@ -399,11 +402,11 @@ def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlaye entity_hex = int( static_witness_logic.ENTITIES_BY_NAME[location]["entity_hex"], 0 ) - location = WitnessLocation( + location_obj = WitnessLocation( world.player, location, loc_id, ret, entity_hex ) - ret.locations.append(location) + ret.locations.append(location_obj) if exits: for single_exit in exits: ret.exits.append(Entrance(world.player, single_exit, ret)) diff --git a/worlds/witness/data/static_items.py b/worlds/witness/data/static_items.py index 8eb889f8203a..b0d8fc3c4f6e 100644 --- a/worlds/witness/data/static_items.py +++ b/worlds/witness/data/static_items.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict, List, Set from BaseClasses import ItemClassification @@ -7,7 +7,7 @@ from .static_locations import ID_START ITEM_DATA: Dict[str, ItemData] = {} -ITEM_GROUPS: Dict[str, List[str]] = {} +ITEM_GROUPS: Dict[str, Set[str]] = {} # Useful items that are treated specially at generation time and should not be automatically added to the player's # item list during get_progression_items. @@ -22,13 +22,13 @@ def populate_items() -> None: if definition.category is ItemCategory.SYMBOL: classification = ItemClassification.progression - ITEM_GROUPS.setdefault("Symbols", []).append(item_name) + ITEM_GROUPS.setdefault("Symbols", set()).add(item_name) elif definition.category is ItemCategory.DOOR: classification = ItemClassification.progression - ITEM_GROUPS.setdefault("Doors", []).append(item_name) + ITEM_GROUPS.setdefault("Doors", set()).add(item_name) elif definition.category is ItemCategory.LASER: classification = ItemClassification.progression_skip_balancing - ITEM_GROUPS.setdefault("Lasers", []).append(item_name) + ITEM_GROUPS.setdefault("Lasers", set()).add(item_name) elif definition.category is ItemCategory.USEFUL: classification = ItemClassification.useful elif definition.category is ItemCategory.FILLER: @@ -47,7 +47,7 @@ def populate_items() -> None: def get_item_to_door_mappings() -> Dict[int, List[int]]: output: Dict[int, List[int]] = {} for item_name, item_data in ITEM_DATA.items(): - if not isinstance(item_data.definition, DoorItemDefinition): + if not isinstance(item_data.definition, DoorItemDefinition) or item_data.ap_code is None: continue output[item_data.ap_code] = [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes] return output diff --git a/worlds/witness/data/static_locations.py b/worlds/witness/data/static_locations.py index e11544235ffc..de321d20c0f9 100644 --- a/worlds/witness/data/static_locations.py +++ b/worlds/witness/data/static_locations.py @@ -1,3 +1,5 @@ +from typing import Dict, Set, cast + from . import static_logic as static_witness_logic ID_START = 158000 @@ -441,17 +443,17 @@ "Town Obelisk Side 6", } -ALL_LOCATIONS_TO_ID = dict() +ALL_LOCATIONS_TO_ID: Dict[str, int] = {} -AREA_LOCATION_GROUPS = dict() +AREA_LOCATION_GROUPS: Dict[str, Set[str]] = {} -def get_id(entity_hex: str) -> str: +def get_id(entity_hex: str) -> int: """ Calculates the location ID for any given location """ - return static_witness_logic.ENTITIES_BY_HEX[entity_hex]["id"] + return cast(int, static_witness_logic.ENTITIES_BY_HEX[entity_hex]["id"]) def get_event_name(entity_hex: str) -> str: @@ -461,7 +463,7 @@ def get_event_name(entity_hex: str) -> str: action = " Opened" if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] == "Door" else " Solved" - return static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"] + action + return cast(str, static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"]) + action ALL_LOCATIONS_TO_IDS = { @@ -479,4 +481,4 @@ def get_event_name(entity_hex: str) -> str: for loc in ALL_LOCATIONS_TO_IDS: area = static_witness_logic.ENTITIES_BY_NAME[loc]["area"]["name"] - AREA_LOCATION_GROUPS.setdefault(area, []).append(loc) + AREA_LOCATION_GROUPS.setdefault(area, set()).add(loc) diff --git a/worlds/witness/data/static_logic.py b/worlds/witness/data/static_logic.py index ecd95ea6c0fa..a9175c0c30b3 100644 --- a/worlds/witness/data/static_logic.py +++ b/worlds/witness/data/static_logic.py @@ -1,5 +1,5 @@ from collections import defaultdict -from typing import Dict, List, Set, Tuple +from typing import Any, Dict, List, Optional, Set, Tuple from Utils import cache_argsless @@ -24,13 +24,37 @@ class StaticWitnessLogicObj: - def read_logic_file(self, lines) -> None: + def __init__(self, lines: Optional[List[str]] = None) -> None: + if lines is None: + lines = get_sigma_normal_logic() + + # All regions with a list of panels in them and the connections to other regions, before logic adjustments + self.ALL_REGIONS_BY_NAME: Dict[str, Dict[str, Any]] = {} + self.ALL_AREAS_BY_NAME: Dict[str, Dict[str, Any]] = {} + self.CONNECTIONS_WITH_DUPLICATES: Dict[str, Dict[str, Set[WitnessRule]]] = defaultdict(lambda: defaultdict(set)) + self.STATIC_CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = {} + + self.ENTITIES_BY_HEX: Dict[str, Dict[str, Any]] = {} + self.ENTITIES_BY_NAME: Dict[str, Dict[str, Any]] = {} + self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = {} + + self.OBELISK_SIDE_ID_TO_EP_HEXES: Dict[int, Set[int]] = {} + + self.EP_TO_OBELISK_SIDE: Dict[str, str] = {} + + self.ENTITY_ID_TO_NAME: Dict[str, str] = {} + + self.read_logic_file(lines) + self.reverse_connections() + self.combine_connections() + + def read_logic_file(self, lines: List[str]) -> None: """ Reads the logic file and does the initial population of data structures """ - current_region = dict() - current_area = { + current_region = {} + current_area: Dict[str, Any] = { "name": "Misc", "regions": [], } @@ -155,7 +179,7 @@ def read_logic_file(self, lines) -> None: current_region["entities"].append(entity_hex) current_region["physical_entities"].append(entity_hex) - def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]): + def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]) -> None: target = connection[0] traversal_options = connection[1] @@ -169,13 +193,13 @@ def reverse_connection(self, source_region: str, connection: Tuple[str, Set[Witn if remaining_options: self.CONNECTIONS_WITH_DUPLICATES[target][source_region].add(frozenset(remaining_options)) - def reverse_connections(self): + def reverse_connections(self) -> None: # Iterate all connections for region_name, connections in list(self.CONNECTIONS_WITH_DUPLICATES.items()): for connection in connections.items(): self.reverse_connection(region_name, connection) - def combine_connections(self): + def combine_connections(self) -> None: # All regions need to be present, and this dict is copied later - Thus, defaultdict is not the correct choice. self.STATIC_CONNECTIONS_BY_REGION_NAME = {region_name: set() for region_name in self.ALL_REGIONS_BY_NAME} @@ -184,30 +208,6 @@ def combine_connections(self): combined_req = logical_or_witness_rules(requirement) self.STATIC_CONNECTIONS_BY_REGION_NAME[source].add((target, combined_req)) - def __init__(self, lines=None) -> None: - if lines is None: - lines = get_sigma_normal_logic() - - # All regions with a list of panels in them and the connections to other regions, before logic adjustments - self.ALL_REGIONS_BY_NAME = dict() - self.ALL_AREAS_BY_NAME = dict() - self.CONNECTIONS_WITH_DUPLICATES = defaultdict(lambda: defaultdict(lambda: set())) - self.STATIC_CONNECTIONS_BY_REGION_NAME = dict() - - self.ENTITIES_BY_HEX = dict() - self.ENTITIES_BY_NAME = dict() - self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict() - - self.OBELISK_SIDE_ID_TO_EP_HEXES = dict() - - self.EP_TO_OBELISK_SIDE = dict() - - self.ENTITY_ID_TO_NAME = dict() - - self.read_logic_file(lines) - self.reverse_connections() - self.combine_connections() - # Item data parsed from WitnessItems.txt ALL_ITEMS: Dict[str, ItemDefinition] = {} @@ -276,12 +276,12 @@ def get_sigma_expert() -> StaticWitnessLogicObj: return StaticWitnessLogicObj(get_sigma_expert_logic()) -def __getattr__(name): +def __getattr__(name: str) -> StaticWitnessLogicObj: if name == "vanilla": return get_vanilla() - elif name == "sigma_normal": + if name == "sigma_normal": return get_sigma_normal() - elif name == "sigma_expert": + if name == "sigma_expert": return get_sigma_expert() raise AttributeError(f"module '{__name__}' has no attribute '{name}'") diff --git a/worlds/witness/data/utils.py b/worlds/witness/data/utils.py index 2934308df3ec..f89aaf7d3e18 100644 --- a/worlds/witness/data/utils.py +++ b/worlds/witness/data/utils.py @@ -1,7 +1,9 @@ from math import floor from pkgutil import get_data -from random import random -from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple +from random import Random +from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple, TypeVar + +T = TypeVar("T") # A WitnessRule is just an or-chain of and-conditions. # It represents the set of all options that could fulfill this requirement. @@ -11,9 +13,9 @@ WitnessRule = FrozenSet[FrozenSet[str]] -def weighted_sample(world_random: random, population: List, weights: List[float], k: int) -> List: +def weighted_sample(world_random: Random, population: List[T], weights: List[float], k: int) -> List[T]: positions = range(len(population)) - indices = [] + indices: List[int] = [] while True: needed = k - len(indices) if not needed: @@ -82,13 +84,13 @@ def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str region_obj = { "name": region_name, "shortName": region_name_simple, - "entities": list(), - "physical_entities": list(), + "entities": [], + "physical_entities": [], } return region_obj, options -def parse_lambda(lambda_string) -> WitnessRule: +def parse_lambda(lambda_string: str) -> WitnessRule: """ Turns a lambda String literal like this: a | b & c into a set of sets like this: {{a}, {b, c}} @@ -97,18 +99,18 @@ def parse_lambda(lambda_string) -> WitnessRule: if lambda_string == "True": return frozenset([frozenset()]) split_ands = set(lambda_string.split(" | ")) - lambda_set = frozenset({frozenset(a.split(" & ")) for a in split_ands}) - - return lambda_set + return frozenset({frozenset(a.split(" & ")) for a in split_ands}) -_adjustment_file_cache = dict() +_adjustment_file_cache = {} def get_adjustment_file(adjustment_file: str) -> List[str]: if adjustment_file not in _adjustment_file_cache: - data = get_data(__name__, adjustment_file).decode("utf-8") - _adjustment_file_cache[adjustment_file] = [line.strip() for line in data.split("\n")] + data = get_data(__name__, adjustment_file) + if data is None: + raise FileNotFoundError(f"Could not find {adjustment_file}") + _adjustment_file_cache[adjustment_file] = [line.strip() for line in data.decode("utf-8").split("\n")] return _adjustment_file_cache[adjustment_file] @@ -237,7 +239,7 @@ def logical_and_witness_rules(witness_rules: Iterable[WitnessRule]) -> WitnessRu A logical formula might look like this: {{a, b}, {c, d}}, which would mean "a & b | c & d". These can be easily and-ed by just using the boolean distributive law: (a | b) & c = a & c | a & b. """ - current_overall_requirement = frozenset({frozenset()}) + current_overall_requirement: FrozenSet[FrozenSet[str]] = frozenset({frozenset()}) for next_dnf_requirement in witness_rules: new_requirement: Set[FrozenSet[str]] = set() diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 535a36e13b6f..a1ca1b081d3c 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -1,11 +1,12 @@ import logging from dataclasses import dataclass -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld from .data import static_logic as static_witness_logic from .data.utils import weighted_sample +from .player_items import WitnessItem if TYPE_CHECKING: from . import WitnessWorld @@ -22,7 +23,9 @@ class WitnessLocationHint: def __hash__(self) -> int: return hash(self.location) - def __eq__(self, other) -> bool: + def __eq__(self, other: Any) -> bool: + if not isinstance(other, WitnessLocationHint): + return False return self.location == other.location @@ -171,9 +174,13 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")" item = hint.location.item - item_name = item.name - if item.player != world.player: - item_name += " (" + world.multiworld.get_player_name(item.player) + ")" + + item_name = "Nothing" + if item is not None: + item_name = item.name + + if item.player != world.player: + item_name += " (" + world.multiworld.get_player_name(item.player) + ")" if hint.hint_came_from_location: hint_text = f"{location_name} contains {item_name}." @@ -183,14 +190,17 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes return WitnessWordedHint(hint_text, hint.location) -def hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]) -> Optional[WitnessLocationHint]: - def get_real_location(multiworld: MultiWorld, location: Location): +def hint_from_item(world: "WitnessWorld", item_name: str, + own_itempool: List["WitnessItem"]) -> Optional[WitnessLocationHint]: + def get_real_location(multiworld: MultiWorld, location: Location) -> Location: """If this location is from an item_link pseudo-world, get the location that the item_link item is on. Return the original location otherwise / as a fallback.""" if location.player not in world.multiworld.groups: return location try: + if not location.item: + return location return multiworld.find_item(location.item.name, location.player) except StopIteration: return location @@ -209,17 +219,11 @@ def get_real_location(multiworld: MultiWorld, location: Location): def hint_from_location(world: "WitnessWorld", location: str) -> Optional[WitnessLocationHint]: - location_obj = world.get_location(location) - item_obj = location_obj.item - item_name = item_obj.name - if item_obj.player != world.player: - item_name += " (" + world.multiworld.get_player_name(item_obj.player) + ")" - - return WitnessLocationHint(location_obj, True) + return WitnessLocationHint(world.get_location(location), True) def get_items_and_locations_in_random_order(world: "WitnessWorld", - own_itempool: List[Item]) -> Tuple[List[str], List[str]]: + own_itempool: List["WitnessItem"]) -> Tuple[List[str], List[str]]: prog_items_in_this_world = sorted( item.name for item in own_itempool if item.advancement and item.code and item.location @@ -235,7 +239,7 @@ def get_items_and_locations_in_random_order(world: "WitnessWorld", return prog_items_in_this_world, locations_in_this_world -def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List[Item], +def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List["WitnessItem"], already_hinted_locations: Set[Location] ) -> Tuple[List[WitnessLocationHint], List[WitnessLocationHint]]: prog_items_in_this_world, loc_in_this_world = get_items_and_locations_in_random_order(world, own_itempool) @@ -282,14 +286,14 @@ def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List[Ite return always_hints, priority_hints -def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List[Item], +def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List["WitnessItem"], already_hinted_locations: Set[Location], hints_to_use_first: List[WitnessLocationHint], unhinted_locations_for_hinted_areas: Dict[str, Set[Location]]) -> List[WitnessWordedHint]: prog_items_in_this_world, locations_in_this_world = get_items_and_locations_in_random_order(world, own_itempool) next_random_hint_is_location = world.random.randrange(0, 2) - hints = [] + hints: List[WitnessWordedHint] = [] # This is a way to reverse a Dict[a,List[b]] to a Dict[b,a] area_reverse_lookup = { @@ -304,6 +308,7 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp logging.warning(f"Ran out of items/locations to hint for player {player_name}.") break + location_hint: Optional[WitnessLocationHint] if hints_to_use_first: location_hint = hints_to_use_first.pop() elif next_random_hint_is_location and locations_in_this_world: @@ -317,7 +322,7 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp next_random_hint_is_location = not next_random_hint_is_location continue - if not location_hint or location_hint.location in already_hinted_locations: + if location_hint is None or location_hint.location in already_hinted_locations: continue # Don't hint locations in areas that are almost fully hinted out already @@ -344,8 +349,8 @@ def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[st When this happens, they are made less likely to receive an area hint. """ - unhinted_locations_per_area = dict() - unhinted_location_percentage_per_area = dict() + unhinted_locations_per_area = {} + unhinted_location_percentage_per_area = {} for area_name, locations in locations_per_area.items(): not_yet_hinted_locations = sum(location not in already_hinted_locations for location in locations) @@ -368,8 +373,8 @@ def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[st def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]], Dict[str, List[Item]]]: potential_areas = list(static_witness_logic.ALL_AREAS_BY_NAME.keys()) - locations_per_area = dict() - items_per_area = dict() + locations_per_area = {} + items_per_area = {} for area in potential_areas: regions = [ @@ -533,7 +538,7 @@ def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int, location_hints_created_in_round_1 = len(generated_hints) - unhinted_locations_per_area: Dict[str, Set[Location]] = dict() + unhinted_locations_per_area: Dict[str, Set[Location]] = {} # Then, make area hints. if area_hints: @@ -584,17 +589,29 @@ def make_compact_hint_data(hint: WitnessWordedHint, local_player_number: int) -> location = hint.location area_amount = hint.area_amount - # None if junk hint, address if location hint, area string if area hint - arg_1 = location.address if location else (hint.area if hint.area else None) + # -1 if junk hint, address if location hint, area string if area hint + arg_1: Union[str, int] + if location and location.address is not None: + arg_1 = location.address + elif hint.area is not None: + arg_1 = hint.area + else: + arg_1 = -1 # self.player if junk hint, player if location hint, progression amount if area hint - arg_2 = area_amount if area_amount is not None else (location.player if location else local_player_number) + arg_2: int + if area_amount is not None: + arg_2 = area_amount + elif location is not None: + arg_2 = location.player + else: + arg_2 = local_player_number return hint.wording, arg_1, arg_2 def make_laser_hints(world: "WitnessWorld", laser_names: List[str]) -> Dict[str, WitnessWordedHint]: - laser_hints_by_name = dict() + laser_hints_by_name = {} for item_name in laser_names: location_hint = hint_from_item(world, item_name, world.own_itempool) diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index df8214ac9221..1796f051b896 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -61,9 +61,7 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> N sorted(self.CHECK_PANELHEX_TO_ID.items(), key=lambda item: item[1]) ) - event_locations = { - p for p in player_logic.USED_EVENT_NAMES_BY_HEX - } + event_locations = set(player_logic.USED_EVENT_NAMES_BY_HEX) self.EVENT_LOCATION_TABLE = { static_witness_locations.get_event_name(entity_hex): None @@ -80,5 +78,5 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> N def add_location_late(self, entity_name: str) -> None: entity_hex = static_witness_logic.ENTITIES_BY_NAME[entity_name]["entity_hex"] - self.CHECK_LOCATION_TABLE[entity_hex] = entity_name + self.CHECK_LOCATION_TABLE[entity_hex] = static_witness_locations.get_id(entity_hex) self.CHECK_PANELHEX_TO_ID[entity_hex] = static_witness_locations.get_id(entity_hex) diff --git a/worlds/witness/options.py b/worlds/witness/options.py index f51d86ba22f3..4855fc715933 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -2,7 +2,7 @@ from schema import And, Schema -from Options import Choice, DefaultOnToggle, OptionDict, OptionGroup, PerGameCommonOptions, Range, Toggle +from Options import Choice, DefaultOnToggle, OptionDict, OptionGroup, PerGameCommonOptions, Range, Toggle, Visibility from .data import static_logic as static_witness_logic from .data.item_definition_classes import ItemCategory, WeightedItemDefinition @@ -35,6 +35,14 @@ class EarlyCaves(Choice): alias_on = 2 +class EarlySymbolItem(DefaultOnToggle): + """ + Put a random helpful symbol item on an early check, specifically Tutorial Gate Open if it is available early. + """ + + visibility = Visibility.none + + class ShuffleSymbols(DefaultOnToggle): """ If on, you will need to unlock puzzle symbols as items to be able to solve the panels that contain those symbols. @@ -325,6 +333,7 @@ class TheWitnessOptions(PerGameCommonOptions): mountain_lasers: MountainLasers challenge_lasers: ChallengeLasers early_caves: EarlyCaves + early_symbol_item: EarlySymbolItem elevators_come_to_you: ElevatorsComeToYou trap_percentage: TrapPercentage trap_weights: TrapWeights diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 627e5acccb90..718fd7d172ba 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -2,7 +2,7 @@ Defines progression, junk and event items for The Witness """ import copy -from typing import TYPE_CHECKING, Dict, List, Set +from typing import TYPE_CHECKING, Dict, List, Set, cast from BaseClasses import Item, ItemClassification, MultiWorld @@ -87,7 +87,8 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, if data.classification == ItemClassification.useful}.items(): if item_name in static_witness_items._special_usefuls: continue - elif item_name == "Energy Capacity": + + if item_name == "Energy Capacity": self._mandatory_items[item_name] = NUM_ENERGY_UPGRADES elif isinstance(item_data.classification, ProgressiveItemDefinition): self._mandatory_items[item_name] = len(item_data.mappings) @@ -184,15 +185,16 @@ def get_early_items(self) -> List[str]: output -= {item for item, weight in inner_item.items() if weight} # Sort the output for consistency across versions if the implementation changes but the logic does not. - return sorted(list(output)) + return sorted(output) def get_door_ids_in_pool(self) -> List[int]: """ Returns the total set of all door IDs that are controlled by items in the pool. """ output: List[int] = [] - for item_name, item_data in {name: data for name, data in self.item_data.items() - if isinstance(data.definition, DoorItemDefinition)}.items(): + for item_name, item_data in dict(self.item_data.items()).items(): + if not isinstance(item_data.definition, DoorItemDefinition): + continue output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes] return output @@ -201,18 +203,21 @@ def get_symbol_ids_not_in_pool(self) -> List[int]: """ Returns the item IDs of symbol items that were defined in the configuration file but are not in the pool. """ - return [data.ap_code for name, data in static_witness_items.ITEM_DATA.items() - if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL] + return [ + # data.ap_code is guaranteed for a symbol definition + cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items() + if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL + ] def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]: output: Dict[int, List[int]] = {} - for item_name, quantity in {name: quantity for name, quantity in self._mandatory_items.items()}.items(): + for item_name, quantity in dict(self._mandatory_items.items()).items(): item = self.item_data[item_name] if isinstance(item.definition, ProgressiveItemDefinition): # Note: we need to reference the static table here rather than the player-specific one because the child # items were removed from the pool when we pruned out all progression items not in the settings. - output[item.ap_code] = [static_witness_items.ITEM_DATA[child_item].ap_code - for child_item in item.definition.child_item_names] + output[cast(int, item.ap_code)] = [cast(int, static_witness_items.ITEM_DATA[child_item].ap_code) + for child_item in item.definition.child_item_names] return output diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 4335f9524f1e..e8d11f43f51c 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -22,6 +22,7 @@ from .data import static_logic as static_witness_logic from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition +from .data.static_logic import StaticWitnessLogicObj from .data.utils import ( WitnessRule, define_new_region, @@ -58,6 +59,95 @@ class WitnessPlayerLogic: """WITNESS LOGIC CLASS""" + VICTORY_LOCATION: str + + def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]) -> None: + self.YAML_DISABLED_LOCATIONS: Set[str] = disabled_locations + self.YAML_ADDED_ITEMS: Dict[str, int] = start_inv + + self.EVENT_PANELS_FROM_PANELS: Set[str] = set() + self.EVENT_PANELS_FROM_REGIONS: Set[str] = set() + + self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES: Set[str] = set() + + self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY: Set[str] = set() + + self.UNREACHABLE_REGIONS: Set[str] = set() + + self.THEORETICAL_ITEMS: Set[str] = set() + self.THEORETICAL_ITEMS_NO_MULTI: Set[str] = set() + self.MULTI_AMOUNTS: Dict[str, int] = defaultdict(lambda: 1) + self.MULTI_LISTS: Dict[str, List[str]] = {} + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: Set[str] = set() + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set() + self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {} + self.STARTING_INVENTORY: Set[str] = set() + + self.DIFFICULTY = world.options.puzzle_randomization + + self.REFERENCE_LOGIC: StaticWitnessLogicObj + if self.DIFFICULTY == "sigma_expert": + self.REFERENCE_LOGIC = static_witness_logic.sigma_expert + elif self.DIFFICULTY == "none": + self.REFERENCE_LOGIC = static_witness_logic.vanilla + else: + self.REFERENCE_LOGIC = static_witness_logic.sigma_normal + + self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy( + self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME + ) + self.CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy( + self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME + ) + self.DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = copy.deepcopy( + self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX + ) + self.REQUIREMENTS_BY_HEX: Dict[str, WitnessRule] = {} + + self.EVENT_ITEM_PAIRS: Dict[str, str] = {} + self.COMPLETELY_DISABLED_ENTITIES: Set[str] = set() + self.DISABLE_EVERYTHING_BEHIND: Set[str] = set() + self.PRECOMPLETED_LOCATIONS: Set[str] = set() + self.EXCLUDED_LOCATIONS: Set[str] = set() + self.ADDED_CHECKS: Set[str] = set() + self.VICTORY_LOCATION = "0x0356B" + + self.ALWAYS_EVENT_NAMES_BY_HEX = { + "0x00509": "+1 Laser (Symmetry Laser)", + "0x012FB": "+1 Laser (Desert Laser)", + "0x09F98": "Desert Laser Redirection", + "0x01539": "+1 Laser (Quarry Laser)", + "0x181B3": "+1 Laser (Shadows Laser)", + "0x014BB": "+1 Laser (Keep Laser)", + "0x17C65": "+1 Laser (Monastery Laser)", + "0x032F9": "+1 Laser (Town Laser)", + "0x00274": "+1 Laser (Jungle Laser)", + "0x0C2B2": "+1 Laser (Bunker Laser)", + "0x00BF6": "+1 Laser (Swamp Laser)", + "0x028A4": "+1 Laser (Treehouse Laser)", + "0x17C34": "Mountain Entry", + "0xFFF00": "Bottom Floor Discard Turns On", + } + + self.USED_EVENT_NAMES_BY_HEX: Dict[str, str] = {} + self.CONDITIONAL_EVENTS: Dict[Tuple[str, str], str] = {} + + # The basic requirements to solve each entity come from StaticWitnessLogic. + # However, for any given world, the options (e.g. which item shuffles are enabled) affect the requirements. + self.make_options_adjustments(world) + self.determine_unrequired_entities(world) + self.find_unsolvable_entities(world) + + # After we have adjusted the raw requirements, we perform a dependency reduction for the entity requirements. + # This will make the access conditions way faster, instead of recursively checking dependent entities each time. + self.make_dependency_reduced_checklist() + + # Finalize which items actually exist in the MultiWorld and which get grouped into progressive items. + self.finalize_items() + + # Create event-item pairs for specific panels in the game. + self.make_event_panel_lists() + def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: """ Panels in this game often only turn on when other panels are solved. @@ -77,9 +167,9 @@ def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: # For the requirement of an entity, we consider two things: # 1. Any items this entity needs (e.g. Symbols or Door Items) - these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex].get("items", frozenset({frozenset()})) + these_items: WitnessRule = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex].get("items", frozenset({frozenset()})) # 2. Any entities that this entity depends on (e.g. one panel powering on the next panel in a set) - these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["entities"] + these_panels: WitnessRule = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["entities"] # Remove any items that don't actually exist in the settings (e.g. Symbol Shuffle turned off) these_items = frozenset({ @@ -91,47 +181,49 @@ def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: for subset in these_items: self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset) - # If this entity is opened by a door item that exists in the itempool, add that item to its requirements. - # Also, remove any original power requirements this entity might have had. + # Handle door entities (door shuffle) if entity_hex in self.DOOR_ITEMS_BY_ID: + # If this entity is opened by a door item that exists in the itempool, add that item to its requirements. door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]}) for dependent_item in door_items: self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependent_item) - all_options = logical_and_witness_rules([door_items, these_items]) + these_items = logical_and_witness_rules([door_items, these_items]) - # If this entity is not an EP, and it has an associated door item, ignore the original power dependencies - if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] != "EP": + # A door entity is opened by its door item instead of previous entities powering it. + # That means we need to ignore any dependent requirements. + # However, there are some entities that depend on other entities because of an environmental reason. + # Those requirements need to be preserved even in door shuffle. + entity_dependencies_need_to_be_preserved = ( + # EPs keep all their entity dependencies + static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] == "EP" # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved, # except in Expert, where that dependency doesn't exist, but now there *is* a power dependency. # In the future, it'd be wise to make a distinction between "power dependencies" and other dependencies. - if entity_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels): - these_items = all_options - + or entity_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels) # Another dependency that is not power-based: The Symmetry Island Upper Panel latches - elif entity_hex == "0x1C349": - these_items = all_options + or entity_hex == "0x1C349" + ) - else: - return frozenset(all_options) - - else: - these_items = all_options + # If this is not one of those special cases, solving this door entity only needs its own item requirement. + # Dependent entities from these_panels are ignored, and we just return these_items directly. + if not entity_dependencies_need_to_be_preserved: + return these_items # Now that we have item requirements and entity dependencies, it's time for the dependency reduction. # For each entity that this entity depends on (e.g. a panel turning on another panel), # Add that entities requirements to this entity. # If there are multiple options, consider each, and then or-chain them. - all_options = list() + all_options = [] for option in these_panels: - dependent_items_for_option = frozenset({frozenset()}) + dependent_items_for_option: WitnessRule = frozenset({frozenset()}) # For each entity in this option, resolve it to its actual requirement. for option_entity in option: - dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity) + dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity, {}) if option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect", "PP2 Weirdness", "Theater to Tunnels"}: @@ -399,6 +491,16 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: mnt_lasers = world.options.mountain_lasers chal_lasers = world.options.challenge_lasers + # Victory Condition + if victory == "elevator": + self.VICTORY_LOCATION = "0x3D9A9" + elif victory == "challenge": + self.VICTORY_LOCATION = "0x0356B" + elif victory == "mountain_box_short": + self.VICTORY_LOCATION = "0x09F7F" + elif victory == "mountain_box_long": + self.VICTORY_LOCATION = "0xFFF00" + # Exclude panels from the post-game if shuffle_postgame is false. if not world.options.shuffle_postgame: adjustment_linesets_in_order += self.handle_postgame(world) @@ -418,17 +520,6 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: if not victory == "challenge": adjustment_linesets_in_order.append(["Disabled Locations:", "0x0A332"]) - # Victory Condition - - if victory == "elevator": - self.VICTORY_LOCATION = "0x3D9A9" - elif victory == "challenge": - self.VICTORY_LOCATION = "0x0356B" - elif victory == "mountain_box_short": - self.VICTORY_LOCATION = "0x09F7F" - elif victory == "mountain_box_long": - self.VICTORY_LOCATION = "0xFFF00" - # Long box can usually only be solved by opening Mountain Entry. However, if it requires 7 lasers or less # (challenge_lasers <= 7), you can now solve it without opening Mountain Entry first. # Furthermore, if the user sets mountain_lasers > 7, the box is rotated to not require Mountain Entry either. @@ -526,13 +617,16 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: current_adjustment_type = line[:-1] continue + if current_adjustment_type is None: + raise ValueError(f"Adjustment lineset {adjustment_lineset} is malformed") + self.make_single_adjustment(current_adjustment_type, line) for entity_id in self.COMPLETELY_DISABLED_ENTITIES: if entity_id in self.DOOR_ITEMS_BY_ID: del self.DOOR_ITEMS_BY_ID[entity_id] - def discover_reachable_regions(self): + def discover_reachable_regions(self) -> Set[str]: """ Some options disable panels or remove specific items. This can make entire regions completely unreachable, because all their incoming connections are invalid. @@ -641,7 +735,7 @@ def reduce_connection_requirement(self, connection: Tuple[str, WitnessRule]) -> # Check each traversal option individually for option in connection[1]: - individual_entity_requirements = [] + individual_entity_requirements: List[WitnessRule] = [] for entity in option: # If a connection requires solving a disabled entity, it is not valid. if not self.solvability_guaranteed(entity) or entity in self.DISABLE_EVERYTHING_BEHIND: @@ -665,7 +759,7 @@ def reduce_connection_requirement(self, connection: Tuple[str, WitnessRule]) -> return logical_or_witness_rules(all_possibilities) - def make_dependency_reduced_checklist(self): + def make_dependency_reduced_checklist(self) -> None: """ Every entity has a requirement. This requirement may involve other entities. Example: Solving a panel powers a cable, and that cable turns on the next panel. @@ -680,12 +774,12 @@ def make_dependency_reduced_checklist(self): # Requirements are cached per entity. However, we might redo the whole reduction process multiple times. # So, we first clear this cache. - self.REQUIREMENTS_BY_HEX = dict() + self.REQUIREMENTS_BY_HEX = {} # We also clear any data structures that we might have filled in a previous dependency reduction - self.REQUIREMENTS_BY_HEX = dict() - self.USED_EVENT_NAMES_BY_HEX = dict() - self.CONNECTIONS_BY_REGION_NAME = dict() + self.REQUIREMENTS_BY_HEX = {} + self.USED_EVENT_NAMES_BY_HEX = {} + self.CONNECTIONS_BY_REGION_NAME = {} self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set() # Make independent requirements for entities @@ -696,22 +790,18 @@ def make_dependency_reduced_checklist(self): # Make independent region connection requirements based on the entities they require for region, connections in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL.items(): - self.CONNECTIONS_BY_REGION_NAME[region] = [] - - new_connections = [] + new_connections = set() for connection in connections: overall_requirement = self.reduce_connection_requirement(connection) # If there is a way to use this connection, add it. if overall_requirement: - new_connections.append((connection[0], overall_requirement)) + new_connections.add((connection[0], overall_requirement)) - # If there are any usable outgoing connections from this region, add them. - if new_connections: - self.CONNECTIONS_BY_REGION_NAME[region] = new_connections + self.CONNECTIONS_BY_REGION_NAME[region] = new_connections - def finalize_items(self): + def finalize_items(self) -> None: """ Finalise which items are used in the world, and handle their progressive versions. """ @@ -809,8 +899,7 @@ def make_event_item_pair(self, entity_hex: str) -> Tuple[str, str]: if entity_hex not in self.USED_EVENT_NAMES_BY_HEX: warning(f'Entity "{name}" does not have an associated event name.') self.USED_EVENT_NAMES_BY_HEX[entity_hex] = name + " Event" - pair = (name, self.USED_EVENT_NAMES_BY_HEX[entity_hex]) - return pair + return (name, self.USED_EVENT_NAMES_BY_HEX[entity_hex]) def make_event_panel_lists(self) -> None: """ @@ -829,85 +918,3 @@ def make_event_panel_lists(self) -> None: for panel in self.USED_EVENT_NAMES_BY_HEX: pair = self.make_event_item_pair(panel) self.EVENT_ITEM_PAIRS[pair[0]] = pair[1] - - def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]) -> None: - self.YAML_DISABLED_LOCATIONS = disabled_locations - self.YAML_ADDED_ITEMS = start_inv - - self.EVENT_PANELS_FROM_PANELS = set() - self.EVENT_PANELS_FROM_REGIONS = set() - - self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES = set() - - self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY = set() - - self.UNREACHABLE_REGIONS = set() - - self.THEORETICAL_ITEMS = set() - self.THEORETICAL_ITEMS_NO_MULTI = set() - self.MULTI_AMOUNTS = defaultdict(lambda: 1) - self.MULTI_LISTS = dict() - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set() - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = set() - self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {} - self.STARTING_INVENTORY = set() - - self.DIFFICULTY = world.options.puzzle_randomization - - if self.DIFFICULTY == "sigma_normal": - self.REFERENCE_LOGIC = static_witness_logic.sigma_normal - elif self.DIFFICULTY == "sigma_expert": - self.REFERENCE_LOGIC = static_witness_logic.sigma_expert - elif self.DIFFICULTY == "none": - self.REFERENCE_LOGIC = static_witness_logic.vanilla - - self.CONNECTIONS_BY_REGION_NAME_THEORETICAL = copy.deepcopy( - self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME - ) - self.CONNECTIONS_BY_REGION_NAME = dict() - self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) - self.REQUIREMENTS_BY_HEX = dict() - - self.EVENT_ITEM_PAIRS = dict() - self.COMPLETELY_DISABLED_ENTITIES = set() - self.DISABLE_EVERYTHING_BEHIND = set() - self.PRECOMPLETED_LOCATIONS = set() - self.EXCLUDED_LOCATIONS = set() - self.ADDED_CHECKS = set() - self.VICTORY_LOCATION = "0x0356B" - - self.ALWAYS_EVENT_NAMES_BY_HEX = { - "0x00509": "+1 Laser (Symmetry Laser)", - "0x012FB": "+1 Laser (Desert Laser)", - "0x09F98": "Desert Laser Redirection", - "0x01539": "+1 Laser (Quarry Laser)", - "0x181B3": "+1 Laser (Shadows Laser)", - "0x014BB": "+1 Laser (Keep Laser)", - "0x17C65": "+1 Laser (Monastery Laser)", - "0x032F9": "+1 Laser (Town Laser)", - "0x00274": "+1 Laser (Jungle Laser)", - "0x0C2B2": "+1 Laser (Bunker Laser)", - "0x00BF6": "+1 Laser (Swamp Laser)", - "0x028A4": "+1 Laser (Treehouse Laser)", - "0x17C34": "Mountain Entry", - "0xFFF00": "Bottom Floor Discard Turns On", - } - - self.USED_EVENT_NAMES_BY_HEX = {} - self.CONDITIONAL_EVENTS = {} - - # The basic requirements to solve each entity come from StaticWitnessLogic. - # However, for any given world, the options (e.g. which item shuffles are enabled) affect the requirements. - self.make_options_adjustments(world) - self.determine_unrequired_entities(world) - self.find_unsolvable_entities(world) - - # After we have adjusted the raw requirements, we perform a dependency reduction for the entity requirements. - # This will make the access conditions way faster, instead of recursively checking dependent entities each time. - self.make_dependency_reduced_checklist() - - # Finalize which items actually exist in the MultiWorld and which get grouped into progressive items. - self.finalize_items() - - # Create event-item pairs for specific panels in the game. - self.make_event_panel_lists() diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index 35f4e9544212..2528c8abe22b 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -9,9 +9,11 @@ from worlds.generic.Rules import CollectionRule +from .data import static_locations as static_witness_locations from .data import static_logic as static_witness_logic +from .data.static_logic import StaticWitnessLogicObj from .data.utils import WitnessRule, optimize_witness_rule -from .locations import WitnessPlayerLocations, static_witness_locations +from .locations import WitnessPlayerLocations from .player_logic import WitnessPlayerLogic if TYPE_CHECKING: @@ -21,8 +23,20 @@ class WitnessPlayerRegions: """Class that defines Witness Regions""" - player_locations = None - logic = None + def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None: + difficulty = world.options.puzzle_randomization + + self.reference_logic: StaticWitnessLogicObj + if difficulty == "sigma_normal": + self.reference_logic = static_witness_logic.sigma_normal + elif difficulty == "sigma_expert": + self.reference_logic = static_witness_logic.sigma_expert + else: + self.reference_logic = static_witness_logic.vanilla + + self.player_locations = player_locations + self.two_way_entrance_register: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: []) + self.created_region_names: Set[str] = set() @staticmethod def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> CollectionRule: @@ -36,7 +50,7 @@ def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> Collect return _meets_item_requirements(item_requirement, world) def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, req: WitnessRule, - regions_by_name: Dict[str, Region]): + regions_by_name: Dict[str, Region]) -> None: """ connect two regions and set the corresponding requirement """ @@ -89,8 +103,8 @@ def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic """ from . import create_region - all_locations = set() - regions_by_name = dict() + all_locations: Set[str] = set() + regions_by_name: Dict[str, Region] = {} regions_to_create = { k: v for k, v in self.reference_logic.ALL_REGIONS_BY_NAME.items() @@ -121,17 +135,3 @@ def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic for region_name, region in regions_to_create.items(): for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]: self.connect_if_possible(world, region_name, connection[0], connection[1], regions_by_name) - - def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None: - difficulty = world.options.puzzle_randomization - - if difficulty == "sigma_normal": - self.reference_logic = static_witness_logic.sigma_normal - elif difficulty == "sigma_expert": - self.reference_logic = static_witness_logic.sigma_expert - elif difficulty == "none": - self.reference_logic = static_witness_logic.vanilla - - self.player_locations = player_locations - self.two_way_entrance_register: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: []) - self.created_region_names: Set[str] = set() diff --git a/worlds/witness/ruff.toml b/worlds/witness/ruff.toml index d42361a4aaa9..a35711cce66d 100644 --- a/worlds/witness/ruff.toml +++ b/worlds/witness/ruff.toml @@ -1,10 +1,10 @@ line-length = 120 [lint] -select = ["E", "F", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"] -ignore = ["RUF012", "RUF100"] +select = ["C", "E", "F", "R", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"] +ignore = ["C9", "RUF012", "RUF100"] -[per-file-ignores] +[lint.per-file-ignores] # The way options definitions work right now, I am forced to break line length requirements. "options.py" = ["E501"] # The import list would just be so big if I imported every option individually in presets.py diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index b4982d1830b2..12a9a1ed4b59 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -37,8 +37,8 @@ def _has_laser(laser_hex: str, world: "WitnessWorld", player: int, redirect_requ _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)(state) and state.has("Desert Laser Redirection", player) ) - else: - return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations) + + return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations) def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> CollectionRule: @@ -63,8 +63,8 @@ def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logi if entity_name + " Solved" in player_locations.EVENT_LOCATION_TABLE: return lambda state: state.has(player_logic.EVENT_ITEM_PAIRS[entity_name + " Solved"], player) - else: - return make_lambda(panel, world) + + return make_lambda(panel, world) def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: @@ -175,12 +175,10 @@ def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: # We can get to Hedge 3 from Hedge 2. If we can get from Keep to Hedge 2, we're good. # This covers both Hedge 1 Exit and Hedge 2 Shortcut, because Hedge 1 is just part of the Keep region. - hedge_2_from_keep = any( + return any( e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 2nd Maze", "Keep"] ) - return hedge_2_from_keep - def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> bool: """ @@ -211,14 +209,12 @@ def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> # We also need a way from Town to Tunnels. - tunnels_from_town = ( + return ( any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Tunnels", "Windmill Interior"]) and any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Town", "Windmill Interior"]) or any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Tunnels", "Town"]) ) - return tunnels_from_town - def _has_item(item: str, world: "WitnessWorld", player: int, player_logic: WitnessPlayerLogic, player_locations: WitnessPlayerLocations) -> CollectionRule: @@ -237,9 +233,9 @@ def _has_item(item: str, world: "WitnessWorld", player: int, if item == "11 Lasers + Redirect": laser_req = world.options.challenge_lasers.value return _has_lasers(laser_req, world, True) - elif item == "PP2 Weirdness": + if item == "PP2 Weirdness": return lambda state: _can_do_expert_pp2(state, world) - elif item == "Theater to Tunnels": + if item == "Theater to Tunnels": return lambda state: _can_do_theater_to_tunnels(state, world) if item in player_logic.USED_EVENT_NAMES_BY_HEX: return _can_solve_panel(item, world, player, player_logic, player_locations) diff --git a/worlds/witness/test/__init__.py b/worlds/witness/test/__init__.py new file mode 100644 index 000000000000..0a24467feab2 --- /dev/null +++ b/worlds/witness/test/__init__.py @@ -0,0 +1,161 @@ +from test.bases import WorldTestBase +from test.general import gen_steps, setup_multiworld +from test.multiworld.test_multiworlds import MultiworldTestBase +from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union, cast + +from BaseClasses import CollectionState, Entrance, Item, Location, Region + +from .. import WitnessWorld + + +class WitnessTestBase(WorldTestBase): + game = "The Witness" + player: ClassVar[int] = 1 + + world: WitnessWorld + + def can_beat_game_with_items(self, items: Iterable[Item]) -> bool: + """ + Check that the items listed are enough to beat the game. + """ + + state = CollectionState(self.multiworld) + for item in items: + state.collect(item) + return state.multiworld.can_beat_game(state) + + def assert_dependency_on_event_item(self, spot: Union[Location, Region, Entrance], item_name: str) -> None: + """ + WorldTestBase.assertAccessDependency, but modified & simplified to work with event items + """ + event_items = [item for item in self.multiworld.get_items() if item.name == item_name] + self.assertTrue(event_items, f"Event item {item_name} does not exist.") + + event_locations = [cast(Location, event_item.location) for event_item in event_items] + + # Checking for an access dependency on an event item requires a bit of extra work, + # as state.remove forces a sweep, which will pick up the event item again right after we tried to remove it. + # So, we temporarily set the access rules of the event locations to be impossible. + original_rules = {event_location.name: event_location.access_rule for event_location in event_locations} + for event_location in event_locations: + event_location.access_rule = lambda _: False + + # We can't use self.assertAccessDependency here, it doesn't work for event items. (As of 2024-06-30) + test_state = self.multiworld.get_all_state(False) + + self.assertFalse(spot.can_reach(test_state), f"{spot.name} is reachable without {item_name}") + + test_state.collect(event_items[0]) + + self.assertTrue(spot.can_reach(test_state), f"{spot.name} is not reachable despite having {item_name}") + + # Restore original access rules. + for event_location in event_locations: + event_location.access_rule = original_rules[event_location.name] + + def assert_location_exists(self, location_name: str, strict_check: bool = True) -> None: + """ + Assert that a location exists in this world. + If strict_check, also make sure that this (non-event) location COULD exist. + """ + + if strict_check: + self.assertIn(location_name, self.world.location_name_to_id, f"Location {location_name} can never exist") + + try: + self.world.get_location(location_name) + except KeyError: + self.fail(f"Location {location_name} does not exist.") + + def assert_location_does_not_exist(self, location_name: str, strict_check: bool = True) -> None: + """ + Assert that a location exists in this world. + If strict_check, be explicit about whether the location could exist in the first place. + """ + + if strict_check: + self.assertIn(location_name, self.world.location_name_to_id, f"Location {location_name} can never exist") + + self.assertRaises( + KeyError, + lambda _: self.world.get_location(location_name), + f"Location {location_name} exists, but is not supposed to.", + ) + + def assert_can_beat_with_minimally(self, required_item_counts: Mapping[str, int]) -> None: + """ + Assert that the specified mapping of items is enough to beat the game, + and that having one less of any item would result in the game being unbeatable. + """ + # Find the actual items + found_items = [item for item in self.multiworld.get_items() if item.name in required_item_counts] + actual_items: Dict[str, List[Item]] = {item_name: [] for item_name in required_item_counts} + for item in found_items: + if len(actual_items[item.name]) < required_item_counts[item.name]: + actual_items[item.name].append(item) + + # Assert that enough items exist in the item pool to satisfy the specified required counts + for item_name, item_objects in actual_items.items(): + self.assertEqual( + len(item_objects), + required_item_counts[item_name], + f"Couldn't find {required_item_counts[item_name]} copies of item {item_name} available in the pool, " + f"only found {len(item_objects)}", + ) + + # assert that multiworld is beatable with the items specified + self.assertTrue( + self.can_beat_game_with_items(item for items in actual_items.values() for item in items), + f"Could not beat game with items: {required_item_counts}", + ) + + # assert that one less copy of any item would result in the multiworld being unbeatable + for item_name, item_objects in actual_items.items(): + with self.subTest(f"Verify cannot beat game with one less copy of {item_name}"): + removed_item = item_objects.pop() + self.assertFalse( + self.can_beat_game_with_items(item for items in actual_items.values() for item in items), + f"Game was beatable despite having {len(item_objects)} copies of {item_name} " + f"instead of the specified {required_item_counts[item_name]}", + ) + item_objects.append(removed_item) + + +class WitnessMultiworldTestBase(MultiworldTestBase): + options_per_world: List[Dict[str, Any]] + common_options: Dict[str, Any] = {} + + def setUp(self) -> None: + """ + Set up a multiworld with multiple players, each using different options. + """ + + self.multiworld = setup_multiworld([WitnessWorld] * len(self.options_per_world), ()) + + for world, options in zip(self.multiworld.worlds.values(), self.options_per_world): + for option_name, option_value in {**self.common_options, **options}.items(): + option = getattr(world.options, option_name) + self.assertIsNotNone(option) + + option.value = option.from_any(option_value).value + + self.assertSteps(gen_steps) + + def collect_by_name(self, item_names: Union[str, Iterable[str]], player: int) -> List[Item]: + """ + Collect all copies of a specified item name (or list of item names) for a player in the multiworld item pool. + """ + + items = self.get_items_by_name(item_names, player) + for item in items: + self.multiworld.state.collect(item) + return items + + def get_items_by_name(self, item_names: Union[str, Iterable[str]], player: int) -> List[Item]: + """ + Return all copies of a specified item name (or list of item names) for a player in the multiworld item pool. + """ + + if isinstance(item_names, str): + item_names = (item_names,) + return [item for item in self.multiworld.itempool if item.name in item_names and item.player == player] diff --git a/worlds/witness/test/test_auto_elevators.py b/worlds/witness/test/test_auto_elevators.py new file mode 100644 index 000000000000..16b1b5a56d37 --- /dev/null +++ b/worlds/witness/test/test_auto_elevators.py @@ -0,0 +1,66 @@ +from ..test import WitnessMultiworldTestBase, WitnessTestBase + + +class TestElevatorsComeToYou(WitnessTestBase): + options = { + "elevators_come_to_you": True, + "shuffle_doors": "mixed", + "shuffle_symbols": False, + } + + def test_bunker_laser(self) -> None: + """ + In elevators_come_to_you, Bunker can be entered from the back. + This means that you can access the laser with just Bunker Elevator Control (Panel). + It also means that you can, for example, access UV Room with the Control and the Elevator Room Entry Door. + """ + + self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", self.player)) + + self.collect_by_name("Bunker Elevator Control (Panel)") + + self.assertTrue(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Bunker UV Room 2", "Location", self.player)) + + self.collect_by_name("Bunker Elevator Room Entry (Door)") + self.collect_by_name("Bunker Drop-Down Door Controls (Panel)") + + self.assertTrue(self.multiworld.state.can_reach("Bunker UV Room 2", "Location", self.player)) + + +class TestElevatorsComeToYouBleed(WitnessMultiworldTestBase): + options_per_world = [ + { + "elevators_come_to_you": False, + }, + { + "elevators_come_to_you": True, + }, + { + "elevators_come_to_you": False, + }, + ] + + common_options = { + "shuffle_symbols": False, + "shuffle_doors": "panels", + } + + def test_correct_access_per_player(self) -> None: + """ + Test that in a multiworld with players that alternate the elevators_come_to_you option, + the actual behavior alternates as well and doesn't bleed over from slot to slot. + (This is essentially a "does connection info bleed over" test). + """ + + self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 1)) + self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 2)) + self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 3)) + + self.collect_by_name(["Bunker Elevator Control (Panel)"], 1) + self.collect_by_name(["Bunker Elevator Control (Panel)"], 2) + self.collect_by_name(["Bunker Elevator Control (Panel)"], 3) + + self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 1)) + self.assertTrue(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 2)) + self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 3)) diff --git a/worlds/witness/test/test_disable_non_randomized.py b/worlds/witness/test/test_disable_non_randomized.py new file mode 100644 index 000000000000..e7cb1597b2ba --- /dev/null +++ b/worlds/witness/test/test_disable_non_randomized.py @@ -0,0 +1,37 @@ +from ..rules import _has_lasers +from ..test import WitnessTestBase + + +class TestDisableNonRandomized(WitnessTestBase): + options = { + "disable_non_randomized_puzzles": True, + "shuffle_doors": "panels", + "early_symbol_item": False, + } + + def test_locations_got_disabled_and_alternate_activation_triggers_work(self) -> None: + """ + Test the different behaviors of the disable_non_randomized mode: + + 1. Unrandomized locations like Orchard Apple Tree 5 are disabled. + 2. Certain doors or lasers that would usually be activated by unrandomized panels depend on event items instead. + 3. These alternate activations are tied to solving Discarded Panels. + """ + + with self.subTest("Test that unrandomized locations are disabled."): + self.assert_location_does_not_exist("Orchard Apple Tree 5") + + with self.subTest("Test that alternate activation trigger events exist."): + self.assert_dependency_on_event_item( + self.world.get_entrance("Town Tower After Third Door to Town Tower Top"), + "Town Tower 4th Door Opens", + ) + + with self.subTest("Test that alternate activation triggers award lasers."): + self.assertFalse(_has_lasers(1, self.world, False)(self.multiworld.state)) + + self.collect_by_name("Triangles") + + # Alternate triggers yield Bunker Laser (Mountainside Discard) and Monastery Laser (Desert Discard) + self.assertTrue(_has_lasers(2, self.world, False)(self.multiworld.state)) + self.assertFalse(_has_lasers(3, self.world, False)(self.multiworld.state)) diff --git a/worlds/witness/test/test_door_shuffle.py b/worlds/witness/test/test_door_shuffle.py new file mode 100644 index 000000000000..0e38c32d69e2 --- /dev/null +++ b/worlds/witness/test/test_door_shuffle.py @@ -0,0 +1,24 @@ +from ..test import WitnessTestBase + + +class TestIndividualDoors(WitnessTestBase): + options = { + "shuffle_doors": "doors", + "door_groupings": "off", + } + + def test_swamp_laser_shortcut(self) -> None: + """ + Test that Door Shuffle grants early access to Swamp Laser from the back shortcut. + """ + + self.assertTrue(self.get_items_by_name("Swamp Laser Shortcut (Door)")) + + self.assertAccessDependency( + ["Swamp Laser Panel"], + [ + ["Swamp Laser Shortcut (Door)"], + ["Swamp Red Underwater Exit (Door)"], + ], + only_check_listed=True, + ) diff --git a/worlds/witness/test/test_ep_shuffle.py b/worlds/witness/test/test_ep_shuffle.py new file mode 100644 index 000000000000..342390916675 --- /dev/null +++ b/worlds/witness/test/test_ep_shuffle.py @@ -0,0 +1,54 @@ +from ..test import WitnessTestBase + + +class TestIndividualEPs(WitnessTestBase): + options = { + "shuffle_EPs": "individual", + "EP_difficulty": "normal", + "obelisk_keys": True, + "disable_non_randomized_puzzles": True, + "shuffle_postgame": False, + "victory_condition": "mountain_box_short", + "early_caves": "off", + } + + def test_correct_eps_exist_and_are_locked(self) -> None: + """ + Test that EP locations exist in shuffle_EPs, but only the ones that actually should (based on options) + """ + + # Test Tutorial First Hallways EP as a proxy for "EPs exist at all" + # Don't wrap in a subtest - If this fails, there is no point. + self.assert_location_exists("Tutorial First Hallway EP") + + with self.subTest("Test that disable_non_randomized disables Monastery Garden Left EP"): + self.assert_location_does_not_exist("Monastery Garden Left EP") + + with self.subTest("Test that shuffle_postgame being off disables postgame EPs."): + self.assert_location_does_not_exist("Caves Skylight EP") + + with self.subTest("Test that ep_difficulty being set to normal excludes tedious EPs."): + self.assert_location_does_not_exist("Shipwreck Couch EP") + + with self.subTest("Test that EPs are being locked by Obelisk Keys."): + self.assertAccessDependency(["Desert Sand Snake EP"], [["Desert Obelisk Key"]], True) + + +class TestObeliskSides(WitnessTestBase): + options = { + "shuffle_EPs": "obelisk_sides", + "EP_difficulty": "eclipse", + "shuffle_vault_boxes": True, + "shuffle_postgame": True, + } + + def test_eclipse_required_for_town_side_6(self) -> None: + """ + Test that Obelisk Sides require the appropriate event items from the individual EPs. + Specifically, assert that Town Obelisk Side 6 needs Theater Eclipse EP. + This doubles as a test for Theater Eclipse EP existing with the right options. + """ + + self.assert_dependency_on_event_item( + self.world.get_location("Town Obelisk Side 6"), "Town Obelisk Side 6 - Theater Eclipse EP" + ) diff --git a/worlds/witness/test/test_lasers.py b/worlds/witness/test/test_lasers.py new file mode 100644 index 000000000000..f09897ce4053 --- /dev/null +++ b/worlds/witness/test/test_lasers.py @@ -0,0 +1,185 @@ +from ..test import WitnessTestBase + + +class TestSymbolsRequiredToWinElevatorNormal(WitnessTestBase): + options = { + "shuffle_lasers": True, + "puzzle_randomization": "sigma_normal", + "mountain_lasers": 1, + "victory_condition": "elevator", + "early_symbol_item": False, + } + + def test_symbols_to_win(self) -> None: + """ + In symbol shuffle, the only way to reach the Elevator is through Mountain Entry by descending the Mountain. + This requires a very specific set of symbol items per puzzle randomization mode. + In this case, we check Sigma Normal Puzzles. + """ + + exact_requirement = { + "Monastery Laser": 1, + "Progressive Dots": 2, + "Progressive Stars": 2, + "Progressive Symmetry": 2, + "Black/White Squares": 1, + "Colored Squares": 1, + "Shapers": 1, + "Rotated Shapers": 1, + "Eraser": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + +class TestSymbolsRequiredToWinElevatorExpert(WitnessTestBase): + options = { + "shuffle_lasers": True, + "mountain_lasers": 1, + "victory_condition": "elevator", + "early_symbol_item": False, + "puzzle_randomization": "sigma_expert", + } + + def test_symbols_to_win(self) -> None: + """ + In symbol shuffle, the only way to reach the Elevator is through Mountain Entry by descending the Mountain. + This requires a very specific set of symbol items per puzzle randomization mode. + In this case, we check Sigma Expert Puzzles. + """ + + exact_requirement = { + "Monastery Laser": 1, + "Progressive Dots": 2, + "Progressive Stars": 2, + "Progressive Symmetry": 2, + "Black/White Squares": 1, + "Colored Squares": 1, + "Shapers": 1, + "Rotated Shapers": 1, + "Negative Shapers": 1, + "Eraser": 1, + "Triangles": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + +class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase): + options = { + "shuffle_lasers": True, + "mountain_lasers": 1, + "victory_condition": "elevator", + "early_symbol_item": False, + "puzzle_randomization": "none", + } + + def test_symbols_to_win(self) -> None: + """ + In symbol shuffle, the only way to reach the Elevator is through Mountain Entry by descending the Mountain. + This requires a very specific set of symbol items per puzzle randomization mode. + In this case, we check Vanilla Puzzles. + """ + + exact_requirement = { + "Monastery Laser": 1, + "Progressive Dots": 2, + "Progressive Stars": 2, + "Progressive Symmetry": 1, + "Black/White Squares": 1, + "Colored Squares": 1, + "Shapers": 1, + "Rotated Shapers": 1, + "Eraser": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + +class TestPanelsRequiredToWinElevator(WitnessTestBase): + options = { + "shuffle_lasers": True, + "mountain_lasers": 1, + "victory_condition": "elevator", + "early_symbol_item": False, + "shuffle_symbols": False, + "shuffle_doors": "panels", + "door_groupings": "off", + } + + def test_panels_to_win(self) -> None: + """ + In door panel shuffle , the only way to reach the Elevator is through Mountain Entry by descending the Mountain. + This requires some control panels for each of the Mountain Floors. + """ + + exact_requirement = { + "Desert Laser": 1, + "Town Desert Laser Redirect Control (Panel)": 1, + "Mountain Floor 1 Light Bridge (Panel)": 1, + "Mountain Floor 2 Light Bridge Near (Panel)": 1, + "Mountain Floor 2 Light Bridge Far (Panel)": 1, + "Mountain Floor 2 Elevator Control (Panel)": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + +class TestDoorsRequiredToWinElevator(WitnessTestBase): + options = { + "shuffle_lasers": True, + "mountain_lasers": 1, + "victory_condition": "elevator", + "early_symbol_item": False, + "shuffle_symbols": False, + "shuffle_doors": "doors", + "door_groupings": "off", + } + + def test_doors_to_elevator_paths(self) -> None: + """ + In remote door shuffle, there are three ways to win. + + - Through the normal route (Mountain Entry -> Descend through Mountain -> Reach Bottom Floor) + - Through the Caves using the Caves Shortcuts (Caves -> Reach Bottom Floor) + - Through the Caves via Challenge (Tunnels -> Challenge -> Caves -> Reach Bottom Floor) + """ + + with self.subTest("Test Elevator victory in shuffle_doors through Mountain Entry."): + exact_requirement = { + "Monastery Laser": 1, + "Mountain Floor 1 Exit (Door)": 1, + "Mountain Floor 2 Staircase Near (Door)": 1, + "Mountain Floor 2 Staircase Far (Door)": 1, + "Mountain Floor 2 Exit (Door)": 1, + "Mountain Bottom Floor Giant Puzzle Exit (Door)": 1, + "Mountain Bottom Floor Pillars Room Entry (Door)": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + with self.subTest("Test Elevator victory in shuffle_doors through Caves Shortcuts."): + exact_requirement = { + "Monastery Laser": 1, # Elevator Panel itself has a laser lock + "Caves Mountain Shortcut (Door)": 1, + "Caves Entry (Door)": 1, + "Mountain Bottom Floor Rock (Door)": 1, + "Mountain Bottom Floor Pillars Room Entry (Door)": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + with self.subTest("Test Elevator victory in shuffle_doors through Tunnels->Challenge->Caves."): + exact_requirement = { + "Monastery Laser": 1, # Elevator Panel itself has a laser lock + "Windmill Entry (Door)": 1, + "Tunnels Theater Shortcut (Door)": 1, + "Tunnels Entry (Door)": 1, + "Challenge Entry (Door)": 1, + "Caves Pillar Door": 1, + "Caves Entry (Door)": 1, + "Mountain Bottom Floor Rock (Door)": 1, + "Mountain Bottom Floor Pillars Room Entry (Door)": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) diff --git a/worlds/witness/test/test_roll_other_options.py b/worlds/witness/test/test_roll_other_options.py new file mode 100644 index 000000000000..71743c326038 --- /dev/null +++ b/worlds/witness/test/test_roll_other_options.py @@ -0,0 +1,58 @@ +from ..test import WitnessTestBase + +# These are just some random options combinations, just to catch whether I broke anything obvious + + +class TestExpertNonRandomizedEPs(WitnessTestBase): + options = { + "disable_non_randomized": True, + "puzzle_randomization": "sigma_expert", + "shuffle_EPs": "individual", + "ep_difficulty": "eclipse", + "victory_condition": "challenge", + "shuffle_discarded_panels": False, + "shuffle_boat": False, + } + + +class TestVanillaAutoElevatorsPanels(WitnessTestBase): + options = { + "puzzle_randomization": "none", + "elevators_come_to_you": True, + "shuffle_doors": "panels", + "victory_condition": "mountain_box_short", + "early_caves": True, + "shuffle_vault_boxes": True, + "mountain_lasers": 11, + } + + +class TestMiscOptions(WitnessTestBase): + options = { + "death_link": True, + "death_link_amnesty": 3, + "laser_hints": True, + "hint_amount": 40, + "area_hint_percentage": 100, + } + + +class TestMaxEntityShuffle(WitnessTestBase): + options = { + "shuffle_symbols": False, + "shuffle_doors": "mixed", + "shuffle_EPs": "individual", + "obelisk_keys": True, + "shuffle_lasers": "anywhere", + "victory_condition": "mountain_box_long", + } + + +class TestPostgameGroupedDoors(WitnessTestBase): + options = { + "shuffle_postgame": True, + "shuffle_discarded_panels": True, + "shuffle_doors": "doors", + "door_groupings": "regional", + "victory_condition": "elevator", + } diff --git a/worlds/witness/test/test_symbol_shuffle.py b/worlds/witness/test/test_symbol_shuffle.py new file mode 100644 index 000000000000..8012480075a7 --- /dev/null +++ b/worlds/witness/test/test_symbol_shuffle.py @@ -0,0 +1,74 @@ +from ..test import WitnessMultiworldTestBase, WitnessTestBase + + +class TestSymbols(WitnessTestBase): + options = { + "early_symbol_item": False, + } + + def test_progressive_symbols(self) -> None: + """ + Test that Dots & Full Dots are correctly replaced by 2x Progressive Dots, + and test that Dots puzzles and Full Dots puzzles require 1 and 2 copies of this item respectively. + """ + + progressive_dots = self.get_items_by_name("Progressive Dots") + self.assertEqual(len(progressive_dots), 2) + + self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + self.assertFalse( + self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player) + ) + + self.collect(progressive_dots.pop()) + + self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + self.assertFalse( + self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player) + ) + + self.collect(progressive_dots.pop()) + + self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + self.assertTrue( + self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player) + ) + + +class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase): + options_per_world = [ + { + "puzzle_randomization": "sigma_normal", + }, + { + "puzzle_randomization": "sigma_expert", + }, + { + "puzzle_randomization": "none", + }, + ] + + common_options = { + "shuffle_discarded_panels": True, + "early_symbol_item": False, + } + + def test_arrows_exist_and_are_required_in_expert_seeds_only(self) -> None: + """ + In sigma_expert, Discarded Panels require Arrows. + In sigma_normal, Discarded Panels require Triangles, and Arrows shouldn't exist at all as an item. + """ + + with self.subTest("Test that Arrows exist only in the expert seed."): + self.assertFalse(self.get_items_by_name("Arrows", 1)) + self.assertTrue(self.get_items_by_name("Arrows", 2)) + self.assertFalse(self.get_items_by_name("Arrows", 3)) + + with self.subTest("Test that Discards ask for Triangles in normal, but Arrows in expert."): + desert_discard = "0x17CE7" + triangles = frozenset({frozenset({"Triangles"})}) + arrows = frozenset({frozenset({"Arrows"})}) + + self.assertEqual(self.multiworld.worlds[1].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles) + self.assertEqual(self.multiworld.worlds[2].player_logic.REQUIREMENTS_BY_HEX[desert_discard], arrows) + self.assertEqual(self.multiworld.worlds[3].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles) diff --git a/worlds/yoshisisland/Client.py b/worlds/yoshisisland/Client.py index 2a710b046a70..9b9e0ff52b87 100644 --- a/worlds/yoshisisland/Client.py +++ b/worlds/yoshisisland/Client.py @@ -87,9 +87,7 @@ async def game_watcher(self, ctx: "SNIContext") -> None: if game_mode is None: return - elif goal_flag[0] != 0x00: - await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) - ctx.finished_game = True + elif game_mode[0] not in VALID_GAME_STATES: return elif item_received[0] > 0x00: @@ -101,6 +99,10 @@ async def game_watcher(self, ctx: "SNIContext") -> None: ctx.rom = None return + if goal_flag[0] != 0x00: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + new_checks = [] from .Rom import location_table @@ -116,7 +118,7 @@ async def game_watcher(self, ctx: "SNIContext") -> None: for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names.lookup_in_slot(new_check_id) + location = ctx.location_names.lookup_in_game(new_check_id) total_locations = len(ctx.missing_locations) + len(ctx.checked_locations) snes_logger.info(f"New Check: {location} ({len(ctx.locations_checked)}/{total_locations})") await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [new_check_id]}]) @@ -127,7 +129,7 @@ async def game_watcher(self, ctx: "SNIContext") -> None: item = ctx.items_received[recv_index] recv_index += 1 logging.info("Received %s from %s (%s) (%d/%d in list)" % ( - color(ctx.item_names.lookup_in_slot(item.item), "red", "bold"), + color(ctx.item_names.lookup_in_game(item.item), "red", "bold"), color(ctx.player_names[item.player], "yellow"), ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 205cc9ad6ba1..cf61d93ca4ce 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -145,10 +145,10 @@ def generate_early(self) -> None: self._item_counts = item_counts with redirect_stdout(self.lsi): # type: ignore - self.zz_system.make_randomizer(zz_op) - - self.zz_system.seed(self.multiworld.seed) + self.zz_system.set_options(zz_op) + self.zz_system.seed(self.random.randrange(1999999999)) self.zz_system.make_map() + self.zz_system.make_randomizer() # just in case the options changed anything (I don't think they do) assert self.zz_system.randomizer, "init failed" diff --git a/worlds/zillion/client.py b/worlds/zillion/client.py index be32028463c7..09d0565e1c5e 100644 --- a/worlds/zillion/client.py +++ b/worlds/zillion/client.py @@ -347,6 +347,11 @@ def process_from_game_queue(self) -> None: "operations": [{"operation": "replace", "value": doors_b64}] } async_start(self.send_msgs([payload])) + elif isinstance(event_from_game, events.MapEventFromGame): + row = event_from_game.map_index // 8 + col = event_from_game.map_index % 8 + room_name = f"({chr(row + 64)}-{col + 1})" + logger.info(f"You are at {room_name}") else: logger.warning(f"WARNING: unhandled event from game {event_from_game}") diff --git a/worlds/zillion/gen_data.py b/worlds/zillion/gen_data.py index aa24ff8961b3..13cbee9ced20 100644 --- a/worlds/zillion/gen_data.py +++ b/worlds/zillion/gen_data.py @@ -28,6 +28,13 @@ def to_json(self) -> str: def from_json(gen_data_str: str) -> "GenData": """ the reverse of `to_json` """ from_json = json.loads(gen_data_str) + + # backwards compatibility for seeds generated before new map_gen options + room_gen = from_json["zz_game"]["options"].get("room_gen", None) + if room_gen is not None: + from_json["zz_game"]["options"]["map_gen"] = {False: "none", True: "rooms"}.get(room_gen, "none") + del from_json["zz_game"]["options"]["room_gen"] + return GenData( from_json["multi_items"], ZzGame.from_jsonable(from_json["zz_game"]), diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index d75dd1a1c22c..5de0b65c82f0 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -1,9 +1,9 @@ from collections import Counter from dataclasses import dataclass -from typing import ClassVar, Dict, Tuple +from typing import ClassVar, Dict, Literal, Tuple from typing_extensions import TypeGuard # remove when Python >= 3.10 -from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Toggle +from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle from zilliandomizer.options import ( Options as ZzOptions, char_to_gun, char_to_jump, ID, @@ -251,9 +251,25 @@ class ZillionStartingCards(NamedRange): } -class ZillionRoomGen(Toggle): - """ whether to generate rooms with random terrain """ - display_name = "room generation" +class ZillionMapGen(Choice): + """ + - none: vanilla map + - rooms: random terrain inside rooms, but path through base is vanilla + - full: random path through base + """ + display_name = "map generation" + option_none = 0 + option_rooms = 1 + option_full = 2 + default = 0 + + def zz_value(self) -> Literal['none', 'rooms', 'full']: + if self.value == ZillionMapGen.option_none: + return "none" + if self.value == ZillionMapGen.option_rooms: + return "rooms" + assert self.value == ZillionMapGen.option_full + return "full" @dataclass @@ -276,7 +292,9 @@ class ZillionOptions(PerGameCommonOptions): early_scope: ZillionEarlyScope skill: ZillionSkill starting_cards: ZillionStartingCards - room_gen: ZillionRoomGen + map_gen: ZillionMapGen + + room_gen: Removed z_option_groups = [ @@ -375,7 +393,7 @@ def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": starting_cards = options.starting_cards - room_gen = options.room_gen + map_gen = options.map_gen.zz_value() zz_item_counts = convert_item_counts(item_counts) zz_op = ZzOptions( @@ -393,7 +411,7 @@ def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": bool(options.early_scope.value), True, # balance defense starting_cards.value, - bool(room_gen.value) + map_gen ) zz_validate(zz_op) return zz_op, item_counts diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt index ae7d9b173308..d6b01ac107ae 100644 --- a/worlds/zillion/requirements.txt +++ b/worlds/zillion/requirements.txt @@ -1,2 +1,2 @@ -zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@1dd2ce01c9d818caba5844529699b3ad026d6a07#0.7.1 +zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@33045067f626266850f91c8045b9d3a9f52d02b0#0.9.0 typing-extensions>=4.7, <5 diff --git a/worlds/zillion/test/TestOptions.py b/worlds/zillion/test/TestOptions.py index c4f02d4bd3be..3820c32dd016 100644 --- a/worlds/zillion/test/TestOptions.py +++ b/worlds/zillion/test/TestOptions.py @@ -1,6 +1,6 @@ from . import ZillionTestBase -from worlds.zillion.options import ZillionJumpLevels, ZillionGunLevels, ZillionOptions, validate +from ..options import ZillionJumpLevels, ZillionGunLevels, ZillionOptions, validate from zilliandomizer.options import VBLR_CHOICES diff --git a/worlds/zillion/test/TestReproducibleRandom.py b/worlds/zillion/test/TestReproducibleRandom.py index 392db657d90a..a92fae240709 100644 --- a/worlds/zillion/test/TestReproducibleRandom.py +++ b/worlds/zillion/test/TestReproducibleRandom.py @@ -1,7 +1,7 @@ from typing import cast from . import ZillionTestBase -from worlds.zillion import ZillionWorld +from .. import ZillionWorld class SeedTest(ZillionTestBase): diff --git a/worlds/zillion/test/__init__.py b/worlds/zillion/test/__init__.py index 93c0512fb045..fe62bae34c9e 100644 --- a/worlds/zillion/test/__init__.py +++ b/worlds/zillion/test/__init__.py @@ -1,6 +1,6 @@ from typing import cast from test.bases import WorldTestBase -from worlds.zillion import ZillionWorld +from .. import ZillionWorld class ZillionTestBase(WorldTestBase):